Now loading user settings

CURA-11561
This commit is contained in:
Erwan MATHIEU 2024-02-02 16:05:36 +01:00
parent 733ef4d3d8
commit ab0a52063d
4 changed files with 178 additions and 90 deletions

View file

@ -5,6 +5,7 @@ from configparser import ConfigParser
import zipfile import zipfile
import os import os
import json import json
import re
from typing import cast, Dict, List, Optional, Tuple, Any, Set from typing import cast, Dict, List, Optional, Tuple, Any, Set
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -141,10 +142,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._old_new_materials: Dict[str, str] = {} self._old_new_materials: Dict[str, str] = {}
self._machine_info = None self._machine_info = None
self._load_profile = False
def _clearState(self): def _clearState(self):
self._id_mapping = {} self._id_mapping = {}
self._old_new_materials = {} self._old_new_materials = {}
self._machine_info = None self._machine_info = None
self._load_profile = False
def getNewId(self, old_id: str): 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. """Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
@ -228,7 +232,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._resolve_strategies = {k: None for k in resolve_strategy_keys} self._resolve_strategies = {k: None for k in resolve_strategy_keys}
containers_found_dict = {k: False for k in resolve_strategy_keys} containers_found_dict = {k: False for k in resolve_strategy_keys}
# Check whether the file is a PCB # Check whether the file is a PCB, which changes some import options
is_pcb = file_name.endswith('.pcb') is_pcb = file_name.endswith('.pcb')
# #
@ -621,6 +625,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity) self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
self._dialog.setMissingPackagesMetadata(missing_package_metadata) self._dialog.setMissingPackagesMetadata(missing_package_metadata)
self._dialog.setHasVisibleSelectSameProfileChanged(is_pcb) self._dialog.setHasVisibleSelectSameProfileChanged(is_pcb)
self._dialog.setAllowCreatemachine(not is_pcb)
self._dialog.show() self._dialog.show()
# Choosing the initially selected printer in MachineSelector # Choosing the initially selected printer in MachineSelector
@ -652,6 +657,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setIsNetworkedMachine(is_networked_machine) self._dialog.setIsNetworkedMachine(is_networked_machine)
self._dialog.setIsAbstractMachine(is_abstract_machine) self._dialog.setIsAbstractMachine(is_abstract_machine)
self._dialog.setMachineName(machine_name) self._dialog.setMachineName(machine_name)
self._dialog.updateCompatibleMachine()
self._dialog.setSelectSameProfileChecked(self._dialog.isCompatibleMachine)
# Block until the dialog is closed. # Block until the dialog is closed.
self._dialog.waitForClose() self._dialog.waitForClose()
@ -659,6 +666,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if self._dialog.getResult() == {}: if self._dialog.getResult() == {}:
return WorkspaceReader.PreReadResult.cancelled return WorkspaceReader.PreReadResult.cancelled
self._load_profile = not is_pcb or self._dialog.selectSameProfileChecked
self._resolve_strategies = self._dialog.getResult() self._resolve_strategies = self._dialog.getResult()
# #
# There can be 3 resolve strategies coming from the dialog: # There can be 3 resolve strategies coming from the dialog:
@ -694,16 +703,16 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
except EnvironmentError as e: except EnvironmentError as e:
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!", message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)), "Project file <filename>{0}</filename> is suddenly inaccessible: <message>{1}</message>.", file_name, str(e)),
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
message_type = Message.MessageType.ERROR) message_type = Message.MessageType.ERROR)
message.show() message.show()
self.setWorkspaceName("") self.setWorkspaceName("")
return [], {} return [], {}
except zipfile.BadZipFile as e: except zipfile.BadZipFile as e:
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!", message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)), "Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.", file_name, str(e)),
title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"), title = i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
message_type = Message.MessageType.ERROR) message_type = Message.MessageType.ERROR)
message.show() message.show()
self.setWorkspaceName("") self.setWorkspaceName("")
return [], {} return [], {}
@ -767,7 +776,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if not global_stacks: if not global_stacks:
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!", message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag <filename>!",
"Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name), "Project file <filename>{0}</filename> is made using profiles that are unknown to this version of UltiMaker Cura.", file_name),
message_type = Message.MessageType.ERROR) message_type = Message.MessageType.ERROR)
message.show() message.show()
self.setWorkspaceName("") self.setWorkspaceName("")
return [], {} return [], {}
@ -781,84 +790,107 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
for stack in extruder_stacks: for stack in extruder_stacks:
stack.setNextStack(global_stack, connect_signals = False) stack.setNextStack(global_stack, connect_signals = False)
Logger.log("d", "Workspace loading is checking definitions...") user_settings = {}
# 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)]
for definition_container_file in definition_container_files:
container_id = self._stripFileToId(definition_container_file)
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id) if self._load_profile:
if not definitions: Logger.log("d", "Workspace loading is checking definitions...")
definition_container = DefinitionContainer(container_id) # Get all the definition files & check if they exist. If not, add them.
try: definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)]
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"), for definition_container_file in definition_container_files:
file_name = definition_container_file) container_id = self._stripFileToId(definition_container_file)
except ContainerFormatError:
# We cannot just skip the definition file because everything else later will just break if the
# machine definition cannot be found.
Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
definition_container_file, file_name)
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
self._container_registry.addContainer(definition_container)
Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Logger.log("d", "Workspace loading is checking materials...") definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
# Get all the material files and check if they exist. If not, add them. if not definitions:
xml_material_profile = self._getXmlProfileClass() definition_container = DefinitionContainer(container_id)
if self._material_container_suffix is None:
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
if xml_material_profile:
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
for material_container_file in material_container_files:
to_deserialize_material = False
container_id = self._stripFileToId(material_container_file)
need_new_name = False
materials = self._container_registry.findInstanceContainers(id = container_id)
if not materials:
# No material found, deserialize this material later and add it
to_deserialize_material = True
else:
material_container = materials[0]
old_material_root_id = material_container.getMetaDataEntry("base_file")
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
to_deserialize_material = True
if self._resolve_strategies["material"] == "override":
# Remove the old materials and then deserialize the one from the project
root_material_id = material_container.getMetaDataEntry("base_file")
application.getContainerRegistry().removeContainer(root_material_id)
elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added.
container_id = self.getNewId(container_id)
self._old_new_materials[old_material_root_id] = container_id
need_new_name = True
if to_deserialize_material:
material_container = xml_material_profile(container_id)
try: try:
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
file_name = container_id + "." + self._material_container_suffix) file_name = definition_container_file)
except ContainerFormatError: except ContainerFormatError:
Logger.logException("e", "Failed to deserialize material file %s in project file %s", # We cannot just skip the definition file because everything else later will just break if the
material_container_file, file_name) # machine definition cannot be found.
continue Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
if need_new_name: definition_container_file, file_name)
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName()) definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
material_container.setName(new_name) self._container_registry.addContainer(definition_container)
material_container.setDirty(True)
self._container_registry.addContainer(material_container)
Job.yieldThread() Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze. QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
if global_stack: Logger.log("d", "Workspace loading is checking materials...")
# Handle quality changes if any # Get all the material files and check if they exist. If not, add them.
self._processQualityChanges(global_stack) xml_material_profile = self._getXmlProfileClass()
if self._material_container_suffix is None:
self._material_container_suffix = ContainerRegistry.getMimeTypeForContainer(xml_material_profile).suffixes[0]
if xml_material_profile:
material_container_files = [name for name in cura_file_names if name.endswith(self._material_container_suffix)]
for material_container_file in material_container_files:
to_deserialize_material = False
container_id = self._stripFileToId(material_container_file)
need_new_name = False
materials = self._container_registry.findInstanceContainers(id = container_id)
# Prepare the machine if not materials:
self._applyChangesToMachine(global_stack, extruder_stack_dict) # No material found, deserialize this material later and add it
to_deserialize_material = True
else:
material_container = materials[0]
old_material_root_id = material_container.getMetaDataEntry("base_file")
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
to_deserialize_material = True
if self._resolve_strategies["material"] == "override":
# Remove the old materials and then deserialize the one from the project
root_material_id = material_container.getMetaDataEntry("base_file")
application.getContainerRegistry().removeContainer(root_material_id)
elif self._resolve_strategies["material"] == "new":
# Note that we *must* deserialize it with a new ID, as multiple containers will be
# auto created & added.
container_id = self.getNewId(container_id)
self._old_new_materials[old_material_root_id] = container_id
need_new_name = True
if to_deserialize_material:
material_container = xml_material_profile(container_id)
try:
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = container_id + "." + self._material_container_suffix)
except ContainerFormatError:
Logger.logException("e", "Failed to deserialize material file %s in project file %s",
material_container_file, file_name)
continue
if need_new_name:
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
material_container.setName(new_name)
material_container.setDirty(True)
self._container_registry.addContainer(material_container)
Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
else:
Logger.log("d", "Workspace loading user settings...")
try:
user_settings = json.loads(archive.open("Cura/user-settings.json").read().decode("utf-8"))
except KeyError as e:
# If there is no user settings file, it's not a PCB, so notify user of failure.
Logger.log("w", "File %s is not a valid PCB.", file_name)
message = Message(
i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
file_name, str(e)),
title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
message_type=Message.MessageType.ERROR)
message.show()
self.setWorkspaceName("")
return [], {}
if global_stack:
if self._load_profile:
# Handle quality changes if any
self._processQualityChanges(global_stack)
# Prepare the machine
self._applyChangesToMachine(global_stack, extruder_stack_dict)
else:
self._applyUserSettings(global_stack, extruder_stack_dict, user_settings)
Logger.log("d", "Workspace loading is notifying rest of the code of changes...") Logger.log("d", "Workspace loading is notifying rest of the code of changes...")
# Actually change the active machine. # Actually change the active machine.
@ -1181,21 +1213,47 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id] material_node = machine_node.variants[extruder_stack.variant.getName()].materials[root_material_id]
extruder_stack.material = material_node.container extruder_stack.material = material_node.container
def _applyChangesToMachine(self, global_stack, extruder_stack_dict): def _clearMachineSettings(self, global_stack, extruder_stack_dict):
# Clear all first
self._clearStack(global_stack) self._clearStack(global_stack)
for extruder_stack in extruder_stack_dict.values(): for extruder_stack in extruder_stack_dict.values():
self._clearStack(extruder_stack) self._clearStack(extruder_stack)
self._quality_changes_to_apply = None
self._quality_type_to_apply = None
self._intent_category_to_apply = None
self._user_settings_to_apply = None
def _applyUserSettings(self, global_stack, extruder_stack_dict, user_settings):
# Clear all first
self._clearMachineSettings(global_stack, extruder_stack_dict)
for stack_name, settings in user_settings.items():
if stack_name == 'global':
ThreeMFWorkspaceReader._applyUserSettingsOnStack(global_stack, settings)
else:
extruder_match = re.fullmatch('extruder_([0-9]+)', stack_name)
if extruder_match is not None:
extruder_nr = extruder_match.group(1)
if extruder_nr in extruder_stack_dict:
ThreeMFWorkspaceReader._applyUserSettingsOnStack(extruder_stack_dict[extruder_nr], settings)
@staticmethod
def _applyUserSettingsOnStack(stack, user_settings):
user_settings_container = stack.userChanges
for setting_to_import, setting_value in user_settings.items():
user_settings_container.setProperty(setting_to_import, 'value', setting_value)
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first
self._clearMachineSettings(global_stack, extruder_stack_dict)
self._applyDefinitionChanges(global_stack, extruder_stack_dict) self._applyDefinitionChanges(global_stack, extruder_stack_dict)
self._applyUserChanges(global_stack, extruder_stack_dict) self._applyUserChanges(global_stack, extruder_stack_dict)
self._applyVariants(global_stack, extruder_stack_dict) self._applyVariants(global_stack, extruder_stack_dict)
self._applyMaterials(global_stack, extruder_stack_dict) self._applyMaterials(global_stack, extruder_stack_dict)
# prepare the quality to select # prepare the quality to select
self._quality_changes_to_apply = None
self._quality_type_to_apply = None
self._intent_category_to_apply = None
if self._machine_info.quality_changes_info is not None: if self._machine_info.quality_changes_info is not None:
self._quality_changes_to_apply = self._machine_info.quality_changes_info.name self._quality_changes_to_apply = self._machine_info.quality_changes_info.name
else: else:

View file

@ -73,6 +73,8 @@ class WorkspaceDialog(QObject):
self._is_networked_machine = False self._is_networked_machine = False
self._is_compatible_machine = False self._is_compatible_machine = False
self._has_visible_select_same_profile = False self._has_visible_select_same_profile = False
self._select_same_profile_checked = True
self._allow_create_machine = True
machineConflictChanged = pyqtSignal() machineConflictChanged = pyqtSignal()
qualityChangesConflictChanged = pyqtSignal() qualityChangesConflictChanged = pyqtSignal()
@ -98,6 +100,7 @@ class WorkspaceDialog(QObject):
missingPackagesChanged = pyqtSignal() missingPackagesChanged = pyqtSignal()
isCompatibleMachineChanged = pyqtSignal() isCompatibleMachineChanged = pyqtSignal()
hasVisibleSelectSameProfileChanged = pyqtSignal() hasVisibleSelectSameProfileChanged = pyqtSignal()
selectSameProfileCheckedChanged = pyqtSignal()
@pyqtProperty(bool, notify = isPrinterGroupChanged) @pyqtProperty(bool, notify = isPrinterGroupChanged)
def isPrinterGroup(self) -> bool: def isPrinterGroup(self) -> bool:
@ -295,17 +298,19 @@ class WorkspaceDialog(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def setMachineToOverride(self, machine_name: str) -> None: def setMachineToOverride(self, machine_name: str) -> None:
self._override_machine = machine_name
self.updateCompatibleMachine()
def updateCompatibleMachine(self):
registry = ContainerRegistry.getInstance() registry = ContainerRegistry.getInstance()
containers_expected = registry.findDefinitionContainers(name = self._machine_type) containers_expected = registry.findDefinitionContainers(name=self._machine_type)
containers_selected = registry.findContainerStacks(id = machine_name) containers_selected = registry.findContainerStacks(id=self._override_machine)
if len(containers_expected) == 1 and len(containers_selected) == 1: if len(containers_expected) == 1 and len(containers_selected) == 1:
new_compatible_machine = (containers_expected[0] == containers_selected[0].definition) new_compatible_machine = (containers_expected[0] == containers_selected[0].definition)
if new_compatible_machine != self._is_compatible_machine: if new_compatible_machine != self._is_compatible_machine:
self._is_compatible_machine = new_compatible_machine self._is_compatible_machine = new_compatible_machine
self.isCompatibleMachineChanged.emit() self.isCompatibleMachineChanged.emit()
self._override_machine = machine_name
@pyqtProperty(bool, notify = isCompatibleMachineChanged) @pyqtProperty(bool, notify = isCompatibleMachineChanged)
def isCompatibleMachine(self) -> bool: def isCompatibleMachine(self) -> bool:
return self._is_compatible_machine return self._is_compatible_machine
@ -319,6 +324,22 @@ class WorkspaceDialog(QObject):
def hasVisibleSelectSameProfile(self): def hasVisibleSelectSameProfile(self):
return self._has_visible_select_same_profile 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
def setAllowCreatemachine(self, allow_create_machine):
self._allow_create_machine = allow_create_machine
@pyqtProperty(bool, constant = True)
def allowCreateMachine(self):
return self._allow_create_machine
@pyqtSlot() @pyqtSlot()
def closeBackend(self) -> None: def closeBackend(self) -> None:
"""Close the backend: otherwise one could end up with "Slicing...""" """Close the backend: otherwise one could end up with "Slicing..."""

View file

@ -120,13 +120,17 @@ UM.Dialog
minDropDownWidth: machineSelector.width minDropDownWidth: machineSelector.width
buttons: [ Component
{
id: componentNewPrinter
Cura.SecondaryButton Cura.SecondaryButton
{ {
id: createNewPrinter id: createNewPrinter
text: catalog.i18nc("@button", "Create new") text: catalog.i18nc("@button", "Create new")
fixedWidthMode: true fixedWidthMode: true
width: parent.width - leftPadding * 1.5 width: parent.width - leftPadding * 1.5
visible: manager.allowCreateMachine
onClicked: onClicked:
{ {
toggleContent() toggleContent()
@ -136,7 +140,9 @@ UM.Dialog
manager.setIsNetworkedMachine(false) manager.setIsNetworkedMachine(false)
} }
} }
] }
buttons: manager.allowCreateMachine ? [componentNewPrinter.createObject()] : []
onSelectPrinter: function(machine) onSelectPrinter: function(machine)
{ {
@ -191,9 +197,12 @@ UM.Dialog
{ {
text: catalog.i18nc("@action:checkbox", "Select the same profile") text: catalog.i18nc("@action:checkbox", "Select the same profile")
enabled: manager.isCompatibleMachine enabled: manager.isCompatibleMachine
onEnabledChanged: checked = enabled 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") 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 visible: manager.hasVisibleSelectSameProfile
checked: manager.selectSameProfileChecked
onCheckedChanged: manager.selectSameProfileChecked = checked
} }
} }

View file

@ -105,7 +105,7 @@ class SettingsExportModel(QObject):
@staticmethod @staticmethod
def _exportSettings(settings_stack): def _exportSettings(settings_stack):
user_settings_container = settings_stack.getTop() user_settings_container = settings_stack.userChanges
user_keys = user_settings_container.getAllKeys() user_keys = user_settings_container.getAllKeys()
settings_export = [] settings_export = []