mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-25 15:44:04 -06:00
commit
3c6f31b81f
272 changed files with 76023 additions and 68799 deletions
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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@"
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
#
|
#
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.")
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
3
plugins/Toolbox/resources/images/shop.svg
Normal file
3
plugins/Toolbox/resources/images/shop.svg
Normal 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 |
|
@ -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
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
51
plugins/Toolbox/src/CloudSync/CloudApiClient.py
Normal file
51
plugins/Toolbox/src/CloudSync/CloudApiClient.py
Normal 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)
|
|
@ -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")
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue