diff --git a/README.md b/README.md index ff39e4142a..345a55d12f 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ For crashes and similar issues, please attach the following information: * (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output) * The Cura GUI log file, located at - * `%APPDATA%\cura\\cura.log` (Windows), or usually `C:\Users\\\AppData\Roaming\cura\\cura.log` - * `$USER/Library/Application Support/cura//cura.log` (OSX) - * `$USER/.local/share/cura//cura.log` (Ubuntu/Linux) + * `%APPDATA%\cura\\cura.log` (Windows), or usually `C:\Users\\AppData\Roaming\cura\\cura.log` + * `$HOME/Library/Application Support/cura//cura.log` (OSX) + * `$HOME/.local/share/cura//cura.log` (Ubuntu/Linux) If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder diff --git a/cura/API/Account.py b/cura/API/Account.py index 728d0690a3..2e48a040ad 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -109,7 +109,6 @@ class Account(QObject): self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged) self._authorization_service.loadAuthDataFromPreferences() - @pyqtProperty(int, notify=syncStateChanged) def syncState(self): return self._sync_state @@ -178,6 +177,7 @@ class Account(QObject): if error_message: if self._error_message: self._error_message.hide() + Logger.log("w", "Failed to login: %s", error_message) self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed")) self._error_message.show() self._logged_in = False diff --git a/cura/Machines/Models/MaterialManagementModel.py b/cura/Machines/Models/MaterialManagementModel.py index cd35a78353..6663dbdae1 100644 --- a/cura/Machines/Models/MaterialManagementModel.py +++ b/cura/Machines/Models/MaterialManagementModel.py @@ -2,9 +2,10 @@ # Cura is released under the terms of the LGPLv3 or higher. import copy # To duplicate materials. -from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page. +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl from typing import Any, Dict, Optional, TYPE_CHECKING import uuid # To generate new GUIDs for new materials. +import zipfile # To export all materials in a .zip archive. from UM.i18n import i18nCatalog from UM.Logger import Logger @@ -20,11 +21,6 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") class MaterialManagementModel(QObject): - """Proxy class to the materials page in the preferences. - - This class handles the actions in that page, such as creating new materials, renaming them, etc. - """ - favoritesChanged = pyqtSignal(str) """Triggered when a favorite is added or removed. @@ -264,3 +260,40 @@ class MaterialManagementModel(QObject): self.favoritesChanged.emit(material_base_file) except ValueError: # Material was not in the favorites list. Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file)) + + @pyqtSlot(result = QUrl) + def getPreferredExportAllPath(self) -> QUrl: + """ + Get the preferred path to export materials to. + + If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local + file path. + :return: The preferred path to export all materials to. + """ + cura_application = cura.CuraApplication.CuraApplication.getInstance() + device_manager = cura_application.getOutputDeviceManager() + devices = device_manager.getOutputDevices() + for device in devices: + if device.__class__.__name__ == "RemovableDriveOutputDevice": + return QUrl.fromLocalFile(device.getId()) + else: # No removable drives? Use local path. + return cura_application.getDefaultPath("dialog_material_path") + + @pyqtSlot(QUrl) + def exportAll(self, file_path: QUrl) -> None: + """ + Export all materials to a certain file path. + :param file_path: The path to export the materials to. + """ + registry = CuraContainerRegistry.getInstance() + + archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED) + for metadata in registry.findInstanceContainersMetadata(type = "material"): + if metadata["base_file"] != metadata["id"]: # Only process base files. + continue + if metadata["id"] == "empty_material": # Don't export the empty material. + continue + material = registry.findContainers(id = metadata["id"])[0] + suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix + filename = metadata["id"] + "." + suffix + archive.writestr(filename, material.serialize()) diff --git a/cura/OAuth2/LocalAuthorizationServer.py b/cura/OAuth2/LocalAuthorizationServer.py index ac14b00985..219191c295 100644 --- a/cura/OAuth2/LocalAuthorizationServer.py +++ b/cura/OAuth2/LocalAuthorizationServer.py @@ -54,6 +54,7 @@ class LocalAuthorizationServer: 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. + Logger.log("d", "Auth web server was already running. Updating the verification code") self._web_server.setVerificationCode(verification_code) return @@ -85,6 +86,7 @@ class LocalAuthorizationServer: except OSError: # OS error can happen if the socket was already closed. We really don't care about that case. pass + Logger.log("d", "Local oauth2 web server was shut down") self._web_server = None self._web_server_thread = None @@ -96,12 +98,13 @@ class LocalAuthorizationServer: :return: None """ + Logger.log("d", "Local web server for authorization has started") if self._web_server: if sys.platform == "win32": try: self._web_server.serve_forever() - except OSError as e: - Logger.warning(str(e)) + except OSError: + Logger.logException("w", "An exception happened while serving the auth server") else: # Leave the default behavior in non-windows platforms self._web_server.serve_forever() diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index 2c7cbf5e25..282034c0ee 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -86,6 +86,14 @@ class GlobalStack(CuraContainerStack): def supportsNetworkConnection(self): return self.getMetaDataEntry("supports_network_connection", False) + @pyqtProperty(bool, constant = True) + def supportsMaterialExport(self): + """ + Whether the printer supports Cura's export format of material profiles. + :return: ``True`` if it supports it, or ``False`` if not. + """ + return self.getMetaDataEntry("supports_material_export", False) + @classmethod def getLoadingPriority(cls) -> int: return 2 diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 1aa6c86dcb..fa9a8c5474 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -4,12 +4,12 @@ import argparse #To run the engine in debug mode if the front-end is in debug mode. from collections import defaultdict import os -from PyQt5.QtCore import QObject, QTimer, pyqtSlot +from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSlot import sys from time import time from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING -from PyQt5.QtGui import QImage +from PyQt5.QtGui import QDesktopServices, QImage from UM.Backend.Backend import Backend, BackendState from UM.Scene.SceneNode import SceneNode @@ -157,6 +157,18 @@ class CuraEngineBackend(QObject, Backend): self.determineAutoSlicing() application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) + self._slicing_error_message = Message( + text = catalog.i18nc("@message", "Slicing failed with an unexpected error. Please consider reporting a bug on our issue tracker."), + title = catalog.i18nc("@message:title", "Slicing failed") + ) + self._slicing_error_message.addAction( + action_id = "report_bug", + name = catalog.i18nc("@message:button", "Report a bug"), + description = catalog.i18nc("@message:description", "Report a bug on Ultimaker Cura's issue tracker."), + icon = "[no_icon]" + ) + self._slicing_error_message.actionTriggered.connect(self._reportBackendError) + self._snapshot = None #type: Optional[QImage] application.initializationFinished.connect(self.initialize) @@ -922,9 +934,22 @@ class CuraEngineBackend(QObject, Backend): if not self._restart: if self._process: # type: ignore - Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore + return_code = self._process.wait() + if return_code != 0: + Logger.log("e", f"Backend exited abnormally with return code {return_code}!") + self._slicing_error_message.show() + self.setState(BackendState.Error) + self.stopSlicing() + else: + Logger.log("d", "Backend finished slicing. Resetting process and socket.") self._process = None # type: ignore + def _reportBackendError(self, _message_id: str, _action_id: str) -> None: + """ + Triggered when the user wants to report an error in the back-end. + """ + QDesktopServices.openUrl(QUrl("https://github.com/Ultimaker/Cura/issues/new/choose")) + def _onGlobalStackChanged(self) -> None: """Called when the global container stack changes""" diff --git a/plugins/DigitalLibrary/resources/images/projects_not_found.svg b/plugins/DigitalLibrary/resources/images/projects_not_found.svg new file mode 100644 index 0000000000..ba118ebc0a --- /dev/null +++ b/plugins/DigitalLibrary/resources/images/projects_not_found.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/DigitalLibrary/resources/qml/CreateNewProjectPopup.qml b/plugins/DigitalLibrary/resources/qml/CreateNewProjectPopup.qml index 75fb8d5811..a7297c12fb 100644 --- a/plugins/DigitalLibrary/resources/qml/CreateNewProjectPopup.qml +++ b/plugins/DigitalLibrary/resources/qml/CreateNewProjectPopup.qml @@ -93,7 +93,7 @@ Popup } validator: RegExpValidator { - regExp: /^[^\\\/\*\?\|\[\]]{0,96}$/ + regExp: /^[^\\\/\*\?\|\[\]]{0,99}$/ } text: PrintInformation.jobName @@ -148,7 +148,7 @@ Popup anchors.bottom: parent.bottom anchors.right: parent.right text: "Create" - enabled: newProjectNameTextField.text != "" && !busy + enabled: newProjectNameTextField.text.length >= 2 && !busy onClicked: { diff --git a/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml b/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml index 03bd655957..30e3513019 100644 --- a/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml @@ -63,7 +63,7 @@ Item anchors.topMargin: UM.Theme.getSize("thin_margin").height validator: RegExpValidator { - regExp: /^[^\\\/\*\?\|\[\]]{0,96}$/ + regExp: /^[\w\-\. ()]{0,255}$/ } text: PrintInformation.jobName @@ -200,7 +200,7 @@ Item anchors.bottom: parent.bottom anchors.right: parent.right text: "Save" - enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text != "" + enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 onClicked: { diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index 2de0e78cc7..8b919e299d 100644 --- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml @@ -1,10 +1,12 @@ // Copyright (C) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 import QtQuick.Window 2.2 import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.1 import UM 1.2 as UM import Cura 1.6 as Cura @@ -18,7 +20,7 @@ Item width: parent.width height: parent.height - property alias createNewProjectButtonVisible: createNewProjectButton.visible + property bool createNewProjectButtonVisible: true anchors { @@ -29,31 +31,58 @@ Item margins: UM.Theme.getSize("default_margin").width } - Label + RowLayout { - id: selectProjectLabel + id: headerRow - text: "Select Project" - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("small_button_text") - anchors.top: parent.top - anchors.left: parent.left - visible: projectListContainer.visible - } - - Cura.SecondaryButton - { - id: createNewProjectButton - - anchors.verticalCenter: selectProjectLabel.verticalCenter - anchors.right: parent.right - text: "New Library project" - - onClicked: + anchors { - createNewProjectPopup.open() + top: parent.top + left: parent.left + right: parent.right + } + height: childrenRect.height + spacing: UM.Theme.getSize("default_margin").width + + Cura.TextField + { + id: searchBar + Layout.fillWidth: true + implicitHeight: createNewProjectButton.height + + onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field. + + leftIcon: UM.Theme.getIcon("Magnifier") + placeholderText: "Search" + } + + Cura.SecondaryButton + { + id: createNewProjectButton + + text: "New Library project" + visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) + + onClicked: + { + createNewProjectPopup.open() + } + busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress + } + + + Cura.SecondaryButton + { + id: upgradePlanButton + + text: "Upgrade plan" + iconSource: UM.Theme.getIcon("LinkExternal") + visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) + tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects." + tooltipWidth: parent.width * 0.5 + + onClicked: Qt.openUrlExternally("https://ultimaker.com/software/ultimaker-essentials/sign-up-cura?utm_source=cura&utm_medium=software&utm_campaign=lib-max") } - busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress } Item @@ -76,19 +105,18 @@ Item { id: digitalFactoryImage anchors.horizontalCenter: parent.horizontalCenter - source: "../images/digital_factory.svg" + source: searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg" fillMode: Image.PreserveAspectFit width: parent.width - 2 * UM.Theme.getSize("thick_margin").width - sourceSize.width: width - sourceSize.height: height } Label { id: noLibraryProjectsLabel anchors.horizontalCenter: parent.horizontalCenter - text: "It appears that you don't have any projects in the Library yet." + text: searchBar.text === "" ? "It appears that you don't have any projects in the Library yet." : "No projects found that match the search query." font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") } Cura.TertiaryButton @@ -97,6 +125,7 @@ Item anchors.horizontalCenter: parent.horizontalCenter text: "Visit Digital Library" onClicked: Qt.openUrlExternally(CuraApplication.ultimakerDigitalFactoryUrl + "/app/library") + visible: searchBar.text === "" //Show the link to Digital Library when there are no projects in the user's Library. } } } @@ -106,7 +135,7 @@ Item id: projectListContainer anchors { - top: selectProjectLabel.bottom + top: headerRow.bottom topMargin: UM.Theme.getSize("default_margin").height bottom: parent.bottom left: parent.left diff --git a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py index b0e34adaba..ad87ea9b8a 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py @@ -22,6 +22,7 @@ from .DFFileUploader import DFFileUploader from .DFLibraryFileUploadRequest import DFLibraryFileUploadRequest from .DFLibraryFileUploadResponse import DFLibraryFileUploadResponse from .DFPrintJobUploadRequest import DFPrintJobUploadRequest +from .DigitalFactoryFeatureBudgetResponse import DigitalFactoryFeatureBudgetResponse from .DigitalFactoryFileResponse import DigitalFactoryFileResponse from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse from .PaginationLinks import PaginationLinks @@ -54,9 +55,67 @@ class DigitalFactoryApiClient: self._http = HttpRequestManager.getInstance() self._on_error = on_error self._file_uploader = None # type: Optional[DFFileUploader] + self._library_max_private_projects: Optional[int] = None self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager] + def checkUserHasAccess(self, callback: Callable) -> None: + """Checks if the user has any sort of access to the digital library. + A user is considered to have access if the max-# of private projects is greater then 0 (or -1 for unlimited). + """ + + def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None: + if (response is not None and isinstance(response, DigitalFactoryFeatureBudgetResponse) and + response.library_max_private_projects is not None): + callback( + response.library_max_private_projects == -1 or # Note: -1 is unlimited + response.library_max_private_projects > 0) + self._library_max_private_projects = response.library_max_private_projects + else: + Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}") + callback(False) + + self._http.get(f"{self.CURA_API_ROOT}/feature_budgets", + scope = self._scope, + callback = self._parseCallback(callbackWrap, DigitalFactoryFeatureBudgetResponse, callbackWrap), + error_callback = callbackWrap, + timeout = self.DEFAULT_REQUEST_TIMEOUT) + + def checkUserCanCreateNewLibraryProject(self, callback: Callable) -> None: + """ + Checks if the user is allowed to create new library projects. + A user is allowed to create new library projects if the haven't reached their maximum allowed private projects. + """ + + def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None: + if response is not None: + if isinstance(response, DigitalFactoryProjectResponse): # The user has only one private project + callback(True) + elif isinstance(response, list) and all(isinstance(r, DigitalFactoryProjectResponse) for r in response): + callback(len(response) < cast(int, self._library_max_private_projects)) + else: + Logger.warning(f"Digital Factory: Incorrect response type received when requesting private projects: {str(response)}") + callback(False) + else: + Logger.warning(f"Digital Factory: Response is empty, likely an error: {str(response)}") + callback(False) + + if self._library_max_private_projects is not None and self._library_max_private_projects > 0: + # The user has a limit in the number of private projects they can create. Check whether they have already + # reached that limit. + # Note: Set the pagination manager to None when doing this get request, or else the next/previous links + # of the pagination will become corrupted + url = f"{self.CURA_API_ROOT}/projects?shared=false&limit={self._library_max_private_projects}" + self._http.get(url, + scope = self._scope, + callback = self._parseCallback(callbackWrap, DigitalFactoryProjectResponse, callbackWrap, pagination_manager = None), + error_callback = callbackWrap, + timeout = self.DEFAULT_REQUEST_TIMEOUT) + else: + # If the limit is -1, then the user is allowed unlimited projects. If its 0 then they are not allowed to + # create any projects + callback(self._library_max_private_projects == -1) + def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None: """ Retrieves a digital factory project by its library project id. @@ -73,7 +132,7 @@ class DigitalFactoryApiClient: error_callback = failed, timeout = self.DEFAULT_REQUEST_TIMEOUT) - def getProjectsFirstPage(self, on_finished: Callable[[List[DigitalFactoryProjectResponse]], Any], failed: Callable) -> None: + def getProjectsFirstPage(self, search_filter: str, on_finished: Callable[[List[DigitalFactoryProjectResponse]], Any], failed: Callable) -> None: """ Retrieves digital factory projects for the user that is currently logged in. @@ -81,13 +140,18 @@ class DigitalFactoryApiClient: according to the limit set in the pagination manager. If there is no projects pagination manager, this function leaves the project limit to the default set on the server side (999999). + :param search_filter: Text to filter the search results. If given an empty string, results are not filtered. :param on_finished: The function to be called after the result is parsed. :param failed: The function to be called if the request fails. """ - url = "{}/projects".format(self.CURA_API_ROOT) + url = f"{self.CURA_API_ROOT}/projects" + query_character = "?" if self._projects_pagination_mgr: self._projects_pagination_mgr.reset() # reset to clear all the links and response metadata - url += "?limit={}".format(self._projects_pagination_mgr.limit) + url += f"{query_character}limit={self._projects_pagination_mgr.limit}" + query_character = "&" + if search_filter != "": + url += f"{query_character}search={search_filter}" self._http.get(url, scope = self._scope, @@ -301,12 +365,10 @@ class DigitalFactoryApiClient: :param on_finished: The function to be called after the result is parsed. :param on_error: The function to be called if anything goes wrong. """ - - display_name = re.sub(r"[^a-zA-Z0-9- ./™®ö+']", " ", project_name) - Logger.log("i", "Attempt to create new DF project '{}'.".format(display_name)) + Logger.log("i", "Attempt to create new DF project '{}'.".format(project_name)) url = "{}/projects".format(self.CURA_API_ROOT) - data = json.dumps({"data": {"display_name": display_name}}).encode() + data = json.dumps({"data": {"display_name": project_name}}).encode() self._http.put(url, scope = self._scope, data = data, diff --git a/plugins/DigitalLibrary/src/DigitalFactoryController.py b/plugins/DigitalLibrary/src/DigitalFactoryController.py index 352a8c70f2..cd0f0be638 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryController.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryController.py @@ -1,4 +1,6 @@ # Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + import json import math import os @@ -8,7 +10,7 @@ from enum import IntEnum from pathlib import Path from typing import Optional, List, Dict, Any, cast -from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QUrl +from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QTimer, QUrl from PyQt5.QtNetwork import QNetworkReply from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType @@ -89,6 +91,12 @@ class DigitalFactoryController(QObject): uploadFileError = Signal() uploadFileFinished = Signal() + """Signal to inform about the state of user access.""" + userAccessStateChanged = pyqtSignal(bool) + + """Signal to inform whether the user is allowed to create more Library projects.""" + userCanCreateNewLibraryProjectChanged = pyqtSignal(bool) + def __init__(self, application: CuraApplication) -> None: super().__init__(parent = None) @@ -106,12 +114,18 @@ class DigitalFactoryController(QObject): self._has_more_projects_to_load = False self._account = self._application.getInstance().getCuraAPI().account # type: Account + self._account.loginStateChanged.connect(self._onLoginStateChanged) self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation() # Initialize the project model self._project_model = DigitalFactoryProjectModel() self._selected_project_idx = -1 self._project_creation_error_text = "Something went wrong while creating a new project. Please try again." + self._project_filter = "" + self._project_filter_change_timer = QTimer() + self._project_filter_change_timer.setInterval(200) + self._project_filter_change_timer.setSingleShot(True) + self._project_filter_change_timer.timeout.connect(self._applyProjectFilter) # Initialize the file model self._file_model = DigitalFactoryFileModel() @@ -131,6 +145,9 @@ class DigitalFactoryController(QObject): self._application.engineCreatedSignal.connect(self._onEngineCreated) self._application.initializationFinished.connect(self._applicationInitializationFinished) + self._user_has_access = False + self._user_account_can_create_new_project = False + def clear(self) -> None: self._project_model.clearProjects() self._api.clear() @@ -143,16 +160,22 @@ class DigitalFactoryController(QObject): self.setSelectedProjectIndex(-1) + def _onLoginStateChanged(self, logged_in: bool) -> None: + def callback(has_access, **kwargs): + self._user_has_access = has_access + self.userAccessStateChanged.emit(logged_in) + + self._api.checkUserHasAccess(callback) + def userAccountHasLibraryAccess(self) -> bool: """ Checks whether the currently logged in user account has access to the Digital Library :return: True if the user account has Digital Library access, else False """ - subscriptions = [] # type: List[Dict[str, Any]] - if self._account.userProfile: - subscriptions = self._account.userProfile.get("subscriptions", []) - return len(subscriptions) > 0 + if self._user_has_access: + self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject) + return self._user_has_access def initialize(self, preselected_project_id: Optional[str] = None) -> None: self.clear() @@ -162,7 +185,7 @@ class DigitalFactoryController(QObject): if preselected_project_id: self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed) else: - self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) + self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) def setProjectAsPreselected(self, df_project: DigitalFactoryProjectResponse) -> None: """ @@ -288,6 +311,38 @@ class DigitalFactoryController(QObject): self._selected_file_indices = file_indices self.selectedFileIndicesChanged.emit(file_indices) + def setProjectFilter(self, new_filter: str) -> None: + """ + Called when the user wants to change the search filter for projects. + + The filter is not immediately applied. There is some delay to allow the user to finish typing. + :param new_filter: The new filter that the user wants to apply. + """ + self._project_filter = new_filter + self._project_filter_change_timer.start() + + """ + Signal to notify Qt that the applied filter has changed. + """ + projectFilterChanged = pyqtSignal() + + @pyqtProperty(str, notify = projectFilterChanged, fset = setProjectFilter) + def projectFilter(self) -> str: + """ + The current search filter being applied to the project list. + :return: The current search filter being applied to the project list. + """ + return self._project_filter + + def _applyProjectFilter(self) -> None: + """ + Actually apply the current filter to search for projects with the user-defined search string. + :return: + """ + self.clear() + self.projectFilterChanged.emit() + self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) + @pyqtProperty(QObject, constant = True) def digitalFactoryProjectModel(self) -> "DigitalFactoryProjectModel": return self._project_model @@ -502,7 +557,8 @@ class DigitalFactoryController(QObject): # false, we also need to clean it from the projects model self._project_model.clearProjects() self.setSelectedProjectIndex(-1) - self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) + self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed) + self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject) self.setRetrievingProjectsStatus(RetrievalStatus.InProgress) self._has_preselected_project = new_has_preselected_project self.preselectedProjectChanged.emit() @@ -511,6 +567,14 @@ class DigitalFactoryController(QObject): def hasPreselectedProject(self) -> bool: return self._has_preselected_project + def setCanCreateNewLibraryProject(self, can_create_new_library_project: bool) -> None: + self._user_account_can_create_new_project = can_create_new_library_project + self.userCanCreateNewLibraryProjectChanged.emit(self._user_account_can_create_new_project) + + @pyqtProperty(bool, fset = setCanCreateNewLibraryProject, notify = userCanCreateNewLibraryProjectChanged) + def userAccountCanCreateNewLibraryProject(self) -> bool: + return self._user_account_can_create_new_project + @pyqtSlot(str, "QStringList") def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None: """ diff --git a/plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py b/plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py new file mode 100644 index 0000000000..192f58685a --- /dev/null +++ b/plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py @@ -0,0 +1,43 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from .BaseModel import BaseModel +from typing import Optional + + +class DigitalFactoryFeatureBudgetResponse(BaseModel): + """Class representing the capabilities of a user account for Digital Library. + NOTE: For each max_..._projects fields, '-1' means unlimited! + """ + + def __init__(self, + library_can_use_business_value: Optional[bool] = False, + library_can_use_comments: Optional[bool] = False, + library_can_use_status: Optional[bool] = False, + library_can_use_tags: Optional[bool] = False, + library_can_use_technical_requirements: Optional[bool] = False, + library_max_organization_shared_projects: Optional[int] = None, # -1 means unlimited + library_max_private_projects: Optional[int] = None, # -1 means unlimited + library_max_team_shared_projects: Optional[int] = None, # -1 means unlimited + **kwargs) -> None: + + self.library_can_use_business_value = library_can_use_business_value + self.library_can_use_comments = library_can_use_comments + self.library_can_use_status = library_can_use_status + self.library_can_use_tags = library_can_use_tags + self.library_can_use_technical_requirements = library_can_use_technical_requirements + self.library_max_organization_shared_projects = library_max_organization_shared_projects # -1 means unlimited + self.library_max_private_projects = library_max_private_projects # -1 means unlimited + self.library_max_team_shared_projects = library_max_team_shared_projects # -1 means unlimited + super().__init__(**kwargs) + + def __repr__(self) -> str: + return "max private: {}, max org: {}, max team: {}".format( + self.library_max_private_projects, + self.library_max_organization_shared_projects, + self.library_max_team_shared_projects) + + # Validates the model, raising an exception if the model is invalid. + def validate(self) -> None: + super().validate() + # No validation for now, as the response can be "data: []", which should be interpreted as all False and 0's diff --git a/plugins/DigitalLibrary/src/DigitalFactoryFileProvider.py b/plugins/DigitalLibrary/src/DigitalFactoryFileProvider.py index 7a544afaa1..65a727e21a 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryFileProvider.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryFileProvider.py @@ -22,7 +22,7 @@ class DigitalFactoryFileProvider(FileProvider): self._dialog = None self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account - self._account.loginStateChanged.connect(self._onLoginStateChanged) + self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged) self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess() self.priority = 10 @@ -53,7 +53,7 @@ class DigitalFactoryFileProvider(FileProvider): if not self._dialog: Logger.log("e", "Unable to create the Digital Library Open dialog.") - def _onLoginStateChanged(self, logged_in: bool) -> None: + def _onUserAccessStateChanged(self, logged_in: bool) -> None: """ Sets the enabled status of the DigitalFactoryFileProvider according to the account's login status :param logged_in: The new login status diff --git a/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py b/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py index 202223f9b4..70e3ac34f2 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryOutputDevice.py @@ -45,7 +45,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice): self._writing = False self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account - self._account.loginStateChanged.connect(self._onLoginStateChanged) + self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged) self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess() self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation() @@ -97,7 +97,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice): if not self._dialog: Logger.log("e", "Unable to create the Digital Library Save dialog.") - def _onLoginStateChanged(self, logged_in: bool) -> None: + def _onUserAccessStateChanged(self, logged_in: bool) -> None: """ Sets the enabled status of the DigitalFactoryOutputDevice according to the account's login status :param logged_in: The new login status diff --git a/plugins/DigitalLibrary/tests/TestDigitalLibraryApiClient.py b/plugins/DigitalLibrary/tests/TestDigitalLibraryApiClient.py index ba0a0b15b4..9751838ddf 100644 --- a/plugins/DigitalLibrary/tests/TestDigitalLibraryApiClient.py +++ b/plugins/DigitalLibrary/tests/TestDigitalLibraryApiClient.py @@ -1,3 +1,6 @@ +# Copyright (c) 2021 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from unittest.mock import MagicMock import pytest @@ -37,7 +40,7 @@ def test_getProjectsFirstPage(api_client): failed_callback = MagicMock() # Call - api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback) + api_client.getProjectsFirstPage(search_filter = "filter", on_finished = finished_callback, failed = failed_callback) # Asserts pagination_manager.reset.assert_called_once() # Should be called since we asked for new set of projects @@ -45,16 +48,16 @@ def test_getProjectsFirstPage(api_client): args = http_manager.get.call_args_list[0] # Ensure that it's called with the right limit - assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20" + assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20&search=filter" # Change the limit & try again http_manager.get.reset_mock() pagination_manager.limit = 80 - api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback) + api_client.getProjectsFirstPage(search_filter = "filter", on_finished = finished_callback, failed = failed_callback) args = http_manager.get.call_args_list[0] # Ensure that it's called with the right limit - assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80" + assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80&search=filter" def test_getMoreProjects_noNewProjects(api_client): diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py index ab2bcaad5b..e80acc8d94 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py @@ -1,4 +1,4 @@ -# Copyright (c) 2020 Ultimaker B.V. +# Copyright (c) 2021 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.Logger import Logger @@ -103,20 +103,27 @@ class PerObjectSettingsTool(Tool): new_instance.resetState() # Ensure that the state is not seen as a user state. settings.addInstance(new_instance) - for property_key in ["top_bottom_thickness", "wall_thickness", "wall_line_count"]: + # Override some settings to ensure that the infill mesh by default adds no skin or walls. Or remove them if not an infill mesh. + specialized_settings = { + "top_bottom_thickness": 0, + "top_thickness": "=top_bottom_thickness", + "bottom_thickness": "=top_bottom_thickness", + "top_layers": "=0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))", + "bottom_layers": "=0 if infill_sparse_density == 100 else math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))", + "wall_thickness": 0, + "wall_line_count": "=max(1, round((wall_thickness - wall_line_width_0) / wall_line_width_x) + 1) if wall_thickness != 0 else 0" + } + for property_key in specialized_settings: if mesh_type == "infill_mesh": if settings.getInstance(property_key) is None: definition = stack.getSettingDefinition(property_key) new_instance = SettingInstance(definition, settings) - # We just want the wall_line count to be there in case it was overriden in the global stack. - # as such, we don't need to set a value. - if property_key != "wall_line_count": - new_instance.setProperty("value", 0) + new_instance.setProperty("value", specialized_settings[property_key]) new_instance.resetState() # Ensure that the state is not seen as a user state. settings.addInstance(new_instance) settings_visibility_changed = True - elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and (settings.getProperty(property_key, "value") == 0 or property_key == "wall_line_count"): + elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and property_key in specialized_settings: settings.removeInstance(property_key) settings_visibility_changed = True diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml index 2d3814309e..cf7665bda6 100644 --- a/plugins/PrepareStage/PrepareMenu.qml +++ b/plugins/PrepareStage/PrepareMenu.qml @@ -1,7 +1,7 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2021 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.7 +import QtQuick 2.9 import QtQuick.Layouts 1.1 import QtQuick.Controls 2.3 @@ -13,6 +13,8 @@ Item { id: prepareMenu + property var fileProviderModel: CuraApplication.getFileProviderModel() + UM.I18nCatalog { id: catalog @@ -36,9 +38,9 @@ Item { id: itemRow - anchors.left: openFileButton.right + anchors.left: parent.left anchors.right: parent.right - anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.leftMargin: UM.Theme.getSize("default_margin").width + openFileButton.width + openFileMenu.width property int machineSelectorWidth: Math.round((width - printSetupSelectorItem.width) / 3) height: parent.height @@ -74,22 +76,116 @@ Item } } + //Pop-up shown when there are multiple items to select from. + Cura.ExpandablePopup + { + id: openFileMenu + visible: prepareMenu.fileProviderModel.count > 1 + + contentAlignment: Cura.ExpandablePopup.ContentAlignment.AlignLeft + headerCornerSide: Cura.RoundedRectangle.Direction.All + headerPadding: Math.round((parent.height - UM.Theme.getSize("button_icon").height) / 2) + contentPadding: UM.Theme.getSize("default_lining").width + enabled: visible + + height: parent.height + width: visible ? (headerPadding * 3 + UM.Theme.getSize("button_icon").height + iconSize) : 0 + + headerItem: UM.RecolorImage + { + id: menuIcon + source: UM.Theme.getIcon("Folder", "medium") + color: UM.Theme.getColor("icon") + + sourceSize.height: height + } + + contentItem: Item + { + id: popup + + Column + { + id: openProviderColumn + + //The column doesn't automatically listen to its children rect if the children change internally, so we need to explicitly update the size. + onChildrenRectChanged: + { + popup.height = childrenRect.height + popup.width = childrenRect.width + } + onPositioningComplete: + { + popup.height = childrenRect.height + popup.width = childrenRect.width + } + + Repeater + { + model: prepareMenu.fileProviderModel + delegate: Button + { + leftPadding: UM.Theme.getSize("default_margin").width + rightPadding: UM.Theme.getSize("default_margin").width + width: contentItem.width + leftPadding + rightPadding + height: UM.Theme.getSize("action_button").height + hoverEnabled: true + + contentItem: Label + { + text: model.displayText + color: UM.Theme.getColor("text") + font: UM.Theme.getFont("medium") + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + + width: contentWidth + height: parent.height + } + + onClicked: + { + if(model.index == 0) //The 0th element is the "From Disk" option, which should activate the open local file dialog. + { + Cura.Actions.open.trigger(); + } + else + { + prepareMenu.fileProviderModel.trigger(model.name); + } + } + + background: Rectangle + { + color: parent.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent" + radius: UM.Theme.getSize("action_button_radius").width + width: popup.width + } + } + } + } + } + } + + //If there is just a single item, show a button instead that directly chooses the one option. Button { id: openFileButton - height: UM.Theme.getSize("stage_menu").height - width: UM.Theme.getSize("stage_menu").height + visible: prepareMenu.fileProviderModel.count <= 1 + + height: parent.height + width: visible ? height : 0 //Square button (and don't take up space if invisible). onClicked: Cura.Actions.open.trigger() + enabled: visible && prepareMenu.fileProviderModel.count > 0 hoverEnabled: true contentItem: Item { - anchors.fill: parent UM.RecolorImage { id: buttonIcon - anchors.centerIn: parent source: UM.Theme.getIcon("Folder", "medium") + anchors.centerIn: parent width: UM.Theme.getSize("button_icon").width height: UM.Theme.getSize("button_icon").height color: UM.Theme.getColor("icon") @@ -101,8 +197,8 @@ Item background: Rectangle { id: background - height: UM.Theme.getSize("stage_menu").height - width: UM.Theme.getSize("stage_menu").height + height: parent.height + width: parent.width border.color: UM.Theme.getColor("lining") border.width: UM.Theme.getSize("default_lining").width diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml index 86e686e0fc..6dde44c8ae 100644 --- a/plugins/SimulationView/SimulationViewMenuComponent.qml +++ b/plugins/SimulationView/SimulationViewMenuComponent.qml @@ -203,16 +203,16 @@ Cura.ExpandableComponent style: UM.Theme.styles.checkbox - - UM.RecolorImage + Rectangle { id: swatch anchors.verticalCenter: parent.verticalCenter anchors.right: extrudersModelCheckBox.right width: UM.Theme.getSize("layerview_legend_size").width height: UM.Theme.getSize("layerview_legend_size").height - source: UM.Theme.getIcon("Extruder", "medium") color: model.color + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("lining") } Label diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 1884efec46..9eaa133ef5 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -256,7 +256,16 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): """ self._uploaded_print_job = self._pre_upload_print_job self._progress.hide() - PrintJobUploadSuccessMessage().show() + message = PrintJobUploadSuccessMessage() + message.addAction("monitor print", + name=I18N_CATALOG.i18nc("@action:button", "Monitor print"), + icon="", + description=I18N_CATALOG.i18nc("@action:tooltip", "Track the print in Ultimaker Digital Factory"), + button_align=message.ActionButtonAlignment.ALIGN_RIGHT) + df_url = f"https://digitalfactory.ultimaker.com/app/jobs/{self._cluster.cluster_id}?utm_source=cura&utm_medium=software&utm_campaign=monitor-button" + message.pyQtActionTriggered.connect(lambda message, action: (QDesktopServices.openUrl(QUrl(df_url)), message.hide())) + + message.show() self.writeFinished.emit() def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"): diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py index aa64f338dd..aa3d72ccd8 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py @@ -13,6 +13,5 @@ class PrintJobUploadSuccessMessage(Message): def __init__(self) -> None: super().__init__( text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), - title = I18N_CATALOG.i18nc("@info:title", "Data Sent"), - lifetime = 5 + title = I18N_CATALOG.i18nc("@info:title", "Data Sent") ) diff --git a/resources/definitions/anycubic_i3_mega_s.def.json b/resources/definitions/anycubic_i3_mega_s.def.json index f0c786a393..a0e83627c4 100644 --- a/resources/definitions/anycubic_i3_mega_s.def.json +++ b/resources/definitions/anycubic_i3_mega_s.def.json @@ -115,6 +115,7 @@ "skirt_gap": { "value": 5.0 }, "skirt_line_count": { "value": 4 }, + "meshfix_maximum_deviation": { "value": 0.05 }, "support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 / layer_height)))" }, "support_pattern": { "value": "'zigzag'" }, diff --git a/resources/definitions/anycubic_kossel_linear_plus.def.json b/resources/definitions/anycubic_kossel_linear_plus.def.json index a4aeac75a0..9683cf61cf 100644 --- a/resources/definitions/anycubic_kossel_linear_plus.def.json +++ b/resources/definitions/anycubic_kossel_linear_plus.def.json @@ -5,6 +5,7 @@ "metadata": { "visible": true, "platform": "kossel_pro_build_platform.3mf", + "platform_offset": [0, -0.25, 0], "machine_extruder_trains": { "0": "anycubic_kossel_extruder_0" } diff --git a/resources/definitions/atom2.def.json b/resources/definitions/atom2.def.json new file mode 100644 index 0000000000..d7a26546d8 --- /dev/null +++ b/resources/definitions/atom2.def.json @@ -0,0 +1,35 @@ +{ + "name": "Atom 2", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Victor (Yu Chieh) Lin", + "manufacturer": "Layer One", + "file_formats": "text/x-gcode", + "platform_offset": [0,0,0], + "machine_extruder_trains": { "0": "atom2_extruder_0" + } + }, + + "overrides": { + "machine_name": { "default_value": "Atom 2" }, + "machine_shape": { "default_value": "elliptic" }, + "machine_width": { "default_value": 210 }, + "machine_depth": { "default_value": 210 }, + "machine_height": { "default_value": 320 }, + "machine_extruder_count": { "default_value": 1 }, + "machine_heated_bed": { "default_value": false }, + "machine_center_is_zero": { "default_value": true }, + + "machine_start_gcode": { "default_value": "G21\nG90 \nM107\nG28\nG92 E0\nG1 F200 E3\nG92 E0" }, + "machine_end_gcode": { "default_value": "M104 S0\nG28\nG91\nG1 E-6 F300\nM84\nG90" }, + + "layer_height": { "default_value": 0.2 }, + "default_material_print_temperature": { "default_value": 210 }, + "speed_print": { "default_value": 32 }, + "optimize_wall_printing_order": { "value": "True" }, + "infill_sparse_density": { "default_value": 10 }, + "brim_width": { "default_value": 4 } + } +} diff --git a/resources/definitions/biqu_base.def.json b/resources/definitions/biqu_base.def.json index af5c2465e2..29aa69ec54 100755 --- a/resources/definitions/biqu_base.def.json +++ b/resources/definitions/biqu_base.def.json @@ -85,8 +85,8 @@ "material_flow": { "value": 100 }, "travel_compensate_overlapping_walls_0_enabled": { "value": "False" }, - "z_seam_type": { "value": "'back'" }, - "z_seam_corner": { "value": "'z_seam_corner_weighted'" }, + "z_seam_type": { "value": "'sharpest_corner'" }, + "z_seam_corner": { "value": "'z_seam_corner_inner'" }, "infill_line_width": { "value": "line_width * 1.2" }, "infill_sparse_density": { "value": "20" }, @@ -156,7 +156,7 @@ "support_interface_enable": { "value": true }, "support_interface_height": { "value": "layer_height * 4" }, - "support_interface_density": { "value": 33.333 }, + "support_interface_density": { "value": 75 }, "support_interface_pattern": { "value": "'grid'" }, "support_interface_skip_height": { "value": 0.2 }, "minimum_support_area": { "value": 2 }, diff --git a/resources/definitions/biqu_bx_abl.def.json b/resources/definitions/biqu_bx_abl.def.json new file mode 100755 index 0000000000..af4f50dec0 --- /dev/null +++ b/resources/definitions/biqu_bx_abl.def.json @@ -0,0 +1,51 @@ +{ + "name": "Biqu BX", + "version": 2, + "inherits": "biqu_base", + "metadata": { + "quality_definition": "biqu_base", + "visible": true, + "has_machine_materials": true, + "platform": "BIQU_BX_PLATE.stl", + "platform_offset": [ + 7, + -7.4, + -3 + ] + }, + "overrides": { + "coasting_enable": { "value": false }, + "retraction_amount": { "value": 1 }, + "retraction_speed": { "value": 40 }, + "retraction_extrusion_window": { "value": 1 }, + "retract_at_layer_change": { "value": true }, + "support_enable": { "value": false }, + "support_structure": { "value": "'normal'" }, + "support_type": { "value": "'buildplate'" }, + "support_angle": { "value": 45 }, + "support_infill_rate": { "value": 15 }, + "infill_overlap": { "value": 15.0 }, + "skin_overlap": { "value": 20.0 }, + "fill_outline_gaps": { "value": true }, + "filter_out_tiny_gaps": { "value": true }, + "roofing_layer_count": { "value": 2 }, + "xy_offset_layer_0": { "value": -0.1 }, + "speed_print": { "value": 50 }, + "machine_name": { "default_value": "Biqu BX" }, + "machine_width": { "value": 250 }, + "machine_depth": { "value": 250 }, + "machine_height": { "value": 250 }, + "machine_head_with_fans_polygon": { "default_value": [ + [-33, 35], + [-33, -23], + [33, -23], + [33, 35] + ] + }, + "machine_start_gcode": { + "default_value": "; BIQU BX Start G-code\r\n; For inforation on how to tune this profile and get the\r\n; most out of your BX visit: https:\/\/github.com\/looxonline\/Marlin\r\n; For the official github site visit: https:\/\/github.com\/bigtreetech\/BIQU-BX\r\n\r\nM117 Initial homing sequence. ; Home so that the probe is positioned to heat\r\nG28\r\n\r\nM117 Probe heating position\r\nG0 X65 Y5 Z1 ; Move the probe to the heating position.\r\n\r\nM117 Getting the heaters up to temp!\r\nM104 S140 ; Set Extruder temperature, no wait\r\nM140 S60 ; Set Heat Bed temperature\r\nM190 S60 ; Wait for Heat Bed temperature\r\n\r\nM117 Waiting for probe to warm! ; Wait another 90s for the probe to absorb heat.\r\nG4 S90 \r\n\r\nM117 Post warming re-home\r\nG28 ; Home all axes again after warming\r\n\r\nM117 Z-Dance of my people\r\nG34\r\n\r\nM117 ABL Probing\r\nG29\r\n\r\nM900 K0 L0 T0 ;Edit the K and L values if you have calibrated a k factor for your filament\r\nM900 T0 S0\r\n\r\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\r\nG1 X4.1 Y10 Z0.3 F5000.0 ; Move to start position\r\n\r\nM117 Getting the extruder up to temp\r\nM140 S{material_bed_temperature_layer_0} ; Set Heat Bed temperature\r\nM104 S{material_print_temperature_layer_0} ; Set Extruder temperature\r\nM109 S{material_print_temperature_layer_0} ; Wait for Extruder temperature\r\nM190 S{material_bed_temperature_layer_0} ; Wait for Heat Bed temperature\r\n\r\nG92 E0 ; Reset Extruder\r\nM117 Purging\r\nG1 X4.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\r\nG1 X4.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\r\nG1 X4.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\r\nG92 E0 ; Reset Extruder\r\nM117 Lets make\r\nG1 X8 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish" + }, + + "gantry_height": { "value": 27.5 } + } +} diff --git a/resources/definitions/flsun_sr.def.json b/resources/definitions/flsun_sr.def.json new file mode 100644 index 0000000000..9f68c129bd --- /dev/null +++ b/resources/definitions/flsun_sr.def.json @@ -0,0 +1,76 @@ +{ + "version": 2, + "name": "FlSun SuperRacer", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Thushan Fernando", + "manufacturer": "Flsun", + "platform": "flsun_sr.3mf", + "file_formats": "text/x-gcode", + "has_materials": true, + "has_machine_quality": false, + "preferred_quality_type": "fast", + "machine_extruder_trains": { + "0": "flsun_sr_extruder_0" + } + }, + "overrides": { + "machine_extruder_count": { + "default_value": 1 + }, + "retraction_enable": { + "default_value": true + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_width": { + "default_value": 260 + }, + "machine_depth": { + "default_value": 260 + }, + "machine_height": { + "default_value": 330 + }, + "machine_center_is_zero": { + "default_value": true + }, + "machine_head_with_fans_polygon": { + "default_value": [ + [0, 0], + [0, 0], + [0, 0], + [0, 0] + ] + }, + "z_seam_type": { + "value": "'back'" + }, + "gantry_height": { + "value": "0" + }, + "machine_shape": { + "default_value": "elliptic" + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, + "material_diameter": { + "default_value": 1.75 + }, + "infill_sparse_density": { + "default_value": 15 + }, + "machine_start_gcode": { + "default_value": "G21\nG90\nM82\nM107 T0\nM140 S{material_bed_temperature}\nM104 S{material_print_temperature} T0\nM190 S{material_bed_temperature}\nM109 S{material_print_temperature} T0\nG28\nG92 E0\n" + }, + "machine_end_gcode": { + "default_value": "M107 T0\nM104 S0\nM104 S0 T1\nM140 S0\nG92 E0\nG91\nG1 E-1 F300 \nG1 Z+0.5 E-5 F6000\nG28 X0 Y0\nG90 ;absolute positioning\n" + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + } + } +} diff --git a/resources/definitions/geeetech_a30.def.json b/resources/definitions/geeetech_a30.def.json index 1f08d37445..d4f7df139f 100644 --- a/resources/definitions/geeetech_a30.def.json +++ b/resources/definitions/geeetech_a30.def.json @@ -4,7 +4,7 @@ "inherits": "fdmprinter", "metadata": { "author": "William & Cataldo URSO", - "manufacturer": "Shenzhen Geeetech Technology", + "manufacturer": "Geeetech", "file_formats": "text/x-gcode", "visible": true, "has_materials": true, diff --git a/resources/definitions/mp_mini_delta.def.json b/resources/definitions/mp_mini_delta.def.json index bbdca2c8f8..eda45bb413 100644 --- a/resources/definitions/mp_mini_delta.def.json +++ b/resources/definitions/mp_mini_delta.def.json @@ -3,7 +3,7 @@ "name": "MP Mini Delta", "inherits": "fdmprinter", "metadata": { - "author": "MPMD Facebook Group", + "author": "MPMD V1 Facebook Group", "manufacturer": "Monoprice", "file_formats": "text/x-gcode", "platform": "mp_mini_delta_platform.3mf", @@ -25,7 +25,7 @@ "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 " + "default_value": ";MPMD V1 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": { @@ -47,9 +47,9 @@ "default_value": 0.21 }, "material_bed_temperature": { "value": 40 }, - "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" }, + "line_width": { "value": "round(machine_nozzle_size, 2)" }, + "material_print_temperature_layer_0": { "value": "material_print_temperature" }, + "material_bed_temperature_layer_0": { "value": "material_bed_temperature" }, "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, "machine_max_feedrate_x": { "default_value": 150 }, "machine_max_feedrate_y": { "default_value": 150 }, diff --git a/resources/definitions/mp_mini_delta_v2.def.json b/resources/definitions/mp_mini_delta_v2.def.json new file mode 100644 index 0000000000..30c7cb2823 --- /dev/null +++ b/resources/definitions/mp_mini_delta_v2.def.json @@ -0,0 +1,51 @@ +{ + "version": 2, + "name": "MP Mini Delta V2", + "inherits": "fdmprinter", + "metadata": { + "author": "mpminidelta subreddit", + "manufacturer": "Monoprice", + "file_formats": "text/x-gcode", + "platform": "mp_mini_delta_platform.3mf", + "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_v2_extruder_0" + } + }, + + "overrides": { + "machine_start_gcode": + { + "default_value": ";(**** start.gcode for MP Mini Delta V2****)\nG21\nG90\nM82\nM107\nM104 S170\nG28 X0 Y0\nG28 Z0\nG29 Z0.4\nG1 Z15 F300\nM109 S{material_print_temperature_layer_0}\nG92 E0\nG1 F200 E3\nG92 E0\nG1 F2000\n" + }, + "machine_end_gcode": { + "default_value": ";(**** end.gcode for MP Mini Delta V2****)\nG28;(Stick out the part)\nM190 S0;(Turn off heat bed, don't wait.)\nG92 E10;(Set extruder to 10)\nG1 E7 F200;(retract 3mm)\nM104 S0;(Turn off nozzle, don't wait)\nG4 S300;(Delay 5 minutes)\nM107;(Turn off part fan)\nM84;(Turn off stepper motors.)" + }, + "material_print_temp_prepend":{"default_value":false}, + "material_bed_temperature": { "value": 40 }, + "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 + }, + "line_width": { "value": "round(machine_nozzle_size, 2)" }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "retraction_amount": { "default_value": 5 }, + "retraction_speed": { "default_value": 28 }, + "retraction_hop_enabled": { "default_value": false }, + "retract_at_layer_change": { "default_value": true } + } +} diff --git a/resources/definitions/ultimaker2_plus_connect.def.json b/resources/definitions/ultimaker2_plus_connect.def.json index c0ddcf813f..46c615a262 100644 --- a/resources/definitions/ultimaker2_plus_connect.def.json +++ b/resources/definitions/ultimaker2_plus_connect.def.json @@ -22,7 +22,8 @@ "0": "ultimaker2_plus_connect_extruder_0" }, "supports_usb_connection": false, - "supports_network_connection": true + "supports_network_connection": true, + "supports_material_export": true }, "overrides": { diff --git a/resources/definitions/ultimaker_s3.def.json b/resources/definitions/ultimaker_s3.def.json index 962bff3fa0..43f32a96dc 100644 --- a/resources/definitions/ultimaker_s3.def.json +++ b/resources/definitions/ultimaker_s3.def.json @@ -27,6 +27,7 @@ "first_start_actions": [ "DiscoverUM3Action" ], "supported_actions": [ "DiscoverUM3Action" ], "supports_usb_connection": false, + "supports_material_export": true, "weight": -1, "firmware_update_info": { "id": 213482, diff --git a/resources/definitions/ultimaker_s5.def.json b/resources/definitions/ultimaker_s5.def.json index 8a9880c31a..71de826953 100644 --- a/resources/definitions/ultimaker_s5.def.json +++ b/resources/definitions/ultimaker_s5.def.json @@ -28,6 +28,7 @@ "supported_actions": [ "DiscoverUM3Action" ], "supports_usb_connection": false, "supports_network_connection": true, + "supports_material_export": true, "weight": -2, "firmware_update_info": { "id": 9051, diff --git a/resources/extruders/atom2_extruder_0.def.json b/resources/extruders/atom2_extruder_0.def.json new file mode 100644 index 0000000000..be9d5782ff --- /dev/null +++ b/resources/extruders/atom2_extruder_0.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "atom2", + "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/flsun_sr_extruder_0.def.json b/resources/extruders/flsun_sr_extruder_0.def.json new file mode 100644 index 0000000000..31880b5130 --- /dev/null +++ b/resources/extruders/flsun_sr_extruder_0.def.json @@ -0,0 +1,20 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "flsun_sr", + "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/mp_mini_delta_v2_extruder_0.def.json b/resources/extruders/mp_mini_delta_v2_extruder_0.def.json new file mode 100644 index 0000000000..022ace632f --- /dev/null +++ b/resources/extruders/mp_mini_delta_v2_extruder_0.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 0", + "inherits": "fdmextruder", + "metadata": { + "machine": "mp_mini_delta_v2", + "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/i18n/de_DE/fdmprinter.def.json.po b/resources/i18n/de_DE/fdmprinter.def.json.po index 5d7915ac0d..3eb590b680 100644 --- a/resources/i18n/de_DE/fdmprinter.def.json.po +++ b/resources/i18n/de_DE/fdmprinter.def.json.po @@ -1177,7 +1177,7 @@ msgstr "Hinten links" #: fdmprinter.def.json msgctxt "z_seam_position option back" msgid "Back" -msgstr "Zurück" +msgstr "Hinten" #: fdmprinter.def.json msgctxt "z_seam_position option backright" diff --git a/resources/meshes/BIQU_BX_PLATE.stl b/resources/meshes/BIQU_BX_PLATE.stl new file mode 100644 index 0000000000..9c1726f276 Binary files /dev/null and b/resources/meshes/BIQU_BX_PLATE.stl differ diff --git a/resources/meshes/flsun_sr.3mf b/resources/meshes/flsun_sr.3mf new file mode 100644 index 0000000000..7eba390a66 Binary files /dev/null and b/resources/meshes/flsun_sr.3mf differ diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index 31d5c35d2c..582df3d87c 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -18,6 +18,7 @@ Button property alias textFont: buttonText.font property alias cornerRadius: backgroundRect.radius property alias tooltip: tooltip.tooltipText + property alias tooltipWidth: tooltip.width property color color: UM.Theme.getColor("primary") property color hoverColor: UM.Theme.getColor("primary_hover") diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 7a4a837c66..95c6778b87 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2021 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. pragma Singleton @@ -122,7 +122,15 @@ Item Action { id: quitAction - text: catalog.i18nc("@action:inmenu menubar:file","&Quit") + + //On MacOS, don't translate the "Quit" word. + //Qt moves the "quit" entry to a different place, and if it got renamed can't find it again when it attempts to + //delete the item upon closing the application, causing a crash. + //In the new location, these items are translated automatically according to the system's language. + //For more information, see: + //- https://doc.qt.io/qt-5/macos-issues.html#menu-bar + //- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar + text: (Qt.platform.os == "osx") ? "&Quit" : catalog.i18nc("@action:inmenu menubar:file", "&Quit") iconName: "application-exit" shortcut: StandardKey.Quit } @@ -172,7 +180,14 @@ Item Action { id: preferencesAction - text: catalog.i18nc("@action:inmenu", "Configure Cura...") + //On MacOS, don't translate the "Configure" word. + //Qt moves the "configure" entry to a different place, and if it got renamed can't find it again when it + //attempts to delete the item upon closing the application, causing a crash. + //In the new location, these items are translated automatically according to the system's language. + //For more information, see: + //- https://doc.qt.io/qt-5/macos-issues.html#menu-bar + //- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar + text: (Qt.platform.os == "osx") ? "Configure Cura..." : catalog.i18nc("@action:inmenu", "Configure Cura...") iconName: "configure" } @@ -263,7 +278,15 @@ Item Action { id: aboutAction; - text: catalog.i18nc("@action:inmenu menubar:help", "About..."); + + //On MacOS, don't translate the "About" word. + //Qt moves the "about" entry to a different place, and if it got renamed can't find it again when it + //attempts to delete the item upon closing the application, causing a crash. + //In the new location, these items are translated automatically according to the system's language. + //For more information, see: + //- https://doc.qt.io/qt-5/macos-issues.html#menu-bar + //- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar + text: (Qt.platform.os == "osx") ? "About..." : catalog.i18nc("@action:inmenu menubar:help", "About..."); iconName: "help-about"; } diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 191ae712d4..7782ace3af 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -417,6 +417,7 @@ UM.MainWindow Cura.PrimaryButton { text: model.name + iconSource: UM.Theme.getIcon(model.icon) height: UM.Theme.getSize("message_action_button").height } } @@ -426,6 +427,7 @@ UM.MainWindow Cura.SecondaryButton { text: model.name + iconSource: UM.Theme.getIcon(model.icon) height: UM.Theme.getSize("message_action_button").height } } @@ -434,6 +436,14 @@ UM.MainWindow Cura.TertiaryButton { text: model.name + iconSource: + { + if (model.icon == null || model.icon == "") + { + return UM.Theme.getIcon("LinkExternal") + } + return UM.Theme.getIcon(model.icon) + } height: UM.Theme.getSize("message_action_button").height } } diff --git a/resources/qml/ExpandableComponent.qml b/resources/qml/ExpandableComponent.qml index bbe617e27d..18eb8c0fa6 100644 --- a/resources/qml/ExpandableComponent.qml +++ b/resources/qml/ExpandableComponent.qml @@ -167,7 +167,7 @@ Item verticalCenter: parent.verticalCenter margins: background.padding } - source: expanded ? UM.Theme.getIcon("ChevronSingleDown") : UM.Theme.getIcon("ChevronSingleLeft") + source: UM.Theme.getIcon("ChevronSingleDown") visible: source != "" width: UM.Theme.getSize("standard_arrow").width height: UM.Theme.getSize("standard_arrow").height diff --git a/resources/qml/ExpandablePopup.qml b/resources/qml/ExpandablePopup.qml index da56470bfb..3bcfdbb6f8 100644 --- a/resources/qml/ExpandablePopup.qml +++ b/resources/qml/ExpandablePopup.qml @@ -180,7 +180,7 @@ Item verticalCenter: parent.verticalCenter margins: background.padding } - source: expanded ? UM.Theme.getIcon("ChevronSingleDown") : UM.Theme.getIcon("ChevronSingleLeft") + source: UM.Theme.getIcon("ChevronSingleDown") visible: source != "" width: UM.Theme.getSize("standard_arrow").width height: UM.Theme.getSize("standard_arrow").height diff --git a/resources/qml/MainWindow/ApplicationMenu.qml b/resources/qml/MainWindow/ApplicationMenu.qml index 95cea77248..62b3a71ee8 100644 --- a/resources/qml/MainWindow/ApplicationMenu.qml +++ b/resources/qml/MainWindow/ApplicationMenu.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2018 Ultimaker B.V. +// Copyright (c) 2021 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.7 @@ -48,7 +48,17 @@ Item ViewMenu { title: catalog.i18nc("@title:menu menubar:toplevel", "&View") } - SettingsMenu { title: catalog.i18nc("@title:menu menubar:toplevel", "&Settings") } + SettingsMenu + { + //On MacOS, don't translate the "Settings" word. + //Qt moves the "settings" entry to a different place, and if it got renamed can't find it again when it + //attempts to delete the item upon closing the application, causing a crash. + //In the new location, these items are translated automatically according to the system's language. + //For more information, see: + //- https://doc.qt.io/qt-5/macos-issues.html#menu-bar + //- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar + title: (Qt.platform.os == "osx") ? "&Settings" : catalog.i18nc("@title:menu menubar:toplevel", "&Settings") + } Menu { @@ -91,7 +101,15 @@ Item Menu { id: preferencesMenu - title: catalog.i18nc("@title:menu menubar:toplevel", "P&references") + + //On MacOS, don't translate the "Preferences" word. + //Qt moves the "preferences" entry to a different place, and if it got renamed can't find it again when it + //attempts to delete the item upon closing the application, causing a crash. + //In the new location, these items are translated automatically according to the system's language. + //For more information, see: + //- https://doc.qt.io/qt-5/macos-issues.html#menu-bar + //- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar + title: (Qt.platform.os == "osx") ? "&Preferences" : catalog.i18nc("@title:menu menubar:toplevel", "P&references") MenuItem { action: Cura.Actions.preferences } } diff --git a/resources/qml/Preferences/Materials/MaterialsPage.qml b/resources/qml/Preferences/Materials/MaterialsPage.qml index 791d6685de..4de3ad918b 100644 --- a/resources/qml/Preferences/Materials/MaterialsPage.qml +++ b/resources/qml/Preferences/Materials/MaterialsPage.qml @@ -1,5 +1,5 @@ -// Copyright (c) 2018 Ultimaker B.V. -// Uranium is released under the terms of the LGPLv3 or higher. +// Copyright (c) 2021 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.7 import QtQuick.Controls 1.4 @@ -191,6 +191,21 @@ Item } enabled: base.hasCurrentItem } + + //Sync button. + Button + { + id: syncMaterialsButton + text: catalog.i18nc("@action:button Sending materials to printers", "Sync with Printers") + iconName: "sync-synchronizing" + onClicked: + { + forceActiveFocus(); + exportAllMaterialsDialog.folder = base.materialManagementModel.getPreferredExportAllPath(); + exportAllMaterialsDialog.open(); + } + visible: Cura.MachineManager.activeMachine.supportsMaterialExport + } } Item { @@ -368,6 +383,19 @@ Item } } + FileDialog + { + id: exportAllMaterialsDialog + title: catalog.i18nc("@title:window", "Export All Materials") + selectExisting: false + nameFilters: ["Material archives (*.umm)", "All files (*)"] + onAccepted: + { + base.materialManagementModel.exportAll(fileUrl); + CuraApplication.setDefaultPath("dialog_material_path", folder); + } + } + MessageDialog { id: messageDialog diff --git a/resources/qml/Preferences/SettingVisibilityPage.qml b/resources/qml/Preferences/SettingVisibilityPage.qml index 37149c0009..d2fd5c7e94 100644 --- a/resources/qml/Preferences/SettingVisibilityPage.qml +++ b/resources/qml/Preferences/SettingVisibilityPage.qml @@ -95,7 +95,7 @@ UM.PreferencesPage placeholderText: catalog.i18nc("@label:textbox", "Filter...") - onTextChanged: definitionsModel.filter = {"i18n_label": "*" + text} + onTextChanged: definitionsModel.filter = {"i18n_label|i18n_description": "*" + text} } NewControls.ComboBox diff --git a/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml b/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml index 0ea20f052e..edf6fe5974 100644 --- a/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml +++ b/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml @@ -54,7 +54,7 @@ Item { id: networkPrinterScrollView - maxItemCountAtOnce: 10 // show at max 10 items at once, otherwise you need to scroll. + maxItemCountAtOnce: 9 // show at max 9 items at once, otherwise you need to scroll. onRefreshButtonClicked: { diff --git a/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml b/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml index 5334a15974..58cadbec37 100644 --- a/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml +++ b/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2019 Ultimaker B.V. +// Copyright (c) 2021 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 @@ -214,16 +214,16 @@ Item id: troubleshootingButton anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.rightMargin: UM.Theme.getSize("thin_margin").width anchors.verticalCenter: parent.verticalCenter height: troubleshootingLinkIcon.height - width: troubleshootingLinkIcon.width + troubleshootingLabel.width + UM.Theme.getSize("default_margin").width + width: troubleshootingLinkIcon.width + troubleshootingLabel.width + UM.Theme.getSize("thin_margin").width UM.RecolorImage { id: troubleshootingLinkIcon anchors.right: troubleshootingLabel.left - anchors.rightMargin: UM.Theme.getSize("default_margin").width + anchors.rightMargin: UM.Theme.getSize("thin_margin").width anchors.verticalCenter: parent.verticalCenter height: troubleshootingLabel.height width: height diff --git a/resources/qml/Widgets/TextField.qml b/resources/qml/Widgets/TextField.qml index 28074d4415..c126c8a6e0 100644 --- a/resources/qml/Widgets/TextField.qml +++ b/resources/qml/Widgets/TextField.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2019 Ultimaker B.V. +// Copyright (c) 2021 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.10 @@ -15,6 +15,8 @@ TextField { id: textField + property alias leftIcon: iconLeft.source + UM.I18nCatalog { id: catalog; name: "cura" } hoverEnabled: true @@ -22,6 +24,7 @@ TextField font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") renderType: Text.NativeRendering + leftPadding: iconLeft.visible ? iconLeft.width + UM.Theme.getSize("default_margin").width * 2 : UM.Theme.getSize("thin_margin").width states: [ State @@ -52,7 +55,6 @@ TextField color: UM.Theme.getColor("main_background") - anchors.margins: Math.round(UM.Theme.getSize("default_lining").width) radius: UM.Theme.getSize("setting_control_radius").width border.color: @@ -67,5 +69,23 @@ TextField } return UM.Theme.getColor("setting_control_border") } + + //Optional icon added on the left hand side. + UM.RecolorImage + { + id: iconLeft + + anchors + { + verticalCenter: parent.verticalCenter + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + } + + visible: source != "" + height: UM.Theme.getSize("small_button_icon").height + width: visible ? height : 0 + color: textField.color + } } } diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 0d6fa2f260..457d2092c1 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -16,7 +16,7 @@ "primary_text": [255, 255, 255, 204], "secondary": [95, 95, 95, 255], - "secondary_button": [0, 0, 0, 0], + "secondary_button": [39, 44, 48, 255], "secondary_button_hover": [85, 85, 87, 255], "secondary_button_text": [255, 255, 255, 255], diff --git a/resources/themes/cura-light/icons/medium/ExtruderColor.svg b/resources/themes/cura-light/icons/medium/ExtruderColor.svg index 85360a9622..cd4452b246 100644 --- a/resources/themes/cura-light/icons/medium/ExtruderColor.svg +++ b/resources/themes/cura-light/icons/medium/ExtruderColor.svg @@ -6,8 +6,8 @@ .st0{fill:#231F20;} - + diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 93a40d645d..a2727ec12b 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -193,7 +193,7 @@ "primary_button_hover": [16, 70, 156, 255], "primary_button_text": [255, 255, 255, 255], - "secondary_button": [255, 255, 255, 0], + "secondary_button": [255, 255, 255, 255], "secondary_button_shadow": [216, 216, 216, 255], "secondary_button_hover": [232, 240, 253, 255], "secondary_button_text": [25, 110, 240, 255], diff --git a/resources/variants/biqu_bx_abl_0.2.inst.cfg b/resources/variants/biqu_bx_abl_0.2.inst.cfg new file mode 100755 index 0000000000..b72fdc83bb --- /dev/null +++ b/resources/variants/biqu_bx_abl_0.2.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = 0.2mm Nozzle +version = 4 +definition = biqu_bx_abl + +[metadata] +setting_version = 17 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.2 diff --git a/resources/variants/biqu_bx_abl_0.3.inst.cfg b/resources/variants/biqu_bx_abl_0.3.inst.cfg new file mode 100755 index 0000000000..7b195c0013 --- /dev/null +++ b/resources/variants/biqu_bx_abl_0.3.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = 0.3mm Nozzle +version = 4 +definition = biqu_bx_abl + +[metadata] +setting_version = 17 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.3 diff --git a/resources/variants/biqu_bx_abl_0.4.inst.cfg b/resources/variants/biqu_bx_abl_0.4.inst.cfg new file mode 100755 index 0000000000..106c5ea5d7 --- /dev/null +++ b/resources/variants/biqu_bx_abl_0.4.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = 0.4mm Nozzle +version = 4 +definition = biqu_bx_abl + +[metadata] +setting_version = 17 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/biqu_bx_abl_0.5.inst.cfg b/resources/variants/biqu_bx_abl_0.5.inst.cfg new file mode 100755 index 0000000000..c95388e23f --- /dev/null +++ b/resources/variants/biqu_bx_abl_0.5.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = 0.5mm Nozzle +version = 4 +definition = biqu_bx_abl + +[metadata] +setting_version = 17 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.5 diff --git a/resources/variants/biqu_bx_abl_0.6.inst.cfg b/resources/variants/biqu_bx_abl_0.6.inst.cfg new file mode 100755 index 0000000000..0116e7478f --- /dev/null +++ b/resources/variants/biqu_bx_abl_0.6.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = 0.6mm Nozzle +version = 4 +definition = biqu_bx_abl + +[metadata] +setting_version = 17 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.6 diff --git a/resources/variants/biqu_bx_abl_0.8.inst.cfg b/resources/variants/biqu_bx_abl_0.8.inst.cfg new file mode 100755 index 0000000000..8a5747a9f7 --- /dev/null +++ b/resources/variants/biqu_bx_abl_0.8.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = 0.8mm Nozzle +version = 4 +definition = biqu_bx_abl + +[metadata] +setting_version = 17 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8