diff --git a/cmake/CuraTests.cmake b/cmake/CuraTests.cmake index b1d3e0ddc4..251bec5781 100644 --- a/cmake/CuraTests.cmake +++ b/cmake/CuraTests.cmake @@ -56,6 +56,13 @@ function(cura_add_test) endif() 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}") file(GLOB_RECURSE _plugins plugins/*/__init__.py) diff --git a/cura/API/__init__.py b/cura/API/__init__.py index b3e702263a..26c9a4c829 100644 --- a/cura/API/__init__.py +++ b/cura/API/__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. # 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): - if cls.__instance is None: - if application is None: - raise Exception("Upon first time creation, the application must be set.") - cls.__instance = super(CuraAPI, cls).__new__(cls) - cls._application = application + if cls.__instance is not None: + raise RuntimeError("Tried to create singleton '{class_name}' more than once.".format(class_name = CuraAPI.__name__)) + if application is None: + raise RuntimeError("Upon first time creation, the application must be set.") + cls.__instance = super(CuraAPI, cls).__new__(cls) + cls._application = application return cls.__instance def __init__(self, application: Optional["CuraApplication"] = None) -> None: diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 9ccdcaf64d..4d24a46384 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -145,6 +145,14 @@ class Backup: # \return Whether we had success or not. @staticmethod 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) Resources.factoryReset() Logger.log("d", "Extracting backup to location: %s", target_path) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index f778cb0fab..10dde65b2f 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -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. import os @@ -191,8 +191,6 @@ class CuraApplication(QtApplication): self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions] - self._cura_package_manager = None - self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager] self.empty_container = None # type: EmptyInstanceContainer @@ -632,6 +630,12 @@ class CuraApplication(QtApplication): def showPreferences(self) -> None: 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) def getGlobalContainerStack(self) -> Optional["GlobalStack"]: return self._global_container_stack @@ -1827,15 +1831,21 @@ class CuraApplication(QtApplication): 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. - if not Selection.hasSelection(): - node = self.getController().getScene().findObject(cast(SelectionPass, self.getRenderer().getRenderPass("selection")).getIdAtPosition(x, y)) - if node: - parent = node.getParent() - while(parent and parent.callDecoration("isGroup")): - node = parent - parent = node.getParent() + if Selection.hasSelection(): + return + selection_pass = cast(SelectionPass, self.getRenderer().getRenderPass("selection")) + if not selection_pass: # If you right-click before the rendering has been initialised there might not be a selection pass yet. + print("--------------ding! Got the crash.") + return + 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() def showMoreInformationDialogForAnonymousDataCollection(self): diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index 9fc01ba50b..cc809abf05 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -115,9 +115,10 @@ class AuthorizationHelpers: ) @staticmethod - ## Generate a 16-character verification code. - # \param code_length: How long should the code be? - def generateVerificationCode(code_length: int = 16) -> str: + ## Generate a verification code of arbitrary length. + # \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to + # leave it at 32 + def generateVerificationCode(code_length: int = 32) -> str: return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) @staticmethod diff --git a/cura/OAuth2/AuthorizationRequestHandler.py b/cura/OAuth2/AuthorizationRequestHandler.py index 83b94ed586..b002039491 100644 --- a/cura/OAuth2/AuthorizationRequestHandler.py +++ b/cura/OAuth2/AuthorizationRequestHandler.py @@ -25,6 +25,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]] 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. def do_HEAD(self) -> None: self.do_GET() @@ -58,7 +60,14 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): # \return HTTP ResponseData containing a success page to show to the user. def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: 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. token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode( code, self.verification_code) diff --git a/cura/OAuth2/AuthorizationRequestServer.py b/cura/OAuth2/AuthorizationRequestServer.py index 51a8ceba77..687bbf5ad8 100644 --- a/cura/OAuth2/AuthorizationRequestServer.py +++ b/cura/OAuth2/AuthorizationRequestServer.py @@ -25,3 +25,6 @@ class AuthorizationRequestServer(HTTPServer): ## Set the verification code on the request handler. def setVerificationCode(self, verification_code: str) -> None: self.RequestHandlerClass.verification_code = verification_code # type: ignore + + def setState(self, state: str) -> None: + self.RequestHandlerClass.state = state # type: ignore diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 0848623410..13e0e50373 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -153,13 +153,15 @@ class AuthorizationService: verification_code = self._auth_helpers.generateVerificationCode() challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code) + state = AuthorizationHelpers.generateVerificationCode() + # Create the query string needed for the OAuth2 flow. query_string = urlencode({ "client_id": self._settings.CLIENT_ID, "redirect_uri": self._settings.CALLBACK_URL, "scope": self._settings.CLIENT_SCOPES, "response_type": "code", - "state": "(.Y.)", + "state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020) "code_challenge": challenge_code, "code_challenge_method": "S512" }) @@ -168,7 +170,7 @@ class AuthorizationService: QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string))) # 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. def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: diff --git a/cura/OAuth2/LocalAuthorizationServer.py b/cura/OAuth2/LocalAuthorizationServer.py index a80b0deb28..780adf54aa 100644 --- a/cura/OAuth2/LocalAuthorizationServer.py +++ b/cura/OAuth2/LocalAuthorizationServer.py @@ -36,7 +36,8 @@ class LocalAuthorizationServer: ## Starts the local web server to handle the authorization callback. # \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 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. @@ -53,6 +54,7 @@ class LocalAuthorizationServer: self._web_server.setAuthorizationHelpers(self._auth_helpers) self._web_server.setAuthorizationCallback(self._auth_state_changed_callback) self._web_server.setVerificationCode(verification_code) + self._web_server.setState(state) # Start the server on a new thread. self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 31ea691e76..f78a21aaad 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -747,6 +747,11 @@ class MachineManager(QObject): result = [] # type: List[str] for setting_instance in container.findInstances(): 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"): continue diff --git a/cura_app.py b/cura_app.py index cb97792662..5184a5ab8b 100755 --- a/cura_app.py +++ b/cura_app.py @@ -29,9 +29,13 @@ parser.add_argument("--debug", known_args = vars(parser.parse_known_args()[0]) if with_sentry_sdk: - sentry_env = "production" - if ApplicationMetadata.CuraVersion == "master": + sentry_env = "unknown" # Start off with a "IDK" + if hasattr(sys, "frozen"): + sentry_env = "production" # A frozen build is a "real" distribution. + elif ApplicationMetadata.CuraVersion == "master": sentry_env = "development" + elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion: + sentry_env = "beta" try: if ApplicationMetadata.CuraVersion.split(".")[2] == "99": sentry_env = "nightly" diff --git a/docker/build.sh b/docker/build.sh index 5b035ca08a..a500663c64 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -13,6 +13,8 @@ export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}" cd "${PROJECT_DIR}" + + # # Clone Uranium and set PYTHONPATH first # diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 2e395c9efa..be90f02d37 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -4,7 +4,8 @@ from configparser import ConfigParser import zipfile 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 @@ -732,7 +733,25 @@ class ThreeMFWorkspaceReader(WorkspaceReader): base_file_name = os.path.basename(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): if self._machine_info.quality_changes_info is None: diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 33df0bfe90..10a21f445b 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -73,11 +73,25 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): version_config_parser.write(version_file_string) archive.writestr(version_file, version_file_string.getvalue()) + self._writePluginMetadataToArchive(archive) + # Close the archive & reset states. archive.close() mesh_writer.setStoreArchive(False) 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. # \param container That follows the \type{ContainerInterface} to archive. # \param archive The archive to write to. diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 640aabd30c..9860804542 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -1,5 +1,6 @@ # Copyright (c) 2015 Ultimaker B.V. # Uranium is released under the terms of the LGPLv3 or higher. +from typing import Optional from UM.Mesh.MeshWriter import MeshWriter from UM.Math.Vector import Vector @@ -40,7 +41,7 @@ class ThreeMFWriter(MeshWriter): } self._unit_matrix_string = self._convertMatrixToString(Matrix()) - self._archive = None + self._archive = None # type: Optional[zipfile.ZipFile] self._store_archive = False def _convertMatrixToString(self, matrix): diff --git a/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py b/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py index 001beecd3b..cbd131f17e 100644 --- a/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py +++ b/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py @@ -72,18 +72,25 @@ class DisplayFilenameAndLayerOnLCD(Script): lcd_text = "M117 Printing " + name + " - Layer " i = self.getSettingValueByKey("startNum") for layer in data: - display_text = lcd_text + str(i) + " " + name + display_text = lcd_text + str(i) layer_index = data.index(layer) lines = layer.split("\n") for line in lines: if line.startswith(";LAYER_COUNT:"): max_layer = line max_layer = max_layer.split(":")[1] + if self.getSettingValueByKey("startNum") == 0: + max_layer = str(int(max_layer) - 1) if line.startswith(";LAYER:"): if self.getSettingValueByKey("maxlayer"): display_text = display_text + " of " + max_layer + if not self.getSettingValueByKey("scroll"): + display_text = display_text + " " + name 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) lines.insert(line_index + 1, display_text) i += 1 diff --git a/plugins/PostProcessingPlugin/scripts/TimeLapse.py b/plugins/PostProcessingPlugin/scripts/TimeLapse.py index 53e55a9454..427f80315d 100644 --- a/plugins/PostProcessingPlugin/scripts/TimeLapse.py +++ b/plugins/PostProcessingPlugin/scripts/TimeLapse.py @@ -2,6 +2,7 @@ from ..Script import Script + class TimeLapse(Script): def __init__(self): super().__init__() @@ -75,21 +76,29 @@ class TimeLapse(Script): trigger_command = self.getSettingValueByKey("trigger_command") pause_length = self.getSettingValueByKey("pause_length") gcode_to_append = ";TimeLapse Begin\n" + last_x = 0 + last_y = 0 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(M = 400) + " ;Wait for moves to finish\n" + 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(M=400) + " ;Wait for moves to finish\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 += ";TimeLapse End\n" - for layer in data: + gcode_to_append += self.putValue(G=4, P=pause_length) + " ;Wait for camera\n" + + 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 lines = layer.split("\n") for line in lines: if ";LAYER:" in line: - index = data.index(layer) layer += gcode_to_append - data[index] = layer + layer += "G0 X%s Y%s\n" % (last_x, last_y) + + data[idx] = layer break return data diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 3a860a3055..adbeb7b6db 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -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. 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._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")) + 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 layer data")) QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated) @@ -149,6 +150,7 @@ class SimulationView(CuraView): if self._activity == activity: return self._activity = activity + self._updateSliceWarningVisibility() self.activityChanged.emit() def getSimulationPass(self) -> SimulationPass: @@ -543,11 +545,13 @@ class SimulationView(CuraView): self._composite_pass.getLayerBindings().append("simulationview") self._old_composite_shader = self._composite_pass.getCompositeShader() self._composite_pass.setCompositeShader(self._simulationview_composite_shader) + self._updateSliceWarningVisibility() elif event.type == Event.ViewDeactivateEvent: self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged) Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged) self._wireprint_warning_message.hide() + self._slice_first_warning_message.hide() Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) @@ -661,6 +665,12 @@ class SimulationView(CuraView): 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): def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None: diff --git a/plugins/SliceInfoPlugin/MoreInfoWindow.qml b/plugins/SliceInfoPlugin/MoreInfoWindow.qml index 247df4b025..3a6b6c8741 100644 --- a/plugins/SliceInfoPlugin/MoreInfoWindow.qml +++ b/plugins/SliceInfoPlugin/MoreInfoWindow.qml @@ -72,6 +72,7 @@ Window 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:") + color: UM.Theme.getColor("text") wrapMode: Text.WordWrap renderType: Text.NativeRendering } diff --git a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml b/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml index 06c1102811..17bbdba4c5 100644 --- a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml +++ b/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml @@ -20,6 +20,8 @@ UM.Dialog{ maximumHeight: minimumHeight margin: 0 + property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next") + Rectangle { id: root @@ -90,7 +92,7 @@ UM.Dialog{ Label { 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 color: UM.Theme.getColor("text") height: contentHeight + UM.Theme.getSize("default_margin").height @@ -125,26 +127,6 @@ UM.Dialog{ color: UM.Theme.getColor("text") 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 - Cura.ActionButton + Cura.PrimaryButton { id: nextButton anchors.bottom: parent.bottom anchors.right: parent.right anchors.margins: UM.Theme.getSize("default_margin").height - text: catalog.i18nc("@button", "Next") + text: actionButtonText onClicked: accept() + leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width + rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width } } } diff --git a/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml b/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml index 2c88ac6d5f..dd0237eada 100644 --- a/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml +++ b/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml @@ -4,12 +4,12 @@ import QtQuick 2.10 import QtQuick.Dialogs 1.1 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 -// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles - import UM 1.1 as UM +import Cura 1.6 as Cura UM.Dialog { @@ -19,50 +19,90 @@ UM.Dialog minimumHeight: UM.Theme.getSize("license_window_minimum").height width: minimumWidth height: minimumHeight + backgroundColor: UM.Theme.getColor("main_background") + margin: screenScaleFactor * 10 - Item + ColumnLayout { anchors.fill: parent + spacing: UM.Theme.getSize("thick_margin").height UM.I18nCatalog{id: catalog; name: "cura"} - Label { id: licenseHeader - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right - text: licenseModel.headerText + Layout.fillWidth: true + text: catalog.i18nc("@label", "You need to accept the license to install the package") + color: UM.Theme.getColor("text") wrapMode: Text.Wrap renderType: Text.NativeRendering } - TextArea - { - id: licenseText - anchors.top: licenseHeader.bottom - anchors.bottom: parent.bottom + + Row { + id: packageRow + anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: UM.Theme.getSize("default_margin").height - readOnly: true - text: licenseModel.licenseText + height: childrenRect.height + spacing: UM.Theme.getSize("default_margin").width + leftPadding: UM.Theme.getSize("narrow_margin").width + + Image + { + id: icon + width: 30 * screenScaleFactor + height: width + 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: [ - Button + Cura.PrimaryButton { - id: acceptButton - anchors.margins: UM.Theme.getSize("default_margin").width - text: catalog.i18nc("@action:button", "Accept") + leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width + rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width + + text: licenseModel.acceptButtonText onClicked: { handler.onLicenseAccepted() } - }, - Button + } + ] + + leftButtons: + [ + Cura.SecondaryButton { id: declineButton - anchors.margins: UM.Theme.getSize("default_margin").width - text: catalog.i18nc("@action:button", "Decline") + text: licenseModel.declineButtonText onClicked: { handler.onLicenseDeclined() } } ] diff --git a/plugins/Toolbox/src/CloudApiModel.py b/plugins/Toolbox/src/CloudApiModel.py index 31c3139262..556d54cf88 100644 --- a/plugins/Toolbox/src/CloudApiModel.py +++ b/plugins/Toolbox/src/CloudApiModel.py @@ -18,3 +18,11 @@ class CloudApiModel: cloud_api_root=cloud_api_root, 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 + ) diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py index 78d13f34fe..14db1a992d 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py @@ -8,11 +8,12 @@ from UM import i18nCatalog from UM.Logger import Logger from UM.Message import Message from UM.Signal import Signal -from cura.CuraApplication import CuraApplication +from cura.CuraApplication import CuraApplication, ApplicationMetadata from ..CloudApiModel import CloudApiModel from .SubscribedPackagesModel import SubscribedPackagesModel from ..UltimakerCloudScope import UltimakerCloudScope +from typing import List, Dict, Any class CloudPackageChecker(QObject): def __init__(self, application: CuraApplication) -> None: @@ -25,12 +26,12 @@ class CloudPackageChecker(QObject): self._application.initializationFinished.connect(self._onAppInitialized) 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 initialized. Therefore, we wait until the application is ready. def _onAppInitialized(self) -> None: self._package_manager = self._application.getPackageManager() - # initial check self._fetchUserSubscribedPackages() # check again whenever the login state changes @@ -38,25 +39,51 @@ class CloudPackageChecker(QObject): def _fetchUserSubscribedPackages(self) -> None: if self._application.getCuraAPI().account.isLoggedIn: - self._getUserPackages() + self._getUserSubscribedPackages() - def _handleCompatibilityData(self, json_data) -> None: - user_subscribed_packages = [plugin["package_id"] for plugin in json_data] + def _getUserSubscribedPackages(self) -> None: + 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() + + # We need to re-evaluate the dismissed packages + # (i.e. some package might got updated to the correct SDK version in the meantime, + # hence remove them from the Dismissed Incompatible list) + self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version) user_dismissed_packages = self._package_manager.getDismissedPackages() if user_dismissed_packages: user_installed_packages += 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)) - - self._model.setMetadata(json_data) - self._model.addDiscrepancies(package_discrepancy) - self._model.initialize() - - if not self._model.hasCompatiblePackages: - return None - if package_discrepancy: + self._model.addDiscrepancies(package_discrepancy) + self._model.initialize(subscribed_packages_payload) self._handlePackageDiscrepancies() def _handlePackageDiscrepancies(self) -> None: @@ -64,7 +91,6 @@ class CloudPackageChecker(QObject): sync_message = Message(self._i18n_catalog.i18nc( "@info:generic", "\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", )) sync_message.addAction("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: sync_message.hide() - 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") + self.discrepancies.emit(self._model) \ No newline at end of file diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageManager.py b/plugins/Toolbox/src/CloudSync/CloudPackageManager.py index ee57a1b90d..6720d4f163 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageManager.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageManager.py @@ -1,18 +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 -## 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) + """Manages Cloud subscriptions - def subscribe(self, package_id: str) -> None: + 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 CloudPackageManager.getInstance() instead of CloudPackageManager() + """ + + __instance = None + + @classmethod + def getInstance(cls, app: CuraApplication): + if not cls.__instance: + cls.__instance = CloudPackageManager(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) - self._request_manager.put(url=CloudApiModel.api_url_user_packages, - data=data.encode(), - scope=self._scope - ) + HttpRequestManager.getInstance().put( + url = CloudApiModel.api_url_user_packages, + data = data.encode(), + scope = self._scope + ) + + def _onPackageInstalled(self, package_id: str): + if CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: + # We might already be subscribed, but checking would take one extra request. Instead, simply subscribe + self._subscribe(package_id) diff --git a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py index f6b5622aad..ddf1a39e78 100644 --- a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py @@ -28,13 +28,12 @@ class DiscrepanciesPresenter(QObject): assert self._dialog 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: + # If there are incompatible packages - automatically dismiss them + if model.getIncompatiblePackages(): + self._package_manager.dismissAllIncompatiblePackages(model.getIncompatiblePackages()) # For now, all compatible packages presented to the user should be installed. # Later, we might remove items for which the user unselected the package - model.setItems(model.getCompatiblePackages()) - self.packageMutations.emit(model) + if model.getCompatiblePackages(): + model.setItems(model.getCompatiblePackages()) + self.packageMutations.emit(model) diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py index f19cac047a..d79d031949 100644 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py @@ -62,7 +62,8 @@ class DownloadPresenter: "received": 0, "total": 1, # make sure this is not considered done yet. Also divByZero-safe "file_written": None, - "request_data": request_data + "request_data": request_data, + "package_model": item } self._started = True @@ -128,7 +129,14 @@ class DownloadPresenter: if not item["file_written"]: 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] self._progress_message.hide() diff --git a/plugins/Toolbox/src/CloudSync/LicenseModel.py b/plugins/Toolbox/src/CloudSync/LicenseModel.py index c3b5ee5d31..335a91ef84 100644 --- a/plugins/Toolbox/src/CloudSync/LicenseModel.py +++ b/plugins/Toolbox/src/CloudSync/LicenseModel.py @@ -6,31 +6,52 @@ catalog = i18nCatalog("cura") # Model for the ToolboxLicenseDialog class LicenseModel(QObject): - dialogTitleChanged = pyqtSignal() - headerChanged = pyqtSignal() - licenseTextChanged = pyqtSignal() + DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline") + ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree") - 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__() self._current_page_idx = 0 self._page_count = 1 self._dialogTitle = "" - self._header_text = "" self._license_text = "" self._package_name = "" + self._icon_url = "" + self._decline_button_text = decline_button_text + + @pyqtProperty(str, constant = True) + def acceptButtonText(self): + return self.ACCEPT_BUTTON_TEXT + + @pyqtProperty(str, constant = True) + def declineButtonText(self): + return self._decline_button_text @pyqtProperty(str, notify=dialogTitleChanged) def dialogTitle(self) -> str: return self._dialogTitle - @pyqtProperty(str, notify=headerChanged) - def headerText(self) -> str: - return self._header_text + @pyqtProperty(str, notify=packageNameChanged) + def packageName(self) -> str: + return self._package_name 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.headerChanged.emit() + self._package_name = name + self.packageNameChanged.emit() + + @pyqtProperty(str, notify=iconChanged) + def iconUrl(self) -> str: + return self._icon_url + + def setIconUrl(self, url: str): + self._icon_url = url + self.iconChanged.emit() @pyqtProperty(str, notify=licenseTextChanged) def licenseText(self) -> str: @@ -50,6 +71,7 @@ class LicenseModel(QObject): self._updateDialogTitle() def _updateDialogTitle(self): - self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement ({}/{})" - .format(self._current_page_idx + 1, self._page_count)) + self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement") + if self._page_count > 1: + self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count) self.dialogTitleChanged.emit() diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py index cefe6f4037..5f01166898 100644 --- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py +++ b/plugins/Toolbox/src/CloudSync/LicensePresenter.py @@ -1,5 +1,6 @@ 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 @@ -17,6 +18,7 @@ class LicensePresenter(QObject): def __init__(self, app: CuraApplication) -> None: super().__init__() + self._catalog = i18nCatalog("cura") self._dialog = None # type: Optional[QObject] self._package_manager = app.getPackageManager() # type: PackageManager # Emits List[Dict[str, [Any]] containing for example @@ -25,7 +27,9 @@ class LicensePresenter(QObject): self._current_package_idx = 0 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 @@ -34,7 +38,7 @@ class LicensePresenter(QObject): ## Show a license dialog for multiple packages where users can read a license and accept or decline them # \param plugin_path: Root directory of the Toolbox plugin # \param packages: Dict[package id, file path] - 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) self._initState(packages) @@ -42,12 +46,11 @@ class LicensePresenter(QObject): if self._dialog is None: context_properties = { - "catalog": i18nCatalog("cura"), + "catalog": self._catalog, "licenseModel": self._license_model, "handler": self } self._dialog = self._app.createQmlComponent(path, context_properties) - self._license_model.setPageCount(len(self._package_models)) self._presentCurrentPackage() @pyqtSlot() @@ -60,32 +63,41 @@ class LicensePresenter(QObject): self._package_models[self._current_package_idx]["accepted"] = False self._checkNextPage() - def _initState(self, packages: Dict[str, str]) -> None: - self._package_models = [ - { - "package_id" : package_id, - "package_path" : package_path, - "accepted" : None #: None: no answer yet - } - for package_id, package_path in packages.items() - ] + def _initState(self, packages: Dict[str, Dict[str, str]]) -> None: + + implicitly_accepted_count = 0 + + for package_id, item in packages.items(): + item["package_id"] = package_id + item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"]) + if item["licence_content"] is None: + # Implicitly accept when there is no license + item["accepted"] = True + implicitly_accepted_count = implicitly_accepted_count + 1 + self._package_models.append(item) + else: + item["accepted"] = None #: None: no answer yet + # When presenting the packages, we want to show packages which have a license first. + # In fact, we don't want to show the others at all because they are implicitly accepted + self._package_models.insert(0, item) + CuraApplication.getInstance().processEvents() + self._page_count = len(self._package_models) - implicitly_accepted_count + self._license_model.setPageCount(self._page_count) + def _presentCurrentPackage(self) -> None: package_model = self._package_models[self._current_package_idx] - license_content = self._package_manager.getPackageLicense(package_model["package_path"]) - if license_content is None: - # Implicitly accept when there is no license - self.onLicenseAccepted() - return + package_info = self._package_manager.getPackageInfo(package_model["package_path"]) self._license_model.setCurrentPageIdx(self._current_package_idx) - self._license_model.setPackageName(package_model["package_id"]) - self._license_model.setLicenseText(license_content) + self._license_model.setPackageName(package_info["display_name"]) + self._license_model.setIconUrl(package_model["icon_url"]) + self._license_model.setLicenseText(package_model["licence_content"]) if self._dialog: self._dialog.open() # Does nothing if already open def _checkNextPage(self) -> None: - if self._current_package_idx + 1 < len(self._package_models): + if self._current_package_idx + 1 < self._page_count: self._current_package_idx += 1 self._presentCurrentPackage() else: diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py index 4a0f559748..614d397d91 100644 --- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py +++ b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py @@ -37,27 +37,18 @@ class SubscribedPackagesModel(ListModel): return True 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: self._discrepancies = discrepancy - def getCompatiblePackages(self): - return [x for x in self._items if x["is_compatible"]] + def getCompatiblePackages(self) -> List[Dict[str, Any]]: + 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() - for item in self._metadata: + for item in subscribed_packages_payload: if item["package_id"] not in self._discrepancies: continue package = { diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py index 674fb68729..c24703bf92 100644 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py @@ -1,8 +1,10 @@ import os from typing import List, Dict, Any, cast +from UM import i18n_catalog from UM.Extension import Extension from UM.Logger import Logger +from UM.Message import Message from UM.PluginRegistry import PluginRegistry from cura.CuraApplication import CuraApplication from .CloudPackageChecker import CloudPackageChecker @@ -36,7 +38,8 @@ class SyncOrchestrator(Extension): self._name = "SyncOrchestrator" self._package_manager = app.getPackageManager() - self._cloud_package_manager = CloudPackageManager(app) + # Keep a reference to the CloudPackageManager. it watches for installed packages and subscribes to them + self._cloud_package_manager = CloudPackageManager.getInstance(app) # type: CloudPackageManager self._checker = CloudPackageChecker(app) # type: CloudPackageChecker self._checker.discrepancies.connect(self._onDiscrepancies) @@ -61,32 +64,37 @@ class SyncOrchestrator(Extension): self._download_presenter.download(mutations) ## 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] - def _onDownloadFinished(self, success_items: Dict[str, str], error_items: List[str]) -> None: - # todo handle error items + def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None: + if error_items: + message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items))) + self._showErrorMessage(message) + plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId())) self._license_presenter.present(plugin_path, success_items) # Called when user has accepted / declined all licenses for the downloaded packages def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None: - Logger.debug("Got license answers: {}", answers) - has_changes = False # True when at least one package is installed for item in answers: if item["accepted"]: # install and subscribe packages if not self._package_manager.installPackage(item["package_path"]): - Logger.error("could not install {}".format(item["package_id"])) + message = "Could not install {}".format(item["package_id"]) + self._showErrorMessage(message) continue - self._cloud_package_manager.subscribe(item["package_id"]) has_changes = True else: - # todo unsubscribe declined packages - pass + self._cloud_package_manager.unsubscribe(item["package_id"]) # delete temp file os.remove(item["package_path"]) if has_changes: 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() diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index e0d04bed5b..782d6668ba 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -21,7 +21,6 @@ from cura.Machines.ContainerTree import ContainerTree from .CloudApiModel import CloudApiModel from .AuthorsModel import AuthorsModel -from .CloudSync.CloudPackageManager import CloudPackageManager from .CloudSync.LicenseModel import LicenseModel from .PackagesModel import PackagesModel from .UltimakerCloudScope import UltimakerCloudScope @@ -44,7 +43,6 @@ class Toolbox(QObject, Extension): self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] # Network: - self._cloud_package_manager = CloudPackageManager(application) # type: CloudPackageManager self._download_request_data = None # type: Optional[HttpRequestData] self._download_progress = 0 # type: float self._is_downloading = False # type: bool @@ -147,17 +145,14 @@ class Toolbox(QObject, Extension): 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: 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 self._license_model.setCurrentPageIdx(0) self._license_model.setPageCount(1) + self._license_model.setIconUrl(icon_url) self._license_model.setPackageName(plugin_name) self._license_model.setLicenseText(license_content) @@ -376,7 +371,6 @@ class Toolbox(QObject, Extension): def onLicenseAccepted(self): self.closeLicenseDialog.emit() package_id = self.install(self.getLicenseDialogPluginFileLocation()) - self.subscribe(package_id) @pyqtSlot() @@ -670,14 +664,16 @@ class Toolbox(QObject, Extension): return license_content = self._package_manager.getPackageLicense(file_path) + package_id = package_info["package_id"] 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 - package_id = self.install(file_path) - if package_id != package_info["package_id"]: - Logger.error("Installed package {} does not match {}".format(package_id, package_info["package_id"])) - self.subscribe(package_id) + installed_id = self.install(file_path) + if installed_id != package_id: + Logger.error("Installed package {} does not match {}".format(installed_id, package_id)) # Getter & Setters for Properties: # -------------------------------------------------------------------------- @@ -699,14 +695,14 @@ class Toolbox(QObject, Extension): def isDownloading(self) -> bool: return self._is_downloading - def setActivePackage(self, package: Dict[str, Any]) -> None: + def setActivePackage(self, package: QObject) -> None: if self._active_package != package: self._active_package = package self.activePackageChanged.emit() ## The active package is the package that is currently being downloaded @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) - def activePackage(self) -> Optional[Dict[str, Any]]: + def activePackage(self) -> Optional[QObject]: return self._active_package def setViewCategory(self, category: str = "plugin") -> None: diff --git a/plugins/Toolbox/src/UltimakerCloudScope.py b/plugins/Toolbox/src/UltimakerCloudScope.py index f7707957e6..14583d7d59 100644 --- a/plugins/Toolbox/src/UltimakerCloudScope.py +++ b/plugins/Toolbox/src/UltimakerCloudScope.py @@ -6,6 +6,9 @@ from cura.API import Account 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): def __init__(self, application: CuraApplication): super().__init__(application) diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json index 7cb229150c..bb11891174 100644 --- a/resources/bundled_packages/cura.json +++ b/resources/bundled_packages/cura.json @@ -848,6 +848,23 @@ "website": "https://ultimaker.com" } } + }, + "VersionUpgrade44to45": { + "package_info": { + "package_id": "VersionUpgrade44to45", + "package_type": "plugin", + "display_name": "Version Upgrade 4.4 to 4.5", + "description": "Upgrades configurations from Cura 4.4 to Cura 4.5.", + "package_version": "1.0.0", + "sdk_version": "7.0.0", + "website": "https://ultimaker.com", + "author": { + "author_id": "UltimakerPackages", + "display_name": "Ultimaker B.V.", + "email": "plugins@ultimaker.com", + "website": "https://ultimaker.com" + } + } }, "X3DReader": { "package_info": { diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index ca70f0d7de..3a222215d8 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2122,6 +2122,7 @@ "default_value": 210, "minimum_value_warning": "0", "maximum_value_warning": "285", + "maximum_value": "365", "enabled": false, "settable_per_extruder": true, "settable_per_mesh": false, @@ -2153,6 +2154,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "285", + "maximum_value": "365", "enabled": "machine_nozzle_temp_enabled and not (material_flow_dependent_temperature)", "settable_per_mesh": false, "settable_per_extruder": true @@ -2168,6 +2170,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "285", + "maximum_value": "365", "enabled": "machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true @@ -2183,6 +2186,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "material_standby_temperature", "maximum_value_warning": "material_print_temperature", + "maximum_value": "365", "enabled": "machine_nozzle_temp_enabled and not machine_extruders_share_heater", "settable_per_mesh": false, "settable_per_extruder": true @@ -2198,6 +2202,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "material_standby_temperature", "maximum_value_warning": "material_print_temperature", + "maximum_value": "365", "enabled": "machine_nozzle_temp_enabled and not machine_extruders_share_heater", "settable_per_mesh": false, "settable_per_extruder": true @@ -2227,6 +2232,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "build_volume_temperature", "maximum_value_warning": "130", + "maximum_value": "200", "enabled": false, "settable_per_mesh": false, "settable_per_extruder": false, @@ -2244,6 +2250,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "build_volume_temperature", "maximum_value_warning": "130", + "maximum_value": "200", "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "settable_per_mesh": false, "settable_per_extruder": false, @@ -2261,6 +2268,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "max(build_volume_temperature, max(extruderValues('material_bed_temperature')))", "maximum_value_warning": "130", + "maximum_value": "200", "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "settable_per_mesh": false, "settable_per_extruder": false, @@ -2377,6 +2385,7 @@ "enabled": false, "minimum_value": "-273.15", "maximum_value_warning": "300", + "maximum_value": "365", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -2415,6 +2424,7 @@ "default_value": 50, "enabled": false, "minimum_value": "-273.15", + "maximum_value": "365", "maximum_value_warning": "300", "settable_per_mesh": false, "settable_per_extruder": true @@ -2691,6 +2701,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "260", + "maximum_value": "365", "enabled": "extruders_enabled_count > 1 and machine_nozzle_temp_enabled", "settable_per_mesh": false, "settable_per_extruder": true @@ -5688,7 +5699,7 @@ "unit": "mm", "enabled": "resolveOrValue('prime_tower_enable')", "default_value": 200, - "value": "machine_width - max(extruderValue(adhesion_extruder_nr, 'brim_width') * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 if adhesion_type == 'brim' or (prime_tower_brim_enable and adhesion_type != 'raft') else (extruderValue(adhesion_extruder_nr, 'raft_margin') if adhesion_type == 'raft' else (extruderValue(adhesion_extruder_nr, 'skirt_gap') if adhesion_type == 'skirt' else 0)), max(extruderValues('travel_avoid_distance'))) - max(extruderValues('support_offset')) - sum(extruderValues('skirt_brim_line_width')) * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 - (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0) - 1", + "value": "machine_width - max(extruderValue(adhesion_extruder_nr, 'brim_width') * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 if adhesion_type == 'brim' or (prime_tower_brim_enable and adhesion_type != 'raft') else (extruderValue(adhesion_extruder_nr, 'raft_margin') if adhesion_type == 'raft' else (extruderValue(adhesion_extruder_nr, 'skirt_gap') if adhesion_type == 'skirt' else 0)), max(extruderValues('travel_avoid_distance'))) - max(extruderValues('support_offset')) - sum(extruderValues('skirt_brim_line_width')) * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 - (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0) - max(map(abs, extruderValues('machine_nozzle_offset_x'))) - 1", "maximum_value": "machine_width / 2 if machine_center_is_zero else machine_width", "minimum_value": "resolveOrValue('prime_tower_size') - machine_width / 2 if machine_center_is_zero else resolveOrValue('prime_tower_size')", "settable_per_mesh": false, @@ -5702,7 +5713,7 @@ "unit": "mm", "enabled": "resolveOrValue('prime_tower_enable')", "default_value": 200, - "value": "machine_depth - prime_tower_size - max(extruderValue(adhesion_extruder_nr, 'brim_width') * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 if adhesion_type == 'brim' or (prime_tower_brim_enable and adhesion_type != 'raft') else (extruderValue(adhesion_extruder_nr, 'raft_margin') if adhesion_type == 'raft' else (extruderValue(adhesion_extruder_nr, 'skirt_gap') if adhesion_type == 'skirt' else 0)), max(extruderValues('travel_avoid_distance'))) - max(extruderValues('support_offset')) - sum(extruderValues('skirt_brim_line_width')) * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 - (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0) - 1", + "value": "machine_depth - prime_tower_size - max(extruderValue(adhesion_extruder_nr, 'brim_width') * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 if adhesion_type == 'brim' or (prime_tower_brim_enable and adhesion_type != 'raft') else (extruderValue(adhesion_extruder_nr, 'raft_margin') if adhesion_type == 'raft' else (extruderValue(adhesion_extruder_nr, 'skirt_gap') if adhesion_type == 'skirt' else 0)), max(extruderValues('travel_avoid_distance'))) - max(extruderValues('support_offset')) - sum(extruderValues('skirt_brim_line_width')) * extruderValue(adhesion_extruder_nr, 'initial_layer_line_width_factor') / 100 - (resolveOrValue('draft_shield_dist') if resolveOrValue('draft_shield_enabled') else 0) - max(map(abs, extruderValues('machine_nozzle_offset_y'))) - 1", "maximum_value": "machine_depth / 2 - resolveOrValue('prime_tower_size') if machine_center_is_zero else machine_depth - resolveOrValue('prime_tower_size')", "minimum_value": "machine_depth / -2 if machine_center_is_zero else 0", "settable_per_mesh": false, @@ -5984,7 +5995,7 @@ "print_sequence": { "label": "Print Sequence", - "description": "Whether to print all models one layer at a time or to wait for one model to finish, before moving on to the next. One at a time mode is only possible if all models are separated in such a way that the whole print head can move in between and all models are lower than the distance between the nozzle and the X/Y axes.", + "description": "Whether to print all models one layer at a time or to wait for one model to finish, before moving on to the next. One at a time mode is possible if a) only one extruder is enabled and b) all models are separated in such a way that the whole print head can move in between and all models are lower than the distance between the nozzle and the X/Y axes. ", "type": "enum", "options": { @@ -6666,7 +6677,17 @@ "limit_to_extruder": "wall_0_extruder_nr", "settable_per_mesh": true }, - "magic_fuzzy_skin_thickness": + "magic_fuzzy_skin_outside_only": + { + "label": "Fuzzy Skin Outside Only", + "description": "Jitter only the parts' outlines and not the parts' holes.", + "type": "bool", + "default_value": false, + "enabled": "magic_fuzzy_skin_enabled", + "limit_to_extruder": "wall_0_extruder_nr", + "settable_per_mesh": true + }, + "magic_fuzzy_skin_thickness": { "label": "Fuzzy Skin Thickness", "description": "The width within which to jitter. It's advised to keep this below the outer wall width, since the inner walls are unaltered.", diff --git a/resources/definitions/geeetech_A10.def.json b/resources/definitions/geeetech_A10.def.json new file mode 100644 index 0000000000..62a3f3684e --- /dev/null +++ b/resources/definitions/geeetech_A10.def.json @@ -0,0 +1,57 @@ +{ + "version": 2, + "name": "Geeetech A10", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Amit L", + "manufacturer": "Geeetech", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": + { + "0": "geeetech_A10_1" + } + + }, + + "overrides": { + "machine_name": { "default_value": "Geeetech A10" }, + "machine_width": { + "default_value": 220 + }, + "machine_height": { + "default_value": 220 + }, + "machine_depth": { + "default_value": 260 + }, "machine_center_is_zero": { + "default_value": false + }, + "layer_height": { "default_value": 0.1 }, + "layer_height_0": { "default_value": 0.15 }, + "retraction_amount": { "default_value": 0.8 }, + "retraction_speed": { "default_value": 35 }, + "adhesion_type": { "default_value": "skirt" }, + "machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] }, + "gantry_height": { "value": "28" }, + "machine_max_feedrate_z": { "default_value": 12 }, + "machine_max_feedrate_e": { "default_value": 120 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_acceleration": { "default_value": 1000 }, + "machine_max_jerk_xy": { "default_value": 10 }, + "machine_max_jerk_z": { "default_value": 0.2 }, + "machine_max_jerk_e": { "default_value": 2.5 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { + "default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X180 E40\nG1 F1200 Z2\nG92 E0\nG28" + }, + "machine_end_gcode": { + "default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84" + }, + "machine_extruder_count": { + "default_value": 1 + } + + } +} diff --git a/resources/definitions/geeetech_A10M.def.json b/resources/definitions/geeetech_A10M.def.json new file mode 100644 index 0000000000..d74a04f1ec --- /dev/null +++ b/resources/definitions/geeetech_A10M.def.json @@ -0,0 +1,58 @@ +{ + "version": 2, + "name": "Geeetech A10M", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Amit L", + "manufacturer": "Geeetech", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": + { + "0": "geeetech_A10M_1", + "1": "geeetech_A10M_2" + } + + }, + + "overrides": { + "machine_name": { "default_value": "Geeetech A10M" }, + "machine_width": { + "default_value": 220 + }, + "machine_height": { + "default_value": 220 + }, + "machine_depth": { + "default_value": 260 + }, "machine_center_is_zero": { + "default_value": false + }, + "layer_height": { "default_value": 0.1 }, + "layer_height_0": { "default_value": 0.15 }, + "retraction_amount": { "default_value": 0.8 }, + "retraction_speed": { "default_value": 35 }, + "adhesion_type": { "default_value": "skirt" }, + "machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] }, + "gantry_height": { "value": "28" }, + "machine_max_feedrate_z": { "default_value": 12 }, + "machine_max_feedrate_e": { "default_value": 120 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_acceleration": { "default_value": 1000 }, + "machine_max_jerk_xy": { "default_value": 10 }, + "machine_max_jerk_z": { "default_value": 0.2 }, + "machine_max_jerk_e": { "default_value": 2.5 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { + "default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nM163 S0 P0.50\nM163 S1 P0.50\nM164 S4\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X180 E40\nG1 F1200 Z2\nG92 E0\nG28" + }, + "machine_end_gcode": { + "default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84" + }, + "machine_extruder_count": { + "default_value": 2 + } + + } +} diff --git a/resources/definitions/geeetech_A10T.def.json b/resources/definitions/geeetech_A10T.def.json new file mode 100644 index 0000000000..f989a90982 --- /dev/null +++ b/resources/definitions/geeetech_A10T.def.json @@ -0,0 +1,59 @@ +{ + "version": 2, + "name": "Geeetech A10T", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Amit L", + "manufacturer": "Geeetech", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": + { + "0": "geeetech_A10T_1", + "1": "geeetech_A10T_2", + "2": "geeetech_A10T_3" + } + + }, + + "overrides": { + "machine_name": { "default_value": "Geeetech A10T" }, + "machine_width": { + "default_value": 220 + }, + "machine_height": { + "default_value": 220 + }, + "machine_depth": { + "default_value": 260 + }, "machine_center_is_zero": { + "default_value": false + }, + "layer_height": { "default_value": 0.1 }, + "layer_height_0": { "default_value": 0.15 }, + "retraction_amount": { "default_value": 0.8 }, + "retraction_speed": { "default_value": 35 }, + "adhesion_type": { "default_value": "skirt" }, + "machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] }, + "gantry_height": { "value": "28" }, + "machine_max_feedrate_z": { "default_value": 12 }, + "machine_max_feedrate_e": { "default_value": 120 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_acceleration": { "default_value": 1000 }, + "machine_max_jerk_xy": { "default_value": 10 }, + "machine_max_jerk_z": { "default_value": 0.2 }, + "machine_max_jerk_e": { "default_value": 2.5 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { + "default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nM163 S0 P0.33\nM163 S1 P0.33\nM163 S2 P0.33\nM164 S4\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X180 E40\nG1 F1200 Z2\nG92 E0\nG28" + }, + "machine_end_gcode": { + "default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84" + }, + "machine_extruder_count": { + "default_value": 3 + } + + } +} diff --git a/resources/definitions/geeetech_A20.def.json b/resources/definitions/geeetech_A20.def.json new file mode 100644 index 0000000000..d96452176f --- /dev/null +++ b/resources/definitions/geeetech_A20.def.json @@ -0,0 +1,57 @@ +{ + "version": 2, + "name": "Geeetech A20", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Amit L", + "manufacturer": "Geeetech", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": + { + "0": "geeetech_A20_1" + } + + }, + + "overrides": { + "machine_name": { "default_value": "Geeetech A20" }, + "machine_width": { + "default_value": 250 + }, + "machine_height": { + "default_value": 250 + }, + "machine_depth": { + "default_value": 250 + }, "machine_center_is_zero": { + "default_value": false + }, + "layer_height": { "default_value": 0.1 }, + "layer_height_0": { "default_value": 0.15 }, + "retraction_amount": { "default_value": 0.8 }, + "retraction_speed": { "default_value": 35 }, + "adhesion_type": { "default_value": "skirt" }, + "machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] }, + "gantry_height": { "value": "28" }, + "machine_max_feedrate_z": { "default_value": 12 }, + "machine_max_feedrate_e": { "default_value": 120 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_acceleration": { "default_value": 1000 }, + "machine_max_jerk_xy": { "default_value": 10 }, + "machine_max_jerk_z": { "default_value": 0.2 }, + "machine_max_jerk_e": { "default_value": 2.5 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { + "default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X200 E40\nG1 F1200 Z2\nG92 E0\nG28" + }, + "machine_end_gcode": { + "default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84" + }, + "machine_extruder_count": { + "default_value": 1 + } + + } +} diff --git a/resources/definitions/geeetech_A20M.def.json b/resources/definitions/geeetech_A20M.def.json new file mode 100644 index 0000000000..5c38728ed1 --- /dev/null +++ b/resources/definitions/geeetech_A20M.def.json @@ -0,0 +1,58 @@ +{ + "version": 2, + "name": "Geeetech A20M", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Amit L", + "manufacturer": "Geeetech", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": + { + "0": "geeetech_A20M_1", + "1": "geeetech_A20M_2" + } + + }, + + "overrides": { + "machine_name": { "default_value": "Geeetech A20M" }, + "machine_width": { + "default_value": 250 + }, + "machine_height": { + "default_value": 250 + }, + "machine_depth": { + "default_value": 250 + }, "machine_center_is_zero": { + "default_value": false + }, + "layer_height": { "default_value": 0.1 }, + "layer_height_0": { "default_value": 0.15 }, + "retraction_amount": { "default_value": 0.8 }, + "retraction_speed": { "default_value": 35 }, + "adhesion_type": { "default_value": "skirt" }, + "machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] }, + "gantry_height": { "value": "28" }, + "machine_max_feedrate_z": { "default_value": 12 }, + "machine_max_feedrate_e": { "default_value": 120 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_acceleration": { "default_value": 1000 }, + "machine_max_jerk_xy": { "default_value": 10 }, + "machine_max_jerk_z": { "default_value": 0.2 }, + "machine_max_jerk_e": { "default_value": 2.5 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { + "default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nM163 S0 P0.50\nM163 S1 P0.50\nM164 S4\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X200 E40\nG1 F1200 Z2\nG92 E0\nG28" + }, + "machine_end_gcode": { + "default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84" + }, + "machine_extruder_count": { + "default_value": 2 + } + + } +} diff --git a/resources/definitions/geeetech_A20T.def.json b/resources/definitions/geeetech_A20T.def.json new file mode 100644 index 0000000000..72ed97978c --- /dev/null +++ b/resources/definitions/geeetech_A20T.def.json @@ -0,0 +1,59 @@ +{ + "version": 2, + "name": "Geeetech A20T", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Amit L", + "manufacturer": "Geeetech", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": + { + "0": "geeetech_A20T_1", + "1": "geeetech_A20T_2", + "2": "geeetech_A20T_3" + } + + }, + + "overrides": { + "machine_name": { "default_value": "Geeetech A20T" }, + "machine_width": { + "default_value": 250 + }, + "machine_height": { + "default_value": 250 + }, + "machine_depth": { + "default_value": 250 + }, "machine_center_is_zero": { + "default_value": false + }, + "layer_height": { "default_value": 0.1 }, + "layer_height_0": { "default_value": 0.15 }, + "retraction_amount": { "default_value": 0.8 }, + "retraction_speed": { "default_value": 35 }, + "adhesion_type": { "default_value": "skirt" }, + "machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] }, + "gantry_height": { "value": "28" }, + "machine_max_feedrate_z": { "default_value": 12 }, + "machine_max_feedrate_e": { "default_value": 120 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_acceleration": { "default_value": 1000 }, + "machine_max_jerk_xy": { "default_value": 10 }, + "machine_max_jerk_z": { "default_value": 0.2 }, + "machine_max_jerk_e": { "default_value": 2.5 }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { + "default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nM163 S0 P0.33\nM163 S1 P0.33\nM163 S2 P0.33\nM164 S4\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X200 E40\nG1 F1200 Z2\nG92 E0\nG28" + }, + "machine_end_gcode": { + "default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84" + }, + "machine_extruder_count": { + "default_value": 3 + } + + } +} diff --git a/resources/definitions/makeit_pro_mx.def.json b/resources/definitions/makeit_pro_mx.def.json new file mode 100644 index 0000000000..13770e8571 --- /dev/null +++ b/resources/definitions/makeit_pro_mx.def.json @@ -0,0 +1,96 @@ +{ + "version": 2, + "name": "MAKEiT Pro-MX", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "unknown", + "manufacturer": "MAKEiT 3D", + "file_formats": "text/x-gcode", + "has_materials": false, + "machine_extruder_trains": + { + "0": "makeit_mx_dual_1st", + "1": "makeit_mx_dual_2nd" + } + }, + + "overrides": { + "machine_name": { "default_value": "MAKEiT Pro-MX" }, + "machine_width": { + "default_value": 200 + }, + "machine_height": { + "default_value": 330 + }, + "machine_depth": { + "default_value": 240 + }, + "machine_center_is_zero": { + "default_value": false + }, + "machine_head_with_fans_polygon": + { + "default_value": [ + [ -200, 240 ], + [ -200, -32 ], + [ 200, 240 ], + [ 200, -32 ] + ] + }, + "gantry_height": { + "value": "200" + }, + "machine_use_extruder_offset_to_offset_coords": { + "default_value": true + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "machine_start_gcode": { + "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nG92 E0 ;zero the extruded length\nG28 ;home\nG1 F200 E30 ;extrude 30 mm of feed stock\nG92 E0 ;zero the extruded length\nG1 E-5 ;retract 5 mm\nG28 SC ;Do homeing, clean nozzles and let printer to know that printing started\nG92 X-6 ;Sets Curas checker board to match printers heated bed coordinates\nG1 F{speed_travel}\nM117 Printing..." + }, + "machine_end_gcode": { + "default_value": "M104 T0 S0 ;1st extruder heater off\nM104 T1 S0 ;2nd extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-5 F9000 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+5 X+20 Y+20 F9000 ;move Z up a bit\nM117 MAKEiT Pro@Done\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM81" + }, + "machine_extruder_count": { + "default_value": 2 + }, + "print_sequence": { + "enabled": false + }, + "prime_tower_position_x": { + "value": "185" + }, + "prime_tower_position_y": { + "value": "160" + }, + "layer_height": { + "default_value": 0.2 + }, + "retraction_speed": { + "default_value": 180 + }, + "infill_sparse_density": { + "default_value": 20 + }, + "retraction_amount": { + "default_value": 6 + }, + "speed_print": { + "default_value": 60 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "cool_min_layer_time_fan_speed_max": { + "default_value": 5 + }, + "adhesion_type": { + "default_value": "skirt" + }, + "machine_heated_bed": { + "default_value": true + } + } +} \ No newline at end of file diff --git a/resources/definitions/mp_mini_delta.def.json b/resources/definitions/mp_mini_delta.def.json new file mode 100644 index 0000000000..57e2d7c04c --- /dev/null +++ b/resources/definitions/mp_mini_delta.def.json @@ -0,0 +1,76 @@ +{ + "version": 2, + "name": "MP Mini Delta", + "inherits": "fdmprinter", + "metadata": { + "author": "MPMD Facebook Group", + "manufacturer": "Monoprice", + "category": "Other", + "file_formats": "text/x-gcode", + "platform": "mp_mini_delta_platform.stl", + "supports_usb_connection": true, + "has_machine_quality": false, + "visible": true, + "platform_offset": [0, 0, 0], + "has_materials": true, + "has_variants": false, + "has_machine_materials": false, + "has_variant_materials": false, + "preferred_quality_type": "normal", + "machine_extruder_trains": + { + "0": "mp_mini_delta_extruder_0" + } + }, + + "overrides": { + "machine_start_gcode": + { + "default_value": ";MPMD Basic Calibration Tutorial: \n; https://www.thingiverse.com/thing:3892011 \n; \n; If you want to put calibration values in your \n; Start Gcode, put them here. \n; \n;If on stock firmware, at minimum, consider adding \n;M665 R here since there is a firmware bug. \n; \n; Calibration part ends here \n; \nG90 ; switch to absolute positioning \nG92 E0 ; reset extrusion distance \nG1 E20 F200 ; purge 20mm of filament to prime nozzle. \nG92 E0 ; reset extrusion distance \nG4 S5 ; Pause for 5 seconds to allow time for removing extruded filament \nG28 ; start from home position \nG1 E-6 F900 ; retract 6mm of filament before starting the bed leveling process \nG92 E0 ; reset extrusion distance \nG4 S5 ; pause for 5 seconds to allow time for removing extruded filament \nG29 P2 Z0.28 ; Auto-level ; ADJUST Z higher or lower to set first layer height. Start with 0.02 adjustments. \nG1 Z30 ; raise Z 30mm to prepare for priming the nozzle \nG1 E5 F200 ; extrude 5mm of filament to help prime the nozzle just prior to the start of the print \nG92 E0 ; reset extrusion distance \nG4 S5 ; pause for 5 seconds to allow time for cleaning the nozzle and build plate if needed " + }, + "machine_end_gcode": + { + "default_value": "M107; \nM104 S0; turn off hotend heater \nM140 S0; turn off bed heater \nG91; Switch to use Relative Coordinates \nG1 E-2 F300; retract the filament a bit before lifting the nozzle to release some of the pressure \nG1 Z5 E-5 F4800; move nozzle up a bit and retract filament even more \nG28 X0; return to home positions so the nozzle is out of the way \nM84; turn off stepper motors \nG90; switch to absolute positioning \nM82; absolute extrusion mode" + }, + "machine_width": { "default_value": 110 }, + "machine_depth": { "default_value": 110 }, + "machine_height": { "default_value": 120 }, + "machine_heated_bed": { "default_value": true }, + "machine_shape": { "default_value": "elliptic" }, + "machine_center_is_zero": { "default_value": true }, + "machine_nozzle_size": { + "default_value": 0.4, + "minimum_value": 0.10, + "maximum_value": 0.80 + }, + "layer_height": { + "default_value": 0.14, + "minimum_value": 0.04 + }, + "layer_height_0": { + "default_value": 0.21, + "minimum_value": 0.07 + }, + "line_width": { "value": "round(machine_nozzle_size * 0.875, 2)" }, + "material_print_temperature_layer_0": { "value": "material_print_temperature + 5" }, + "material_bed_temperature_layer_0": { "value": "material_bed_temperature + 5" }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_max_feedrate_x": { "default_value": 150 }, + "machine_max_feedrate_y": { "default_value": 150 }, + "machine_max_feedrate_z": { "default_value": 150 }, + "machine_max_feedrate_e": { "default_value": 50 }, + "machine_max_acceleration_x": { "default_value": 800 }, + "machine_max_acceleration_y": { "default_value": 800 }, + "machine_max_acceleration_z": { "default_value": 800 }, + "machine_max_acceleration_e": { "default_value": 10000 }, + "machine_acceleration": { "default_value": 3000 }, + "machine_max_jerk_xy": { "default_value": 20 }, + "machine_max_jerk_z": { "default_value": 20 }, + "machine_max_jerk_e": { "default_value": 5}, + "retraction_amount": { "default_value": 4 }, + "retraction_speed": { "default_value": 50 }, + "retraction_hop_enabled": { "default_value": false }, + "retract_at_layer_change": { "default_value": true }, + "coasting_enable": { "default_value": true } + } +} diff --git a/resources/definitions/renkforce_rf100.def.json b/resources/definitions/renkforce_rf100.def.json index 2ff34a7519..520f7ef6f8 100644 --- a/resources/definitions/renkforce_rf100.def.json +++ b/resources/definitions/renkforce_rf100.def.json @@ -18,10 +18,10 @@ "default_value": "skirt" }, "bottom_thickness": { - "value": "0.5" + "value": "0.6" }, "brim_width": { - "value": "2.0" + "value": "3.0" }, "cool_fan_enabled": { "value": "True" @@ -39,19 +39,28 @@ "value": "True" }, "cool_min_layer_time": { - "value": "5.0" + "value": "1.0" }, "cool_min_speed": { - "value": "10.0" + "value": "5.0" }, "infill_before_walls": { "value": "True" }, + "infill_line_width": { + "value": "0.6" + }, "infill_overlap": { "value": "15.0" }, + "infill_sparse_density": { + "value": "26.0" + }, + "ironing_enabled": { + "value": "True" + }, "layer_0_z_overlap": { - "value": "0.22" + "value": "0.11" }, "layer_height_0": { "value": "0.3" @@ -60,11 +69,23 @@ "value": "100" }, "machine_end_gcode": { - "default_value": ";End GCode\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 E-5 X-20 Y-20 ;retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nG0 Z{machine_height} ;move the platform all the way down\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done" + "default_value": ";End GCode\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-4 F300 ;move Z up a bit and retract filament even more\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG0 Z{machine_height} F1800 ;move the platform all the way down\nG28 X0 Y0 F1800 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done" }, "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_head_with_fans_polygon": + { + "default_value": [ + [-26, -27], + [38, -27], + [38, 55], + [-26, 55] + ] + }, + "gantry_height": { + "value": "8" + }, "machine_height": { "value": "100" }, @@ -72,7 +93,7 @@ "default_value": "Renkforce RF100" }, "machine_start_gcode": { - "default_value": ";Start GCode\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\n;Put printing message on LCD screen\nM117 Printing..." + "default_value": ";Sliced at: {day} {date} {time}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG1 Z5.0 F1800 ;move Z to 5mm\nG28 X0 Y0 F1800 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstop\nG92 E0 ;zero the extruded length\nG1 F200 E6.0 ;extrude 6.0mm of feed stock to build pressure\nG1 Z5.0 F300 ;move the platform down 5mm\nG92 E0 ;zero the extruded length again\nG1 F1800\n;Put printing message on LCD screen\nM117 Printing..." }, "machine_width": { "value": "100" @@ -90,7 +111,7 @@ "value": "True" }, "raft_airgap": { - "value": "0.22" + "value": "0.33" }, "raft_base_line_spacing": { "value": "3.0" @@ -111,22 +132,25 @@ "value": "0.27" }, "raft_margin": { - "value": "5.0" + "value": "6.0" + }, + "raft_speed": { + "value": "20.0" }, "raft_surface_layers": { - "value": "2.0" + "value": "2" }, "raft_surface_line_spacing": { - "value": "3.0" + "value": "0.4" }, "raft_surface_line_width": { "value": "0.4" }, "raft_surface_thickness": { - "value": "0.27" + "value": "0.1" }, "retraction_amount": { - "value": "2.0" + "value": "5.0" }, "retraction_combing": { "default_value": "all" @@ -134,15 +158,9 @@ "retraction_enable": { "value": "True" }, - "retraction_hop_enabled": { - "value": "1.0" - }, "retraction_min_travel": { "value": "1.5" }, - "retraction_speed": { - "value": "40.0" - }, "skin_overlap": { "value": "15.0" }, @@ -185,6 +203,9 @@ "support_infill_rate": { "value": "15 if support_enable else 0 if support_tree_enable else 15" }, + "support_line_width": { + "value": "0.6" + }, "support_pattern": { "default_value": "lines" }, @@ -192,13 +213,13 @@ "default_value": "everywhere" }, "support_xy_distance": { - "value": "0.5" + "value": "0.7" }, "support_z_distance": { - "value": "0.1" + "value": "0.35" }, - "top_thickness": { - "value": "0.5" + "top_bottom_thickness": { + "value": "0.8" }, "wall_thickness": { "value": "0.8" diff --git a/resources/definitions/renkforce_rf100_v2.def.json b/resources/definitions/renkforce_rf100_v2.def.json new file mode 100644 index 0000000000..2715e5d227 --- /dev/null +++ b/resources/definitions/renkforce_rf100_v2.def.json @@ -0,0 +1,228 @@ +{ + "version": 2, + "name": "Renkforce RF100 V2", + "inherits": "fdmprinter", + "metadata": { + "author": "Simon Peter (based on RF100.ini by Conrad Electronic SE)", + "file_formats": "text/x-gcode", + "manufacturer": "Renkforce", + "visible": true, + "machine_extruder_trains": + { + "0": "renkforce_rf100_extruder_0" + } + }, + + "overrides": { + "adhesion_type": { + "default_value": "skirt" + }, + "bottom_thickness": { + "value": "0.6" + }, + "brim_width": { + "value": "3.0" + }, + "cool_fan_enabled": { + "value": "True" + }, + "cool_fan_full_at_height": { + "value": "0.5" + }, + "cool_fan_speed_max": { + "value": "100.0" + }, + "cool_fan_speed_min": { + "value": "100.0" + }, + "cool_lift_head": { + "value": "True" + }, + "cool_min_layer_time": { + "value": "1.0" + }, + "cool_min_speed": { + "value": "5.0" + }, + "infill_before_walls": { + "value": "True" + }, + "infill_line_width": { + "value": "0.6" + }, + "infill_overlap": { + "value": "15.0" + }, + "infill_sparse_density": { + "value": "26.0" + }, + "ironing_enabled": { + "value": "True" + }, + "layer_0_z_overlap": { + "value": "0.11" + }, + "layer_height_0": { + "value": "0.3" + }, + "machine_depth": { + "value": "120" + }, + "machine_end_gcode": { + "default_value": ";End GCode\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-4 F300 ;move Z up a bit and retract filament even more\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG0 Z{machine_height} F1800 ;move the platform all the way down\nG28 X0 Y0 F1800 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done" + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "machine_head_with_fans_polygon": + { + "default_value": [ + [-26, -27], + [38, -27], + [38, 55], + [-26, 55] + ] + }, + "gantry_height": { + "value": "8" + }, + "machine_height": { + "value": "120" + }, + "machine_name": { + "default_value": "Renkforce RF100 V2" + }, + "machine_start_gcode": { + "default_value": ";Sliced at: {day} {date} {time}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG1 Z5.0 F1800 ;move Z to 5mm\nG28 X0 Y0 F1800 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstop\nG92 E0 ;zero the extruded length\nG1 F200 E6.0 ;extrude 6.0mm of feed stock to build pressure\nG1 Z5.0 F300 ;move the platform down 5mm\nG92 E0 ;zero the extruded length again\nG1 F1800\n;Put printing message on LCD screen\nM117 Printing..." + }, + "machine_width": { + "value": "120" + }, + "material_bed_temperature": { + "enabled": false + }, + "material_flow": { + "value": "110" + }, + "material_print_temperature": { + "value": "210.0" + }, + "ooze_shield_enabled": { + "value": "True" + }, + "raft_airgap": { + "value": "0.33" + }, + "raft_base_line_spacing": { + "value": "3.0" + }, + "raft_base_line_width": { + "value": "1.0" + }, + "raft_base_thickness": { + "value": "0.3" + }, + "raft_interface_line_spacing": { + "value": "3.0" + }, + "raft_interface_line_width": { + "value": "0.4" + }, + "raft_interface_thickness": { + "value": "0.27" + }, + "raft_margin": { + "value": "6.0" + }, + "raft_speed": { + "value": "20.0" + }, + "raft_surface_layers": { + "value": "2" + }, + "raft_surface_line_spacing": { + "value": "0.4" + }, + "raft_surface_line_width": { + "value": "0.4" + }, + "raft_surface_thickness": { + "value": "0.1" + }, + "retraction_amount": { + "value": "5.0" + }, + "retraction_combing": { + "default_value": "all" + }, + "retraction_enable": { + "value": "True" + }, + "retraction_min_travel": { + "value": "1.5" + }, + "skin_overlap": { + "value": "15.0" + }, + "skirt_brim_minimal_length": { + "value": "150.0" + }, + "skirt_gap": { + "value": "3.0" + }, + "skirt_line_count": { + "value": "3" + }, + "speed_infill": { + "value": "50.0" + }, + "speed_layer_0": { + "value": "15.0" + }, + "speed_print": { + "value": "50.0" + }, + "speed_topbottom": { + "value": "30.0" + }, + "speed_travel": { + "value": "50.0" + }, + "speed_wall_0": { + "value": "25.0" + }, + "speed_wall_x": { + "value": "35.0" + }, + "support_angle": { + "value": "60.0" + }, + "support_enable": { + "value": "False" + }, + "support_infill_rate": { + "value": "15 if support_enable else 0 if support_tree_enable else 15" + }, + "support_line_width": { + "value": "0.6" + }, + "support_pattern": { + "default_value": "lines" + }, + "support_type": { + "default_value": "everywhere" + }, + "support_xy_distance": { + "value": "0.7" + }, + "support_z_distance": { + "value": "0.35" + }, + "top_bottom_thickness": { + "value": "0.8" + }, + "wall_thickness": { + "value": "0.8" + } + } +} diff --git a/resources/definitions/renkforce_rf100_xl.def.json b/resources/definitions/renkforce_rf100_xl.def.json new file mode 100644 index 0000000000..4ad438fb08 --- /dev/null +++ b/resources/definitions/renkforce_rf100_xl.def.json @@ -0,0 +1,216 @@ +{ + "version": 2, + "name": "Renkforce RF100 XL", + "inherits": "fdmprinter", + "metadata": { + "author": "Simon Peter (based on RF100.ini by Conrad Electronic SE)", + "file_formats": "text/x-gcode", + "manufacturer": "Renkforce", + "visible": true, + "machine_extruder_trains": + { + "0": "renkforce_rf100_xl_extruder_0" + } + }, + + "overrides": { + "adhesion_type": { + "default_value": "skirt" + }, + "bottom_thickness": { + "value": "0.6" + }, + "brim_width": { + "value": "3.0" + }, + "cool_fan_enabled": { + "value": "True" + }, + "cool_fan_full_at_height": { + "value": "0.5" + }, + "cool_fan_speed_max": { + "value": "100.0" + }, + "cool_fan_speed_min": { + "value": "100.0" + }, + "cool_lift_head": { + "value": "True" + }, + "cool_min_layer_time": { + "value": "1.0" + }, + "cool_min_speed": { + "value": "5.0" + }, + "infill_before_walls": { + "value": "True" + }, + "infill_line_width": { + "value": "0.6" + }, + "infill_overlap": { + "value": "15.0" + }, + "infill_sparse_density": { + "value": "26.0" + }, + "ironing_enabled": { + "value": "True" + }, + "layer_0_z_overlap": { + "value": "0.11" + }, + "layer_height_0": { + "value": "0.3" + }, + "machine_depth": { + "value": "200" + }, + "machine_end_gcode": { + "default_value": ";End GCode\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-4 F300 ;move Z up a bit and retract filament even more\nM104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG0 Z{machine_height} F1800 ;move the platform all the way down\nG28 X0 Y0 F1800 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning\nM117 Done" + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "machine_heated_bed": { + "default_value": "true" + }, + "machine_height": { + "value": "200" + }, + "machine_name": { + "default_value": "Renkforce RF100 XL" + }, + "machine_start_gcode": { + "default_value": ";Sliced at: {day} {date} {time}\nG21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG1 Z5.0 F1800 ;move Z to 5mm\nG28 X0 Y0 F1800 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstop\nG92 E0 ;zero the extruded length\nG1 F200 E6.0 ;extrude 6.0mm of feed stock to build pressure\nG1 Z5.0 F300 ;move the platform down 5mm\nG92 E0 ;zero the extruded length again\nG1 F1800\n;Put printing message on LCD screen\nM117 Printing..." + }, + "machine_width": { + "value": "200" + }, + "material_bed_temperature": { + "value": "70" + }, + "material_print_temperature": { + "value": "210.0" + }, + "ooze_shield_enabled": { + "value": "True" + }, + "raft_airgap": { + "value": "0.33" + }, + "raft_base_line_spacing": { + "value": "3.0" + }, + "raft_base_line_width": { + "value": "1.0" + }, + "raft_base_thickness": { + "value": "0.3" + }, + "raft_interface_line_spacing": { + "value": "3.0" + }, + "raft_interface_line_width": { + "value": "0.4" + }, + "raft_interface_thickness": { + "value": "0.27" + }, + "raft_margin": { + "value": "6.0" + }, + "raft_speed": { + "value": "20.0" + }, + "raft_surface_layers": { + "value": "2" + }, + "raft_surface_line_spacing": { + "value": "0.4" + }, + "raft_surface_line_width": { + "value": "0.4" + }, + "raft_surface_thickness": { + "value": "0.1" + }, + "retraction_amount": { + "value": "5.0" + }, + "retraction_combing": { + "default_value": "all" + }, + "retraction_enable": { + "value": "True" + }, + "retraction_min_travel": { + "value": "1.5" + }, + "skin_overlap": { + "value": "15.0" + }, + "skirt_brim_minimal_length": { + "value": "150.0" + }, + "skirt_gap": { + "value": "3.0" + }, + "skirt_line_count": { + "value": "3" + }, + "speed_infill": { + "value": "50.0" + }, + "speed_layer_0": { + "value": "15.0" + }, + "speed_print": { + "value": "50.0" + }, + "speed_topbottom": { + "value": "30.0" + }, + "speed_travel": { + "value": "50.0" + }, + "speed_wall_0": { + "value": "25.0" + }, + "speed_wall_x": { + "value": "35.0" + }, + "support_angle": { + "value": "60.0" + }, + "support_enable": { + "value": "False" + }, + "support_infill_rate": { + "value": "15 if support_enable else 0 if support_tree_enable else 15" + }, + "support_line_width": { + "value": "0.6" + }, + "support_pattern": { + "default_value": "lines" + }, + "support_type": { + "default_value": "everywhere" + }, + "support_xy_distance": { + "value": "0.7" + }, + "support_z_distance": { + "value": "0.35" + }, + "top_bottom_thickness": { + "value": "0.8" + }, + "wall_thickness": { + "value": "0.8" + } + } +} diff --git a/resources/extruders/geeetech_A10M_1.def.json b/resources/extruders/geeetech_A10M_1.def.json new file mode 100644 index 0000000000..b3c87489e9 --- /dev/null +++ b/resources/extruders/geeetech_A10M_1.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A10M", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A10M_2.def.json b/resources/extruders/geeetech_A10M_2.def.json new file mode 100644 index 0000000000..59c8d36aab --- /dev/null +++ b/resources/extruders/geeetech_A10M_2.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A10M", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A10T_1.def.json b/resources/extruders/geeetech_A10T_1.def.json new file mode 100644 index 0000000000..240a71ccc4 --- /dev/null +++ b/resources/extruders/geeetech_A10T_1.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A10T", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "2" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A10T_2.def.json b/resources/extruders/geeetech_A10T_2.def.json new file mode 100644 index 0000000000..cc93fdd416 --- /dev/null +++ b/resources/extruders/geeetech_A10T_2.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A10T", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "2" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A10T_3.def.json b/resources/extruders/geeetech_A10T_3.def.json new file mode 100644 index 0000000000..ddee8fcd3b --- /dev/null +++ b/resources/extruders/geeetech_A10T_3.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 3", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A10T", + "position": "2" + }, + + "overrides": { + "extruder_nr": { + "default_value": 2, + "maximum_value": "2" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A10_1.def.json b/resources/extruders/geeetech_A10_1.def.json new file mode 100644 index 0000000000..bcf889ab43 --- /dev/null +++ b/resources/extruders/geeetech_A10_1.def.json @@ -0,0 +1,17 @@ +{ + + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A10", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A20M_1.def.json b/resources/extruders/geeetech_A20M_1.def.json new file mode 100644 index 0000000000..b0c00f4144 --- /dev/null +++ b/resources/extruders/geeetech_A20M_1.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A20M", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A20M_2.def.json b/resources/extruders/geeetech_A20M_2.def.json new file mode 100644 index 0000000000..51dd29723a --- /dev/null +++ b/resources/extruders/geeetech_A20M_2.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A20M", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A20T_1.def.json b/resources/extruders/geeetech_A20T_1.def.json new file mode 100644 index 0000000000..4880f2147b --- /dev/null +++ b/resources/extruders/geeetech_A20T_1.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A20T", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "2" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A20T_2.def.json b/resources/extruders/geeetech_A20T_2.def.json new file mode 100644 index 0000000000..d001cbc291 --- /dev/null +++ b/resources/extruders/geeetech_A20T_2.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A20T", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "2" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A20T_3.def.json b/resources/extruders/geeetech_A20T_3.def.json new file mode 100644 index 0000000000..db60908cb9 --- /dev/null +++ b/resources/extruders/geeetech_A20T_3.def.json @@ -0,0 +1,19 @@ +{ + "version": 2, + "name": "Extruder 3", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A20T", + "position": "2" + }, + + "overrides": { + "extruder_nr": { + "default_value": 2, + "maximum_value": "2" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/geeetech_A20_1.def.json b/resources/extruders/geeetech_A20_1.def.json new file mode 100644 index 0000000000..ce47abb402 --- /dev/null +++ b/resources/extruders/geeetech_A20_1.def.json @@ -0,0 +1,16 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "geeetech_A20", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + + } +} diff --git a/resources/extruders/makeit_mx_dual_1st.def.json b/resources/extruders/makeit_mx_dual_1st.def.json new file mode 100644 index 0000000000..48a15bb4e7 --- /dev/null +++ b/resources/extruders/makeit_mx_dual_1st.def.json @@ -0,0 +1,27 @@ +{ + "version": 2, + "name": "1st Extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "makeit_pro_mx", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0.0 }, + "machine_nozzle_offset_y": { "default_value": 0.0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 }, + + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "value": "prime_tower_position_x" }, + "machine_extruder_start_pos_y": { "value": "prime_tower_position_y" }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "value": "prime_tower_position_x" }, + "machine_extruder_end_pos_y": { "value": "prime_tower_position_y" } + } +} \ No newline at end of file diff --git a/resources/extruders/makeit_mx_dual_2nd.def.json b/resources/extruders/makeit_mx_dual_2nd.def.json new file mode 100644 index 0000000000..b17b1b9051 --- /dev/null +++ b/resources/extruders/makeit_mx_dual_2nd.def.json @@ -0,0 +1,27 @@ +{ + "version": 2, + "name": "2nd Extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "makeit_pro_mx", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0.0 }, + "machine_nozzle_offset_y": { "default_value": 0.0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 }, + + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "value": "prime_tower_position_x" }, + "machine_extruder_start_pos_y": { "value": "prime_tower_position_y" }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "value": "prime_tower_position_x" }, + "machine_extruder_end_pos_y": { "value": "prime_tower_position_y" } + } +} \ No newline at end of file diff --git a/resources/extruders/mp_mini_delta_extruder_0.def.json b/resources/extruders/mp_mini_delta_extruder_0.def.json new file mode 100644 index 0000000000..b4eab5c7a2 --- /dev/null +++ b/resources/extruders/mp_mini_delta_extruder_0.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 0", + "inherits": "fdmextruder", + "metadata": { + "machine": "mp_mini_delta", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/renkforce_rf100_xl_extruder_0.def.json b/resources/extruders/renkforce_rf100_xl_extruder_0.def.json new file mode 100644 index 0000000000..718a3738c4 --- /dev/null +++ b/resources/extruders/renkforce_rf100_xl_extruder_0.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "renkforce_rf100_xl", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/meshes/mp_mini_delta_platform.stl b/resources/meshes/mp_mini_delta_platform.stl new file mode 100644 index 0000000000..603cb26e22 Binary files /dev/null and b/resources/meshes/mp_mini_delta_platform.stl differ diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index d6f50f939b..ba305a1104 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -238,7 +238,7 @@ UM.MainWindow if (filename.toLowerCase().endsWith(".curapackage")) { // Try to install plugin & close. - CuraApplication.getPackageManager().installPackageViaDragAndDrop(filename); + CuraApplication.installPackageViaDragAndDrop(filename); packageInstallDialog.text = catalog.i18nc("@label", "This package will be installed after restarting."); packageInstallDialog.icon = StandardIcon.Information; packageInstallDialog.open(); diff --git a/resources/qml/MainWindow/ApplicationMenu.qml b/resources/qml/MainWindow/ApplicationMenu.qml index 1ddb0410b2..05e349841b 100644 --- a/resources/qml/MainWindow/ApplicationMenu.qml +++ b/resources/qml/MainWindow/ApplicationMenu.qml @@ -127,8 +127,8 @@ Item icon: StandardIcon.Question onYes: { - CuraApplication.deleteAll(); - Cura.Actions.resetProfile.trigger(); + CuraApplication.resetWorkspace() + Cura.Actions.resetProfile.trigger() UM.Controller.setActiveStage("PrepareStage") } } diff --git a/resources/qml/Preferences/Materials/MaterialsPage.qml b/resources/qml/Preferences/Materials/MaterialsPage.qml index d635b2b721..791d6685de 100644 --- a/resources/qml/Preferences/Materials/MaterialsPage.qml +++ b/resources/qml/Preferences/Materials/MaterialsPage.qml @@ -125,6 +125,7 @@ Item id: createMenuButton text: catalog.i18nc("@action:button", "Create") iconName: "list-add" + enabled: Cura.MachineManager.activeMachine.hasMaterials onClicked: { forceActiveFocus(); @@ -174,7 +175,7 @@ Item forceActiveFocus(); importMaterialDialog.open(); } - visible: true + enabled: Cura.MachineManager.activeMachine.hasMaterials } // Export button diff --git a/resources/qml/Toolbar.qml b/resources/qml/Toolbar.qml index c2a70143c3..ab32bff619 100644 --- a/resources/qml/Toolbar.qml +++ b/resources/qml/Toolbar.qml @@ -202,8 +202,9 @@ Item // dragging a tool handle. Rectangle { - x: -base.x + base.mouseX + UM.Theme.getSize("default_margin").width - y: -base.y + base.mouseY + UM.Theme.getSize("default_margin").height + id: toolInfo + x: visible ? -base.x + base.mouseX + UM.Theme.getSize("default_margin").width: 0 + y: visible ? -base.y + base.mouseY + UM.Theme.getSize("default_margin").height: 0 width: toolHint.width + UM.Theme.getSize("default_margin").width height: toolHint.height; diff --git a/resources/qml/Widgets/ScrollableTextArea.qml b/resources/qml/Widgets/ScrollableTextArea.qml index b806087f9a..48a7f49255 100644 --- a/resources/qml/Widgets/ScrollableTextArea.qml +++ b/resources/qml/Widgets/ScrollableTextArea.qml @@ -1,36 +1,36 @@ -// Copyright (c) 2019 Ultimaker B.V. -// Cura is released under the terms of the LGPLv3 or higher. - -import QtQuick 2.10 -import QtQuick.Controls 2.3 - -import UM 1.3 as UM -import Cura 1.1 as Cura - - -// -// Cura-style TextArea with scrolls -// -ScrollView -{ - property alias textArea: _textArea - - clip: true - - background: Rectangle // Border - { - color: UM.Theme.getColor("main_background") - border.color: UM.Theme.getColor("lining") - border.width: UM.Theme.getSize("default_lining").width - } - - TextArea - { - id: _textArea - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - textFormat: TextEdit.PlainText - renderType: Text.NativeRendering - selectByMouse: true - } -} +// Copyright (c) 2019 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Controls 2.3 + +import UM 1.3 as UM +import Cura 1.1 as Cura + + +// +// Cura-style TextArea with scrolls +// +ScrollView +{ + property alias textArea: _textArea + + clip: true + + background: Rectangle // Border + { + color: UM.Theme.getColor("main_background") + border.color: UM.Theme.getColor("thick_lining") + border.width: UM.Theme.getSize("default_lining").width + } + + TextArea + { + id: _textArea + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + textFormat: TextEdit.PlainText + renderType: Text.NativeRendering + selectByMouse: true + } +} diff --git a/resources/setting_visibility/expert.cfg b/resources/setting_visibility/expert.cfg index 0080fbb705..03b43f71db 100644 --- a/resources/setting_visibility/expert.cfg +++ b/resources/setting_visibility/expert.cfg @@ -388,6 +388,7 @@ support_conical_enabled support_conical_angle support_conical_min_width magic_fuzzy_skin_enabled +magic_fuzzy_skin_outside_only magic_fuzzy_skin_thickness magic_fuzzy_skin_point_density magic_fuzzy_skin_point_dist diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 282004c3a9..9db024397a 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -8,7 +8,7 @@ "main_background": [39, 44, 48, 255], "message_background": [39, 44, 48, 255], "wide_lining": [31, 36, 39, 255], - "thick_lining": [255, 255, 255, 30], + "thick_lining": [255, 255, 255, 60], "lining": [64, 69, 72, 255], "viewport_overlay": [30, 36, 39, 255], diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index e5009d8633..225e0db569 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -149,7 +149,7 @@ "main_background": [255, 255, 255, 255], "wide_lining": [245, 245, 245, 255], - "thick_lining": [127, 127, 127, 255], + "thick_lining": [180, 180, 180, 255], "lining": [192, 193, 194, 255], "viewport_overlay": [246, 246, 246, 255], @@ -520,6 +520,7 @@ "action_button": [15.0, 2.5], "action_button_icon": [1.0, 1.0], "action_button_radius": [0.15, 0.15], + "dialog_primary_button_padding": [3.0, 0], "radio_button": [1.3, 1.3], diff --git a/scripts/check_invalid_imports.py b/scripts/check_invalid_imports.py new file mode 100644 index 0000000000..ba21b9f822 --- /dev/null +++ b/scripts/check_invalid_imports.py @@ -0,0 +1,65 @@ +import os +import re +import sys +from pathlib import Path + +""" +Run this file with the Cura project root as the working directory +Checks for invalid imports. When importing from plugins, there will be no problems when running from source, +but for some build types the plugins dir is not on the path, so relative imports should be used instead. eg: +from ..UltimakerCloudScope import UltimakerCloudScope <-- OK +import plugins.Toolbox.src ... <-- NOT OK +""" + + +class InvalidImportsChecker: + # compile regex + REGEX = re.compile(r"^\s*(from plugins|import plugins)") + + def check(self): + """ Checks for invalid imports + + :return: True if checks passed, False when the test fails + """ + cwd = os.getcwd() + cura_result = checker.check_dir(os.path.join(cwd, "cura")) + plugins_result = checker.check_dir(os.path.join(cwd, "plugins")) + result = cura_result and plugins_result + if not result: + print("error: sources contain invalid imports. Use relative imports when referencing plugin source files") + + return result + + def check_dir(self, root_dir: str) -> bool: + """ Checks a directory for invalid imports + + :return: True if checks passed, False when the test fails + """ + passed = True + for path_like in Path(root_dir).rglob('*.py'): + if not self.check_file(str(path_like)): + passed = False + + return passed + + def check_file(self, file_path): + """ Checks a file for invalid imports + + :return: True if checks passed, False when the test fails + """ + passed = True + with open(file_path, 'r', encoding = "utf-8") as inputFile: + # loop through each line in file + for line_i, line in enumerate(inputFile, 1): + # check if we have a regex match + match = self.REGEX.search(line) + if match: + path = os.path.relpath(file_path) + print("{path}:{line_i}:{match}".format(path=path, line_i=line_i, match=match.group(1))) + passed = False + return passed + + +if __name__ == "__main__": + checker = InvalidImportsChecker() + sys.exit(0 if checker.check() else 1)