diff --git a/cura/CuraActions.py b/cura/CuraActions.py index e33ce8123d..835c46bba8 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -1,6 +1,5 @@ # Copyright (c) 2023 UltiMaker # Cura is released under the terms of the LGPLv3 or higher. - from typing import List, cast from PyQt6.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty @@ -33,7 +32,6 @@ from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOper from UM.Logger import Logger from UM.Scene.SceneNode import SceneNode - class CuraActions(QObject): def __init__(self, parent: QObject = None) -> None: super().__init__(parent) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index c32017371f..86bb53126c 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -601,9 +601,7 @@ class CuraApplication(QtApplication): preferences.addPreference("mesh/scale_to_fit", False) preferences.addPreference("mesh/scale_tiny_meshes", True) preferences.addPreference("cura/dialog_on_project_save", True) - preferences.addPreference("cura/dialog_on_ucp_project_save", True) preferences.addPreference("cura/asked_dialog_on_project_save", False) - preferences.addPreference("cura/asked_dialog_on_ucp_project_save", False) preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask") preferences.addPreference("cura/use_multi_build_plate", False) @@ -1979,6 +1977,17 @@ class CuraApplication(QtApplication): openProjectFile = pyqtSignal(QUrl, bool, arguments = ["project_file", "add_to_recent_files"]) # Emitted when a project file is about to open. + @pyqtSlot(QUrl, bool) + def readLocalUcpFile(self, file: QUrl, add_to_recent_files: bool = True): + + file_name = QUrl(file).toLocalFile() + workspace_reader = self.getWorkspaceFileHandler() + if workspace_reader is None: + Logger.warning(f"Workspace reader not found, cannot read file {file_name}.") + return + + workspace_reader.readLocalFile(file, add_to_recent_files) + @pyqtSlot(QUrl, str, bool) @pyqtSlot(QUrl, str) @pyqtSlot(QUrl) @@ -2184,6 +2193,12 @@ class CuraApplication(QtApplication): def addNonSliceableExtension(self, extension): self._non_sliceable_extensions.append(extension) + @pyqtSlot(str, result = bool) + def isProjectUcp(self, file_url) -> bool: + file_path = QUrl(file_url).toLocalFile() + workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path) + return workspace_reader.getIsProjectUcp() + @pyqtSlot(str, result=bool) def checkIsValidProjectFile(self, file_url): """Checks if the given file URL is a valid project file. """ @@ -2193,6 +2208,8 @@ class CuraApplication(QtApplication): if workspace_reader is None: return False # non-project files won't get a reader try: + if workspace_reader.getPluginId() == "3MFReader": + workspace_reader.clearOpenAsUcp() result = workspace_reader.preRead(file_path, show_dialog=False) return result == WorkspaceReader.PreReadResult.accepted except: diff --git a/cura/Machines/Models/MachineListModel.py b/cura/Machines/Models/MachineListModel.py index 69a3c53d03..cac52a0e65 100644 --- a/cura/Machines/Models/MachineListModel.py +++ b/cura/Machines/Models/MachineListModel.py @@ -5,7 +5,7 @@ # online cloud connected printers are represented within this ListModel. Additional information such as the number of # connected printers for each printer type is gathered. -from typing import Optional, List, cast +from typing import Optional, List, cast, Dict, Any from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal @@ -30,10 +30,10 @@ class MachineListModel(ListModel): ComponentTypeRole = Qt.ItemDataRole.UserRole + 8 IsNetworkedMachineRole = Qt.ItemDataRole.UserRole + 9 - def __init__(self, parent: Optional[QObject] = None, machines_filter: List[GlobalStack] = None, listenToChanges: bool = True) -> None: + def __init__(self, parent: Optional[QObject] = None, machines_filter: List[GlobalStack] = None, listenToChanges: bool = True, showCloudPrinters: bool = False) -> None: super().__init__(parent) - self._show_cloud_printers = False + self._show_cloud_printers = showCloudPrinters self._machines_filter = machines_filter self._catalog = i18nCatalog("cura") @@ -159,3 +159,8 @@ class MachineListModel(ListModel): "machineCount": machine_count, "catergory": "connected" if is_online else "other", }) + + def getItems(self) -> Dict[str, Any]: + if self.count > 0: + return self.items + return {} \ No newline at end of file diff --git a/plugins/3MFReader/SpecificSettingsModel.py b/plugins/3MFReader/SpecificSettingsModel.py index fd5719d6b3..ac8e7af3ef 100644 --- a/plugins/3MFReader/SpecificSettingsModel.py +++ b/plugins/3MFReader/SpecificSettingsModel.py @@ -3,6 +3,7 @@ from PyQt6.QtCore import Qt +from UM.Logger import Logger from UM.Settings.SettingDefinition import SettingDefinition from UM.Qt.ListModel import ListModel @@ -19,6 +20,8 @@ class SpecificSettingsModel(ListModel): self.addRoleName(self.ValueRole, "value") self._i18n_catalog = None + self._update() + def addSettingsFromStack(self, stack, category, settings): for setting, value in settings.items(): @@ -27,7 +30,7 @@ class SpecificSettingsModel(ListModel): setting_type = stack.getProperty(setting, "type") if setting_type is not None: # This is not very good looking, but will do for now - value = SettingDefinition.settingValueToString(setting_type, value) + " " + unit + value = str(SettingDefinition.settingValueToString(setting_type, value)) + " " + str(unit) else: value = str(value) @@ -36,3 +39,8 @@ class SpecificSettingsModel(ListModel): "label": stack.getProperty(setting, "label"), "value": value }) + + def _update(self): + Logger.debug(f"Updating {self.__class__.__name__}") + self.setItems([]) + return diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 25e2afa8bd..e6992611c1 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -117,6 +117,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._supported_extensions = [".3mf"] self._dialog = WorkspaceDialog() self._3mf_mesh_reader = None + self._is_ucp = None self._container_registry = ContainerRegistry.getInstance() # suffixes registered with the MimeTypes don't start with a dot '.' @@ -143,16 +144,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._old_new_materials: Dict[str, str] = {} self._machine_info = None - self._load_profile = False self._user_settings: Dict[str, Dict[str, Any]] = {} def _clearState(self): self._id_mapping = {} self._old_new_materials = {} self._machine_info = None - self._load_profile = False self._user_settings = {} + def clearOpenAsUcp(self): + self._is_ucp = None + def getNewId(self, old_id: str): """Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. @@ -207,6 +209,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return global_stack_file_list[0], extruder_stack_file_list + def _isProjectUcp(self, file_name) -> bool: + if self._is_ucp == None: + archive = zipfile.ZipFile(file_name, "r") + cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] + self._is_ucp =True if USER_SETTINGS_PATH in cura_file_names else False + + def getIsProjectUcp(self) -> bool: + return self._is_ucp + + def preRead(self, file_name, show_dialog=True, *args, **kwargs): """Read some info so we can make decisions @@ -215,7 +227,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): we don't want to show a dialog. """ self._clearState() - + self._isProjectUcp(file_name) self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) if self._3mf_mesh_reader and self._3mf_mesh_reader.preRead(file_name) == WorkspaceReader.PreReadResult.accepted: pass @@ -242,7 +254,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Read definition containers # machine_definition_id = None - updatable_machines = None if is_ucp else [] + updatable_machines = None if self._is_ucp else [] machine_definition_container_count = 0 extruder_definition_container_count = 0 definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] @@ -609,11 +621,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Load the user specifically exported settings self._dialog.exportedSettingModel.clear() - if is_ucp: + self._dialog.setCurrentMachineName("") + if self._is_ucp: try: self._user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8")) any_extruder_stack = ExtruderManager.getInstance().getExtruderStack(0) actual_global_stack = CuraApplication.getInstance().getGlobalContainerStack() + self._dialog.setCurrentMachineName(actual_global_stack.id) for stack_name, settings in self._user_settings.items(): if stack_name == 'global': @@ -658,15 +672,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setVariantType(variant_type_name) self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setMissingPackagesMetadata(missing_package_metadata) - self._dialog.setHasVisibleSelectSameProfileChanged(is_ucp) - self._dialog.setAllowCreatemachine(not is_ucp) + self._dialog.setAllowCreatemachine(not self._is_ucp) + self._dialog.setIsUcp(self._is_ucp) self._dialog.show() # Choosing the initially selected printer in MachineSelector is_networked_machine = False is_abstract_machine = False - if global_stack and isinstance(global_stack, GlobalStack): + if global_stack and isinstance(global_stack, GlobalStack) and not self._is_ucp: # The machine included in the project file exists locally already, no need to change selected printers. is_networked_machine = global_stack.hasNetworkedConnection() is_abstract_machine = parseBool(existing_global_stack.getMetaDataEntry("is_abstract_machine", False)) @@ -675,7 +689,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): elif self._dialog.updatableMachinesModel.count > 0: # The machine included in the project file does not exist. There is another machine of the same type. # This will always default to an abstract machine first. - machine = self._dialog.updatableMachinesModel.getItem(0) + machine = self._dialog.updatableMachinesModel.getItem(self._dialog.currentMachinePositionIndex) machine_name = machine["name"] is_networked_machine = machine["isNetworked"] is_abstract_machine = machine["isAbstractMachine"] @@ -693,7 +707,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._dialog.setIsAbstractMachine(is_abstract_machine) self._dialog.setMachineName(machine_name) self._dialog.updateCompatibleMachine() - self._dialog.setSelectSameProfileChecked(self._dialog.isCompatibleMachine) # Block until the dialog is closed. self._dialog.waitForClose() @@ -701,8 +714,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._dialog.getResult() == {}: return WorkspaceReader.PreReadResult.cancelled - self._load_profile = not is_ucp or (self._dialog.selectSameProfileChecked and self._dialog.isCompatibleMachine) - self._resolve_strategies = self._dialog.getResult() # # There can be 3 resolve strategies coming from the dialog: @@ -717,7 +728,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if key not in containers_found_dict or strategy is not None: continue self._resolve_strategies[key] = "override" if containers_found_dict[key] else "new" - return WorkspaceReader.PreReadResult.accepted @call_on_qt_thread @@ -825,7 +835,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for stack in extruder_stacks: stack.setNextStack(global_stack, connect_signals = False) - if self._load_profile: + if not self._is_ucp: Logger.log("d", "Workspace loading is checking definitions...") # Get all the definition files & check if they exist. If not, add them. definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] @@ -899,7 +909,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): QCoreApplication.processEvents() # Ensure that the GUI does not freeze. if global_stack: - if self._load_profile: + if not self._is_ucp: # Handle quality changes if any self._processQualityChanges(global_stack) @@ -907,7 +917,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._applyChangesToMachine(global_stack, extruder_stack_dict) else: # Just clear the settings now, so that we can change the active machine without conflicts - self._clearMachineSettings(global_stack, extruder_stack_dict) + self._clearMachineSettings(global_stack, {}) + Logger.log("d", "Workspace loading is notifying rest of the code of changes...") # Actually change the active machine. @@ -917,9 +928,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but # they won't take effect until this function is done. # To solve this, we schedule _updateActiveMachine() for later so it will have the latest data. + self._updateActiveMachine(global_stack) - if not self._load_profile: + if self._is_ucp: # Now we have switched, apply the user settings self._applyUserSettings(global_stack, extruder_stack_dict, self._user_settings) @@ -931,6 +943,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): base_file_name = os.path.basename(file_name) self.setWorkspaceName(base_file_name) + self._is_ucp = None return nodes, self._loadMetadata(file_name) @staticmethod @@ -1309,39 +1322,40 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_manager.setActiveMachine(global_stack.getId()) # Set metadata fields that are missing from the global stack - for key, value in self._machine_info.metadata_dict.items(): - if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata: - global_stack.setMetaDataEntry(key, value) + if not self._is_ucp: + for key, value in self._machine_info.metadata_dict.items(): + if key not in global_stack.getMetaData() and key not in _ignored_machine_network_metadata: + global_stack.setMetaDataEntry(key, value) - if self._quality_changes_to_apply: - quality_changes_group_list = container_tree.getCurrentQualityChangesGroups() - quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None) - if not quality_changes_group: - Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply) - return - machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True) - else: - self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None - quality_group_dict = container_tree.getCurrentQualityGroups() - if self._quality_type_to_apply in quality_group_dict: - quality_group = quality_group_dict[self._quality_type_to_apply] + if self._quality_changes_to_apply !=None: + quality_changes_group_list = container_tree.getCurrentQualityChangesGroups() + quality_changes_group = next((qcg for qcg in quality_changes_group_list if qcg.name == self._quality_changes_to_apply), None) + if not quality_changes_group: + Logger.log("e", "Could not find quality_changes [%s]", self._quality_changes_to_apply) + return + machine_manager.setQualityChangesGroup(quality_changes_group, no_dialog = True) else: - Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply) - preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type") - quality_group = quality_group_dict.get(preferred_quality_type) - if quality_group is None: - Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type) - - if quality_group is not None: - machine_manager.setQualityGroup(quality_group, no_dialog = True) - - # Also apply intent if available - available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories() - if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list: - machine_manager.setIntentByCategory(self._intent_category_to_apply) + self._quality_type_to_apply = self._quality_type_to_apply.lower() if self._quality_type_to_apply else None + quality_group_dict = container_tree.getCurrentQualityGroups() + if self._quality_type_to_apply in quality_group_dict: + quality_group = quality_group_dict[self._quality_type_to_apply] else: - # if no intent is provided, reset to the default (balanced) intent - machine_manager.resetIntents() + Logger.log("i", "Could not find quality type [%s], switch to default", self._quality_type_to_apply) + preferred_quality_type = global_stack.getMetaDataEntry("preferred_quality_type") + quality_group = quality_group_dict.get(preferred_quality_type) + if quality_group is None: + Logger.log("e", "Could not get preferred quality type [%s]", preferred_quality_type) + + if quality_group is not None: + machine_manager.setQualityGroup(quality_group, no_dialog = True) + + # Also apply intent if available + available_intent_category_list = IntentManager.getInstance().currentAvailableIntentCategories() + if self._intent_category_to_apply is not None and self._intent_category_to_apply in available_intent_category_list: + machine_manager.setIntentByCategory(self._intent_category_to_apply) + else: + # if no intent is provided, reset to the default (balanced) intent + machine_manager.resetIntents() # Notify everything/one that is to notify about changes. global_stack.containersChanged.emit(global_stack.getTop()) diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 1fafcf59f5..4b9f1eaa6f 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -63,21 +63,22 @@ class WorkspaceDialog(QObject): self._machine_name = "" self._machine_type = "" self._variant_type = "" + self._current_machine_name = "" self._material_labels = [] self._extruders = [] self._objects_on_plate = False self._is_printer_group = False - self._updatable_machines_model = MachineListModel(self, listenToChanges=False) + self._updatable_machines_model = MachineListModel(self, listenToChanges = False, showCloudPrinters = True) self._missing_package_metadata: List[Dict[str, str]] = [] self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry() self._install_missing_package_dialog: Optional[QObject] = None self._is_abstract_machine = False self._is_networked_machine = False self._is_compatible_machine = False - self._has_visible_select_same_profile = False - self._select_same_profile_checked = True self._allow_create_machine = True self._exported_settings_model = SpecificSettingsModel() + self._current_machine_pos_index = 0 + self._is_ucp = False machineConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal() @@ -102,8 +103,7 @@ class WorkspaceDialog(QObject): isPrinterGroupChanged = pyqtSignal() missingPackagesChanged = pyqtSignal() isCompatibleMachineChanged = pyqtSignal() - hasVisibleSelectSameProfileChanged = pyqtSignal() - selectSameProfileCheckedChanged = pyqtSignal() + isUcpChanged = pyqtSignal() @pyqtProperty(bool, notify = isPrinterGroupChanged) def isPrinterGroup(self) -> bool: @@ -176,8 +176,30 @@ class WorkspaceDialog(QObject): self._machine_name = machine_name self.machineNameChanged.emit() + def setCurrentMachineName(self, machine: str) -> None: + self._current_machine_name = machine + + @pyqtProperty(str, notify = machineNameChanged) + def currentMachineName(self) -> str: + return self._current_machine_name + + @staticmethod + def getIndexOfCurrentMachine(list_of_dicts, key, value, defaultIndex): + for i, d in enumerate(list_of_dicts): + if d.get(key) == value: # found the dictionary + return i + return defaultIndex + + @pyqtProperty(int, notify = machineNameChanged) + def currentMachinePositionIndex(self): + return self._current_machine_pos_index + @pyqtProperty(QObject, notify = updatableMachinesChanged) def updatableMachinesModel(self) -> MachineListModel: + if self._current_machine_name != "": + self._current_machine_pos_index = self.getIndexOfCurrentMachine(self._updatable_machines_model.getItems(), "id", self._current_machine_name, defaultIndex = 0) + else: + self._current_machine_pos_index = 0 return cast(MachineListModel, self._updatable_machines_model) def setUpdatableMachines(self, updatable_machines: List[GlobalStack]) -> None: @@ -318,23 +340,14 @@ class WorkspaceDialog(QObject): def isCompatibleMachine(self) -> bool: return self._is_compatible_machine - def setHasVisibleSelectSameProfileChanged(self, has_visible_select_same_profile): - if has_visible_select_same_profile != self._has_visible_select_same_profile: - self._has_visible_select_same_profile = has_visible_select_same_profile - self.hasVisibleSelectSameProfileChanged.emit() + def setIsUcp(self, isUcp: bool) -> None: + if isUcp != self._is_ucp: + self._is_ucp = isUcp + self.isUcpChanged.emit() - @pyqtProperty(bool, notify = hasVisibleSelectSameProfileChanged) - def hasVisibleSelectSameProfile(self): - return self._has_visible_select_same_profile - - def setSelectSameProfileChecked(self, select_same_profile_checked): - if select_same_profile_checked != self._select_same_profile_checked: - self._select_same_profile_checked = select_same_profile_checked - self.selectSameProfileCheckedChanged.emit() - - @pyqtProperty(bool, notify = selectSameProfileCheckedChanged, fset = setSelectSameProfileChecked) - def selectSameProfileChecked(self): - return self._select_same_profile_checked + @pyqtProperty(bool, notify=isUcpChanged) + def isUcp(self): + return self._is_ucp def setAllowCreatemachine(self, allow_create_machine): self._allow_create_machine = allow_create_machine @@ -343,7 +356,7 @@ class WorkspaceDialog(QObject): def allowCreateMachine(self): return self._allow_create_machine - @pyqtProperty(QObject, constant = True) + @pyqtProperty(QObject) def exportedSettingModel(self): return self._exported_settings_model diff --git a/plugins/3MFReader/WorkspaceDialog.qml b/plugins/3MFReader/WorkspaceDialog.qml index 8d06b32e14..c8d53a1154 100644 --- a/plugins/3MFReader/WorkspaceDialog.qml +++ b/plugins/3MFReader/WorkspaceDialog.qml @@ -12,7 +12,7 @@ import Cura 1.1 as Cura UM.Dialog { id: workspaceDialog - title: catalog.i18nc("@title:window", "Open Project") + title: manager.isUcp? catalog.i18nc("@title:window", "Open Universal Cura Project (UCP)"): catalog.i18nc("@title:window", "Open Project") margin: UM.Theme.getSize("default_margin").width minimumWidth: UM.Theme.getSize("modal_window_minimum").width @@ -28,7 +28,7 @@ UM.Dialog UM.Label { id: titleLabel - text: catalog.i18nc("@action:title", "Summary - Cura Project") + text: manager.isUcp? catalog.i18nc("@action:title", "Summary - Open Universal Cura Project (UCP)"): catalog.i18nc("@action:title", "Summary - Cura Project") font: UM.Theme.getFont("large") anchors.top: parent.top anchors.left: parent.left @@ -96,7 +96,7 @@ UM.Dialog WorkspaceRow { leftLabelText: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name") - rightLabelText: manager.machineName == catalog.i18nc("@button", "Create new") ? "" : manager.machineName + rightLabelText: manager.isUcp? manager.machineType: manager.machineName == catalog.i18nc("@button", "Create new") ? "" : manager.machineName } } @@ -159,7 +159,7 @@ UM.Dialog WorkspaceSection { id: profileSection - title: catalog.i18nc("@action:label", "Profile settings") + title: manager.isUcp? catalog.i18nc("@action:label", "Suggested Profile settings"):catalog.i18nc("@action:label", "Profile settings") iconSource: UM.Theme.getIcon("Sliders") content: Column { @@ -185,32 +185,44 @@ UM.Dialog { leftLabelText: catalog.i18nc("@action:label", "Not in profile") rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings) - visible: manager.numUserSettings != 0 && manager.selectSameProfileChecked && manager.isCompatibleMachine + visible: manager.numUserSettings != 0 && manager.isCompatibleMachine } WorkspaceRow { leftLabelText: catalog.i18nc("@action:label", "Derivative from") rightLabelText: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges) - visible: manager.numSettingsOverridenByQualityChanges != 0 && manager.selectSameProfileChecked && manager.isCompatibleMachine + visible: manager.numSettingsOverridenByQualityChanges != 0 && manager.isCompatibleMachine } + } + } + WorkspaceSection + { + id: ucpProfileSection + visible: manager.isUcp + title: catalog.i18nc("@action:label", "Settings Loaded from UCP file") + iconSource: UM.Theme.getIcon("Settings") + + content: Column + { + id: ucpProfileSettingsValuesTable + spacing: UM.Theme.getSize("default_margin").height + leftPadding: UM.Theme.getSize("medium_button_icon").width + UM.Theme.getSize("default_margin").width WorkspaceRow { - leftLabelText: catalog.i18nc("@action:label", "Specific settings") + leftLabelText: catalog.i18nc("@action:label", "Settings Loaded from UCP file") rightLabelText: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.exportedSettingModel.rowCount()).arg(manager.exportedSettingModel.rowCount()) buttonText: tableViewSpecificSettings.shouldBeVisible ? catalog.i18nc("@action:button", "Hide settings") : catalog.i18nc("@action:button", "Show settings") - visible: !manager.selectSameProfileChecked || !manager.isCompatibleMachine onButtonClicked: tableViewSpecificSettings.shouldBeVisible = !tableViewSpecificSettings.shouldBeVisible } - Cura.TableView { id: tableViewSpecificSettings width: parent.width - parent.leftPadding - UM.Theme.getSize("default_margin").width height: UM.Theme.getSize("card").height - visible: shouldBeVisible && (!manager.selectSameProfileChecked || !manager.isCompatibleMachine) - property bool shouldBeVisible: false + visible: shouldBeVisible && manager.isUcp + property bool shouldBeVisible: true columnHeaders: [ @@ -227,15 +239,11 @@ UM.Dialog } } - UM.CheckBox + property var modelRows: manager.exportedSettingModel.items + onModelRowsChanged: { - text: catalog.i18nc("@action:checkbox", "Select the same profile") - onEnabledChanged: manager.selectSameProfileChecked = enabled - tooltip: enabled ? "" : catalog.i18nc("@tooltip", "You can use the same profile only if you have the same printer as the project was published with") - visible: manager.hasVisibleSelectSameProfile && manager.isCompatibleMachine - - checked: manager.selectSameProfileChecked - onCheckedChanged: manager.selectSameProfileChecked = checked + tableModel.clear() + tableModel.rows = modelRows } } @@ -245,7 +253,7 @@ UM.Dialog id: qualityChangesResolveComboBox model: resolveStrategiesModel textRole: "label" - visible: manager.qualityChangesConflict + visible: manager.qualityChangesConflict && !manager.isUcp contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width textFont: UM.Theme.getFont("medium") @@ -274,7 +282,7 @@ UM.Dialog WorkspaceSection { id: materialSection - title: catalog.i18nc("@action:label", "Material settings") + title: manager.isUcp? catalog.i18nc("@action:label", "Suggested Material settings"): catalog.i18nc("@action:label", "Material settings") iconSource: UM.Theme.getIcon("Spool") content: Column { @@ -299,7 +307,7 @@ UM.Dialog id: materialResolveComboBox model: resolveStrategiesModel textRole: "label" - visible: manager.materialConflict + visible: manager.materialConflict && !manager.isUcp contentLeftPadding: UM.Theme.getSize("default_margin").width + UM.Theme.getSize("narrow_margin").width textFont: UM.Theme.getFont("medium") @@ -330,6 +338,7 @@ UM.Dialog id: visibilitySection title: catalog.i18nc("@action:label", "Setting visibility") iconSource: UM.Theme.getIcon("Eye") + visible : !manager.isUcp content: Column { spacing: UM.Theme.getSize("default_margin").height @@ -467,12 +476,13 @@ UM.Dialog { if (visible) { - // Force relead the comboboxes + // Force reload the comboboxes // Since this dialog is only created once the first time you open it, these comboxes need to be reloaded // each time it is shown after the first time so that the indexes will update correctly. materialSection.reloadValues() profileSection.reloadValues() printerSection.reloadValues() + ucpProfileSection.reloadValues() } } } diff --git a/plugins/3MFReader/WorkspaceSection.qml b/plugins/3MFReader/WorkspaceSection.qml index 63b5e89b41..7c8b01be7a 100644 --- a/plugins/3MFReader/WorkspaceSection.qml +++ b/plugins/3MFReader/WorkspaceSection.qml @@ -84,7 +84,8 @@ Item { anchors.right: parent.right anchors.verticalCenter: comboboxLabel.verticalCenter - + color: UM.Theme.getColor("small_button_text") + icon: UM.Theme.getIcon("Information") text: comboboxTooltipText visible: comboboxTooltipText != "" } diff --git a/plugins/3MFWriter/SettingSelection.qml b/plugins/3MFWriter/SettingSelection.qml index 478c2d393c..794a5aacf6 100644 --- a/plugins/3MFWriter/SettingSelection.qml +++ b/plugins/3MFWriter/SettingSelection.qml @@ -19,7 +19,7 @@ RowLayout Layout.preferredWidth: UM.Theme.getSize("setting").width checked: modelData.selected onClicked: modelData.selected = checked - enabled: modelData.selectable + tooltip: modelData.selectable ? "" :catalog.i18nc("@tooltip", "This setting may not perform well while exporting to UCP. Users are asked to add it at their own risk.") } UM.Label @@ -30,9 +30,10 @@ RowLayout UM.HelpIcon { UM.I18nCatalog { id: catalog; name: "cura" } - text: catalog.i18nc("@tooltip", - "This setting can't be exported because it depends on the used printer capacities") + "This setting may not perform well while exporting to UCP, Users are asked to add it at their own risk.") visible: !modelData.selectable } + + } diff --git a/plugins/3MFWriter/SettingsExportModel.py b/plugins/3MFWriter/SettingsExportModel.py index 3b034236c8..99ffad4bac 100644 --- a/plugins/3MFWriter/SettingsExportModel.py +++ b/plugins/3MFWriter/SettingsExportModel.py @@ -61,43 +61,47 @@ class SettingsExportModel(QObject): 'top_skin_preshrink', 'interlocking_enable'} - def __init__(self, parent = None): + PER_MODEL_EXPORTABLE_SETTINGS_KEYS = {"anti_overhang_mesh", + "infill_mesh", + "cutting_mesh", + "support_mesh"} + + def __init__(self, parent=None): super().__init__(parent) self._settings_groups = [] application = CuraApplication.getInstance() - # Display global settings - global_stack = application.getGlobalContainerStack() - self._settings_groups.append(SettingsExportGroup(global_stack, - "Global settings", - SettingsExportGroup.Category.Global, - self._exportSettings(global_stack))) + self._appendGlobalSettings(application) + self._appendExtruderSettings(application) + self._appendModelSettings(application) - # Display per-extruder settings + def _appendGlobalSettings(self, application): + global_stack = application.getGlobalContainerStack() + self._settings_groups.append(SettingsExportGroup( + global_stack, "Global settings", SettingsExportGroup.Category.Global, self._exportSettings(global_stack))) + + def _appendExtruderSettings(self, application): extruders_stacks = ExtruderManager.getInstance().getUsedExtruderStacks() for extruder_stack in extruders_stacks: - color = "" - if extruder_stack.material: - color = extruder_stack.material.getMetaDataEntry("color_code") + color = extruder_stack.material.getMetaDataEntry("color_code") if extruder_stack.material else "" + self._settings_groups.append(SettingsExportGroup( + extruder_stack, "Extruder settings", SettingsExportGroup.Category.Extruder, + self._exportSettings(extruder_stack), extruder_index=extruder_stack.position, extruder_color=color)) - self._settings_groups.append(SettingsExportGroup(extruder_stack, - "Extruder settings", - SettingsExportGroup.Category.Extruder, - self._exportSettings(extruder_stack), - extruder_index=extruder_stack.position, - extruder_color=color)) + def _appendModelSettings(self, application): + scene = application.getController().getScene() + for scene_node in scene.getRoot().getChildren(): + self._appendNodeSettings(scene_node, "Model settings", SettingsExportGroup.Category.Model) + + def _appendNodeSettings(self, node, title_prefix, category): + stack = node.callDecoration("getStack") + if stack: + self._settings_groups.append(SettingsExportGroup( + stack, f"{title_prefix}", category, self._exportSettings(stack), node.getName())) + for child in node.getChildren(): + self._appendNodeSettings(child, f"Children of {node.getName()}", SettingsExportGroup.Category.Model) - # Display per-model settings - scene_root = application.getController().getScene().getRoot() - for scene_node in scene_root.getChildren(): - per_model_stack = scene_node.callDecoration("getStack") - if per_model_stack is not None: - self._settings_groups.append(SettingsExportGroup(per_model_stack, - "Model settings", - SettingsExportGroup.Category.Model, - self._exportSettings(per_model_stack), - scene_node.getName())) @pyqtProperty(list, constant=True) def settingsGroups(self) -> List[SettingsExportGroup]: @@ -107,8 +111,10 @@ class SettingsExportModel(QObject): def _exportSettings(settings_stack): user_settings_container = settings_stack.userChanges user_keys = user_settings_container.getAllKeys() - + exportable_settings = SettingsExportModel.EXPORTABLE_SETTINGS settings_export = [] + # Check whether any of the user keys exist in PER_MODEL_EXPORTABLE_SETTINGS_KEYS + is_exportable = any(key in SettingsExportModel.PER_MODEL_EXPORTABLE_SETTINGS_KEYS for key in user_keys) for setting_to_export in user_keys: label = settings_stack.getProperty(setting_to_export, "label") @@ -117,7 +123,6 @@ class SettingsExportModel(QObject): setting_type = settings_stack.getProperty(setting_to_export, "type") if setting_type is not None: - # This is not very good looking, but will do for now value = f"{str(SettingDefinition.settingValueToString(setting_type, value))} {unit}" else: value = str(value) @@ -125,6 +130,6 @@ class SettingsExportModel(QObject): settings_export.append(SettingExport(setting_to_export, label, value, - setting_to_export in SettingsExportModel.EXPORTABLE_SETTINGS)) + is_exportable or setting_to_export in exportable_settings)) return settings_export diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 5583059a2f..1c14c37cfd 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -462,40 +462,5 @@ class ThreeMFWriter(MeshWriter): return extra_settings def exportUcp(self): - preferences = CuraApplication.getInstance().getPreferences() - if preferences.getValue("cura/dialog_on_ucp_project_save"): - self._config_dialog = UCPDialog() - self._config_dialog.show() - else: - application = CuraApplication.getInstance() - workspace_handler = application.getInstance().getWorkspaceFileHandler() - - # Set the model to the workspace writer - mesh_writer = workspace_handler.getWriter("3MFWriter") - mesh_writer.setExportModel(SettingsExportModel()) - - # Open file dialog and write the file - device = application.getOutputDeviceManager().getOutputDevice("local_file") - nodes = [application.getController().getScene().getRoot()] - - file_name = CuraApplication.getInstance().getPrintInformation().baseName - - try: - device.requestWrite( - nodes, - file_name, - ["application/vnd.ms-package.3dmanufacturing-3dmodel+xml"], - workspace_handler, - preferred_mimetype_list="application/vnd.ms-package.3dmanufacturing-3dmodel+xml" - ) - except OutputDeviceError.UserCanceledError: - self._onRejected() - except Exception as e: - message = Message( - catalog.i18nc("@info:error", "Unable to write to file: {0}", file_name), - title=catalog.i18nc("@info:title", "Error"), - message_type=Message.MessageType.ERROR - ) - message.show() - Logger.logException("e", "Unable to write to file %s: %s", file_name, e) - self._onRejected() + self._config_dialog = UCPDialog() + self._config_dialog.show() diff --git a/plugins/3MFWriter/UCPDialog.qml b/plugins/3MFWriter/UCPDialog.qml index 3a0e6bf842..5d094f9187 100644 --- a/plugins/3MFWriter/UCPDialog.qml +++ b/plugins/3MFWriter/UCPDialog.qml @@ -19,21 +19,6 @@ UM.Dialog minimumHeight: UM.Theme.getSize("modal_window_minimum").height backgroundColor: UM.Theme.getColor("detail_background") - property bool dontShowAgain: false - - function storeDontShowAgain() - { - UM.Preferences.setValue("cura/dialog_on_ucp_project_save", !dontShowAgainCheckbox.checked) - UM.Preferences.setValue("cura/asked_dialog_on_ucp_project_save", false) - } - - onVisibleChanged: - { - if(visible && UM.Preferences.getValue("cura/asked_dialog_on_ucp_project_save")) - { - dontShowAgain = !UM.Preferences.getValue("cura/dialog_on_ucp_project_save") - } - } headerComponent: Rectangle { @@ -90,15 +75,7 @@ UM.Dialog delegate: SettingsSelectionGroup { Layout.margins: 0 } } } - leftButtons: - [ - UM.CheckBox - { - id: dontShowAgainCheckbox - text: catalog.i18nc("@action:label", "Don't show project summary on save again") - checked: dontShowAgain - } - ] + rightButtons: [ Cura.TertiaryButton @@ -117,9 +94,6 @@ UM.Dialog onClosing: { - storeDontShowAgain() manager.notifyClosed() } - onRejected: storeDontShowAgain() - onAccepted: storeDontShowAgain() } diff --git a/plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py b/plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py index 17d17c9e24..9dac57e218 100644 --- a/plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py +++ b/plugins/PostProcessingPlugin/scripts/LimitXYAccelJerk.py @@ -9,6 +9,7 @@ # When setting an accel limit on multi-extruder printers ALL extruders are effected. # This post does not distinguish between Print Accel and Travel Accel. The limit is the limit for all regardless. Example: Skin Accel = 1000 and Outer Wall accel = 500. If the limit is set to 300 then both Skin and Outer Wall will be Accel = 300. # 9/15/2023 added support for RepRap M566 command for Jerk in mm/min +# 2/4/2024 Added a block so the script doesn't run unless Accel Control is enabled in Cura. This should keep users from increasing the Accel Limits. from ..Script import Script from cura.CuraApplication import CuraApplication @@ -45,6 +46,10 @@ class LimitXYAccelJerk(Script): # Warn the user if the printer is multi-extruder------------------ if ext_count > 1: Message(text = " 'Limit the X-Y Accel/Jerk': The post processor treats all extruders the same. If you have multiple extruders they will all be subject to the same Accel and Jerk limits imposed. If you have different Travel and Print Accel they will also be subject to the same limits. If that is not acceptable then you should not use this Post Processor.").show() + + # Warn the user if Accel Control is not enabled in Cura. This keeps the script from being able to increase the Accel limits----------- + if not bool(extruder[0].getProperty("acceleration_enabled", "value")): + Message(title = "[Limit the X-Y Accel/Jerk]", text = "You must have 'Enable Acceleration Control' checked in Cura or the script will exit.").show() def getSettingDataString(self): return """{ @@ -169,6 +174,13 @@ class LimitXYAccelJerk(Script): extruder = mycura.extruderList machine_name = str(mycura.getProperty("machine_name", "value")) print_sequence = str(mycura.getProperty("print_sequence", "value")) + acceleration_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value")) + + # Exit if acceleration control is not enabled---------------- + if not acceleration_enabled: + Message(title = "[Limit the X-Y Accel/Jerk]", text = "DID NOT RUN. You must have 'Enable Acceleration Control' checked in Cura.").show() + data[0] += "; [LimitXYAccelJerk] DID NOT RUN because 'Enable Acceleration Control' is not checked in Cura.\n" + return data # Exit if 'one_at_a_time' is enabled------------------------- if print_sequence == "one_at_a_time": @@ -183,12 +195,8 @@ class LimitXYAccelJerk(Script): return data type_of_change = str(self.getSettingValueByKey("type_of_change")) - accel_print_enabled = bool(extruder[0].getProperty("acceleration_enabled", "value")) - accel_travel_enabled = bool(extruder[0].getProperty("acceleration_travel_enabled", "value")) accel_print = extruder[0].getProperty("acceleration_print", "value") accel_travel = extruder[0].getProperty("acceleration_travel", "value") - jerk_print_enabled = str(extruder[0].getProperty("jerk_enabled", "value")) - jerk_travel_enabled = str(extruder[0].getProperty("jerk_travel_enabled", "value")) jerk_print_old = extruder[0].getProperty("jerk_print", "value") jerk_travel_old = extruder[0].getProperty("jerk_travel", "value") if int(accel_print) >= int(accel_travel): diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 4983363946..776417e15d 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -701,24 +701,33 @@ UM.MainWindow if (hasProjectFile) { - var projectFile = projectFileUrlList[0]; - - // check preference - var choice = UM.Preferences.getValue("cura/choice_on_open_project"); - if (choice == "open_as_project") + var projectFile = projectFileUrlList[0] + var is_ucp = CuraApplication.isProjectUcp(projectFile); + if (is_ucp) { - openFilesIncludingProjectsDialog.loadProjectFile(projectFile); + askOpenAsProjectOrUcpOrImportModelsDialog.fileUrl = projectFile; + askOpenAsProjectOrUcpOrImportModelsDialog.addToRecent = true; + askOpenAsProjectOrUcpOrImportModelsDialog.show(); } - else if (choice == "open_as_model") + else { - openFilesIncludingProjectsDialog.loadModelFiles([projectFile].slice()); - } - else // always ask - { - // ask whether to open as project or as models - askOpenAsProjectOrModelsDialog.fileUrl = projectFile; - askOpenAsProjectOrModelsDialog.addToRecent = true; - askOpenAsProjectOrModelsDialog.show(); + // check preference + var choice = UM.Preferences.getValue("cura/choice_on_open_project"); + if (choice == "open_as_project") + { + openFilesIncludingProjectsDialog.loadProjectFile(projectFile); + } + else if (choice == "open_as_model") + { + openFilesIncludingProjectsDialog.loadModelFiles([projectFile].slice()); + } + else // always ask + { + // ask whether to open as project or as models + askOpenAsProjectOrModelsDialog.fileUrl = projectFile; + askOpenAsProjectOrModelsDialog.addToRecent = true; + askOpenAsProjectOrModelsDialog.show(); + } } } else @@ -769,14 +778,30 @@ UM.MainWindow id: askOpenAsProjectOrModelsDialog } + AskOpenAsProjectOrUcpOrImportModel + { + id: askOpenAsProjectOrUcpOrImportModelsDialog + } + Connections { target: CuraApplication function onOpenProjectFile(project_file, add_to_recent_files) { - askOpenAsProjectOrModelsDialog.fileUrl = project_file; - askOpenAsProjectOrModelsDialog.addToRecent = add_to_recent_files; - askOpenAsProjectOrModelsDialog.show(); + var is_ucp = CuraApplication.isProjectUcp(project_file); + if (is_ucp) + { + + askOpenAsProjectOrUcpOrImportModelsDialog.fileUrl = project_file; + askOpenAsProjectOrUcpOrImportModelsDialog.addToRecent = add_to_recent_files; + askOpenAsProjectOrUcpOrImportModelsDialog.show(); + } + else + { + askOpenAsProjectOrModelsDialog.fileUrl = project_file; + askOpenAsProjectOrModelsDialog.addToRecent = add_to_recent_files; + askOpenAsProjectOrModelsDialog.show(); + } } } diff --git a/resources/qml/Dialogs/AskOpenAsProjectOrUcpOrImportModel.qml b/resources/qml/Dialogs/AskOpenAsProjectOrUcpOrImportModel.qml new file mode 100644 index 0000000000..a68c48b4a6 --- /dev/null +++ b/resources/qml/Dialogs/AskOpenAsProjectOrUcpOrImportModel.qml @@ -0,0 +1,91 @@ +// Copyright (c) 2022 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 2.1 +import QtQuick.Layouts 1.1 + +import UM 1.5 as UM +import Cura 1.0 as Cura + + +UM.Dialog +{ + // This dialog asks the user whether he/she wants to open a project file as a project or import models. + id: base + + title: catalog.i18nc("@title:window", "Open Universal Cura Project (UCP) file") + width: UM.Theme.getSize("small_popup_dialog").width + height: UM.Theme.getSize("small_popup_dialog").height + backgroundColor: UM.Theme.getColor("main_background") + + maximumHeight: height + maximumWidth: width + minimumHeight: maximumHeight + minimumWidth: maximumWidth + + modality: Qt.WindowModal + + property var fileUrl + property var addToRecent: true //Whether to add this file to the recent files list after reading it. + + + // load the project file as separated models + function loadModelFiles() { + CuraApplication.readLocalFile(base.fileUrl, "open_as_model", base.addToRecent) + + base.hide() + } + + // load the project file as Universal cura project + function loadUcpFiles() { + CuraApplication.readLocalUcpFile(base.fileUrl, base.addToRecent) + + base.hide() + } + + // override UM.Dialog accept + function accept () { + + // when hitting 'enter', we always open as project unless open_as_model was explicitly stored as preference + if (openAsPreference == "open_as_model") { + loadModelFiles() + } else{ + loadUcpFiles() + } + } + + Column + { + anchors.fill: parent + spacing: UM.Theme.getSize("default_margin").height + + UM.Label + { + id: questionText + width: parent.width + text: catalog.i18nc("@text:window", "This is a Cura Universal project file. Would you like to open it as a Cura project or Cura Universal Project or import the models from it?") + wrapMode: Text.WordWrap + } + } + + onAccepted: loadUcpFile() + onRejected: loadModelFiles() + + buttonSpacing: UM.Theme.getSize("thin_margin").width + + rightButtons: + [ + Cura.PrimaryButton + { + text: catalog.i18nc("@action:button", "Open as UCP") + iconSource: UM.Theme.getIcon("CuraShareIcon") + onClicked: loadUcpFiles() + }, + Cura.SecondaryButton + { + text: catalog.i18nc("@action:button", "Import models") + onClicked: loadModelFiles() + } + ] +} diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 0f50f169ef..b753d0e48a 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -784,20 +784,6 @@ UM.PreferencesPage } } - UM.TooltipArea - { - width: childrenRect.width - height: childrenRect.height - text: catalog.i18nc("@info:tooltip", "Should a summary be shown when saving a UCP project file?") - - UM.CheckBox - { - text: catalog.i18nc("@option:check", "Show summary dialog when saving a UCP project") - checked: boolCheck(UM.Preferences.getValue("cura/dialog_on_ucp_project_save")) - onCheckedChanged: UM.Preferences.setValue("cura/dialog_on_ucp_project_save", checked) - } - } - UM.TooltipArea { width: childrenRect.width diff --git a/resources/themes/cura-light/icons/default/CuraShareIcon.svg b/resources/themes/cura-light/icons/default/CuraShareIcon.svg new file mode 100644 index 0000000000..fb9a6b922c --- /dev/null +++ b/resources/themes/cura-light/icons/default/CuraShareIcon.svg @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +