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/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 272ad3a7e9..fa9a8c5474 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -610,15 +610,10 @@ class CuraEngineBackend(QObject, Backend): if error.getErrorCode() not in [Arcus.ErrorCode.BindFailedError, Arcus.ErrorCode.ConnectionResetError, Arcus.ErrorCode.Debug]: Logger.log("w", "A socket error caused the connection to be reset") - elif error.getErrorCode() == Arcus.ErrorCode.ConnectionResetError: - Logger.error("CuraEngine crashed abnormally! The socket connection was reset unexpectedly.") - self._slicing_error_message.show() - self.setState(BackendState.Error) - self.stopSlicing() # _terminate()' function sets the job status to 'cancel', after reconnecting to another Port the job status # needs to be updated. Otherwise backendState is "Unable To Slice" - elif error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None: + if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None: self._start_slice_job.setIsCancelled(False) # Check if there's any slicable object in the scene. diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml index d184264cde..8b919e299d 100644 --- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml +++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml @@ -20,7 +20,7 @@ Item width: parent.width height: parent.height - property alias createNewProjectButtonVisible: createNewProjectButton.visible + property bool createNewProjectButtonVisible: true anchors { @@ -61,6 +61,7 @@ Item id: createNewProjectButton text: "New Library project" + visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed) onClicked: { @@ -68,6 +69,20 @@ Item } 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") + } } Item diff --git a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py index 4ebb3cb051..a9a1ab360b 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryApiClient.py @@ -55,6 +55,7 @@ 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] @@ -69,6 +70,7 @@ class DigitalFactoryApiClient: 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) @@ -79,6 +81,41 @@ class DigitalFactoryApiClient: 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. @@ -329,7 +366,7 @@ class DigitalFactoryApiClient: :param on_error: The function to be called if anything goes wrong. """ - display_name = re.sub(r"[^a-zA-Z0-9- ./™®ö+']", " ", project_name) + display_name = re.sub(r"^[\\w\\-\\. ()]+\\.[a-zA-Z0-9]+$", " ", project_name) Logger.log("i", "Attempt to create new DF project '{}'.".format(display_name)) url = "{}/projects".format(self.CURA_API_ROOT) diff --git a/plugins/DigitalLibrary/src/DigitalFactoryController.py b/plugins/DigitalLibrary/src/DigitalFactoryController.py index 0821cc6925..cd0f0be638 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryController.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryController.py @@ -94,6 +94,9 @@ class DigitalFactoryController(QObject): """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) @@ -143,6 +146,7 @@ class DigitalFactoryController(QObject): 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() @@ -169,10 +173,8 @@ class DigitalFactoryController(QObject): :return: True if the user account has Digital Library access, else False """ - if self._account.userProfile: - subscriptions = self._account.userProfile.get("subscriptions", []) - if len(subscriptions) > 0: - return True + 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: @@ -556,6 +558,7 @@ class DigitalFactoryController(QObject): self._project_model.clearProjects() self.setSelectedProjectIndex(-1) 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() @@ -564,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 index 016306a478..192f58685a 100644 --- a/plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py +++ b/plugins/DigitalLibrary/src/DigitalFactoryFeatureBudgetResponse.py @@ -16,9 +16,9 @@ class DigitalFactoryFeatureBudgetResponse(BaseModel): 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] = False, # -1 means unlimited - library_max_private_projects: Optional[int] = False, # -1 means unlimited - library_max_team_shared_projects: Optional[int] = False, # -1 means unlimited + 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 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/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/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 } }