Merge branch 'master' into CURA-5334_extruder_def_for_all

This commit is contained in:
Jack Ha 2018-06-11 16:56:42 +02:00
commit c97f276583
21 changed files with 131 additions and 115 deletions

View file

@ -3,30 +3,26 @@
from cura.Backups.BackupsManager import BackupsManager
## The back-ups API provides a version-proof bridge between Cura's
# BackupManager and plug-ins that hook into it.
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.backups.createBackup()
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
class Backups:
"""
The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it.
Usage:
from cura.API import CuraAPI
api = CuraAPI()
api.backups.createBackup()
api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})
"""
manager = BackupsManager() # Re-used instance of the backups manager.
## Create a new back-up using the BackupsManager.
# \return Tuple containing a ZIP file with the back-up data and a dict
# with metadata about the back-up.
def createBackup(self) -> (bytes, dict):
"""
Create a new backup using the BackupsManager.
:return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup.
"""
return self.manager.createBackup()
## Restore a back-up using the BackupsManager.
# \param zip_file A ZIP file containing the actual back-up data.
# \param meta_data Some metadata needed for restoring a back-up, like the
# Cura version number.
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
"""
Restore a backup using the BackupManager.
:param zip_file: A ZIP file containing the actual backup data.
:param meta_data: Some meta data needed for restoring a backup, like the Cura version number.
"""
return self.manager.restoreBackup(zip_file, meta_data)

View file

@ -3,14 +3,13 @@
from UM.PluginRegistry import PluginRegistry
from cura.API.Backups import Backups
## The official Cura API that plug-ins can use to interact with Cura.
#
# Python does not technically prevent talking to other classes as well, but
# this API provides a version-safe interface with proper deprecation warnings
# etc. Usage of any other methods than the ones provided in this API can cause
# plug-ins to be unstable.
class CuraAPI:
"""
The official Cura API that plugins can use to interact with Cura.
Python does not technically prevent talking to other classes as well,
but this API provides a version-safe interface with proper deprecation warnings etc.
Usage of any other methods than the ones provided in this API can cause plugins to be unstable.
"""
# For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion

View file

@ -17,12 +17,11 @@ from UM.Resources import Resources
from cura.CuraApplication import CuraApplication
## The back-up class holds all data about a back-up.
#
# It is also responsible for reading and writing the zip file to the user data
# folder.
class Backup:
"""
The backup class holds all data about a backup.
It is also responsible for reading and writing the zip file to the user data folder.
"""
# These files should be ignored when making a backup.
IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
@ -33,10 +32,8 @@ class Backup:
self.zip_file = zip_file # type: Optional[bytes]
self.meta_data = meta_data # type: Optional[dict]
## Create a back-up from the current user config folder.
def makeFromCurrent(self) -> (bool, Optional[str]):
"""
Create a backup from the current user config folder.
"""
cura_release = CuraApplication.getInstance().getVersion()
version_data_dir = Resources.getDataStoragePath()
@ -75,12 +72,10 @@ class Backup:
"plugin_count": str(plugin_count)
}
## Make a full archive from the given root path with the given name.
# \param root_path The root directory to archive recursively.
# \return The archive as bytes.
def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
"""
Make a full archive from the given root path with the given name.
:param root_path: The root directory to archive recursively.
:return: The archive as bytes.
"""
ignore_string = re.compile("|".join(self.IGNORED_FILES))
try:
archive = ZipFile(buffer, "w", ZIP_DEFLATED)
@ -99,15 +94,13 @@ class Backup:
"Could not create archive from user data directory: {}".format(error)))
return None
## Show a UI message.
def _showMessage(self, message: str) -> None:
"""Show a UI message"""
Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show()
## Restore this back-up.
# \return Whether we had success or not.
def restore(self) -> bool:
"""
Restore this backups
:return: A boolean whether we had success or not.
"""
if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None):
# We can restore without the minimum required information.
Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.")
@ -140,14 +133,12 @@ class Backup:
return extracted
## Extract the whole archive to the given target path.
# \param archive The archive as ZipFile.
# \param target_path The target path.
# \return Whether we had success or not.
@staticmethod
def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
"""
Extract the whole archive to the given target path.
:param archive: The archive as ZipFile.
:param target_path: The target path.
:return: A boolean whether we had success or not.
"""
Logger.log("d", "Removing current data in location: %s", target_path)
Resources.factoryReset()
Logger.log("d", "Extracting backup to location: %s", target_path)

View file

@ -7,19 +7,18 @@ from cura.Backups.Backup import Backup
from cura.CuraApplication import CuraApplication
## The BackupsManager is responsible for managing the creating and restoring of
# back-ups.
#
# Back-ups themselves are represented in a different class.
class BackupsManager:
"""
The BackupsManager is responsible for managing the creating and restoring of backups.
Backups themselves are represented in a different class.
"""
def __init__(self):
self._application = CuraApplication.getInstance()
## Get a back-up of the current configuration.
# \return A tuple containing a ZipFile (the actual back-up) and a dict
# containing some metadata (like version).
def createBackup(self) -> (Optional[bytes], Optional[dict]):
"""
Get a backup of the current configuration.
:return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version).
"""
self._disableAutoSave()
backup = Backup()
backup.makeFromCurrent()
@ -27,12 +26,11 @@ class BackupsManager:
# We don't return a Backup here because we want plugins only to interact with our API and not full objects.
return backup.zip_file, backup.meta_data
## Restore a back-up from a given ZipFile.
# \param zip_file A bytes object containing the actual back-up.
# \param meta_data A dict containing some metadata that is needed to
# restore the back-up correctly.
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
"""
Restore a backup from a given ZipFile.
:param zip_file: A bytes object containing the actual backup.
:param meta_data: A dict containing some meta data that is needed to restore the backup correctly.
"""
if not meta_data.get("cura_release", None):
# If there is no "cura_release" specified in the meta data, we don't execute a backup restore.
Logger.log("w", "Tried to restore a backup without specifying a Cura version number.")
@ -47,10 +45,11 @@ class BackupsManager:
# We don't want to store the data at this point as that would override the just-restored backup.
self._application.windowClosed(save_data=False)
## Here we try to disable the auto-save plug-in as it might interfere with
# restoring a back-up.
def _disableAutoSave(self):
"""Here we try to disable the auto-save plugin as it might interfere with restoring a backup."""
self._application.setSaveDataEnabled(False)
## Re-enable auto-save after we're done.
def _enableAutoSave(self):
"""Re-enable auto-save after we're done."""
self._application.setSaveDataEnabled(True)

View file

@ -225,6 +225,8 @@ class CuraApplication(QtApplication):
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
self._container_registry_class = CuraContainerRegistry
from cura.CuraPackageManager import CuraPackageManager
self._package_manager_class = CuraPackageManager
# Adds command line options to the command line parser. This should be called after the application is created and
# before the pre-start.

View file

@ -7,8 +7,11 @@ from UM.Resources import Resources #To find storage paths for some resource type
class CuraPackageManager(PackageManager):
def __init__(self, parent = None):
super().__init__(parent)
def __init__(self, application, parent = None):
super().__init__(application, parent)
def initialize(self):
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)
super().initialize()

View file

@ -8,6 +8,7 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection
from UM.Scene.SceneNodeSettings import SceneNodeSettings
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
@ -80,6 +81,10 @@ class PlatformPhysics:
# only push away objects if this node is a printing mesh
if not node.callDecoration("isNonPrintingMesh") and Application.getInstance().getPreferences().getValue("physics/automatic_push_free"):
# Do not move locked nodes
if node.getSetting(SceneNodeSettings.LockPosition):
continue
# Check for collisions between convex hulls
for other_node in BreadthFirstIterator(root):
# Ignore root, ourselves and anything that is not a normal SceneNode.

View file

@ -42,6 +42,7 @@ class ContainerManager(QObject):
self._container_registry = self._application.getContainerRegistry()
self._machine_manager = self._application.getMachineManager()
self._material_manager = self._application.getMaterialManager()
self._quality_manager = self._application.getQualityManager()
self._container_name_filters = {}
@pyqtSlot(str, str, result=str)
@ -313,10 +314,18 @@ class ContainerManager(QObject):
self._machine_manager.blurSettings.emit()
global_stack = self._machine_manager.activeMachine
current_quality_changes_name = global_stack.qualityChanges.getName()
current_quality_type = global_stack.quality.getMetaDataEntry("quality_type")
extruder_stacks = list(global_stack.extruders.values())
for stack in [global_stack] + extruder_stacks:
# Find the quality_changes container for this stack and merge the contents of the top container into it.
quality_changes = stack.qualityChanges
if quality_changes.getId() == "empty_quality_changes":
quality_changes = self._quality_manager._createQualityChanges(current_quality_type, current_quality_changes_name,
global_stack, stack)
stack.qualityChanges = quality_changes
if not quality_changes or self._container_registry.isReadOnly(quality_changes.getId()):
Logger.log("e", "Could not update quality of a nonexistant or read only quality profile in stack %s", stack.getId())
continue

View file

@ -501,15 +501,6 @@ class ExtruderManager(QObject):
def getInstanceExtruderValues(self, key):
return ExtruderManager.getExtruderValues(key)
## Updates the material container to a material that matches the material diameter set for the printer
def updateMaterialForDiameter(self, extruder_position: int, global_stack = None):
if not global_stack:
global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack:
return
Application.getInstance().getMachineManager()._updateMaterialWithVariant(extruder_position)
## Get the value for a setting from a specific extruder.
#
# This is exposed to SettingFunction to use in value functions.

View file

@ -306,6 +306,11 @@ class MachineManager(QObject):
for position, extruder in global_stack.extruders.items():
material_dict[position] = extruder.material.getMetaDataEntry("base_file")
self._current_root_material_id = material_dict
# Update materials to make sure that the diameters match with the machine's
for position in global_stack.extruders:
self.updateMaterialWithVariant(position)
global_quality = global_stack.quality
quality_type = global_quality.getMetaDataEntry("quality_type")
global_quality_changes = global_stack.qualityChanges
@ -1200,7 +1205,7 @@ class MachineManager(QObject):
current_quality_type, quality_type)
self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True)
def _updateMaterialWithVariant(self, position: Optional[str]):
def updateMaterialWithVariant(self, position: Optional[str]):
if self._global_container_stack is None:
return
if position is None:
@ -1286,7 +1291,7 @@ class MachineManager(QObject):
self._setMaterial(position, material_container_node)
else:
self._global_container_stack.extruders[position].material = self._empty_material_container
self._updateMaterialWithVariant(position)
self.updateMaterialWithVariant(position)
if configuration.buildplateConfiguration is not None:
global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration)
@ -1332,7 +1337,7 @@ class MachineManager(QObject):
self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setGlobalVariant(container_node)
self._updateMaterialWithVariant(None) # Update all materials
self.updateMaterialWithVariant(None) # Update all materials
self._updateQualityWithMaterial()
@pyqtSlot(str, str)
@ -1369,7 +1374,7 @@ class MachineManager(QObject):
self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._setVariantNode(position, container_node)
self._updateMaterialWithVariant(position)
self.updateMaterialWithVariant(position)
self._updateQualityWithMaterial()
# See if we need to show the Discard or Keep changes screen
@ -1433,5 +1438,5 @@ class MachineManager(QObject):
if self._global_container_stack is None:
return
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self._updateMaterialWithVariant(None)
self.updateMaterialWithVariant(None)
self._updateQualityWithMaterial()

View file

@ -9,6 +9,7 @@ from UM.Signal import Signal, signalemitter
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Logger import Logger
from UM.Util import parseBool
from UM.Application import Application
@ -39,7 +40,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
user_container = InstanceContainer(container_id = self._generateUniqueName())
user_container.addMetaDataEntry("type", "user")
self._stack.userChanges = user_container
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId()
self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0)
self._is_non_printing_mesh = False
self._is_non_thumbnail_visible_mesh = False
@ -48,13 +49,25 @@ class SettingOverrideDecorator(SceneNodeDecorator):
Application.getInstance().getContainerRegistry().addContainer(self._stack)
Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack)
Application.getInstance().globalContainerStackChanged.connect(self._onNumberOfExtrudersEnabledChanged)
Application.getInstance().getMachineManager().numberExtrudersEnabledChanged.connect(self._onNumberOfExtrudersEnabledChanged)
self.activeExtruderChanged.connect(self._updateNextStack)
self._updateNextStack()
def _generateUniqueName(self):
return "SettingOverrideInstanceContainer-%s" % uuid.uuid1()
def _onNumberOfExtrudersEnabledChanged(self, *args, **kwargs):
if not parseBool(self._extruder_stack.getMetaDataEntry("enabled", "True")):
# switch to the first extruder that's available
global_stack = Application.getInstance().getMachineManager().activeMachine
for _, extruder in sorted(list(global_stack.extruders.items())):
if parseBool(extruder.getMetaDataEntry("enabled", "True")):
self._extruder_stack = extruder
self._updateNextStack()
break
def __deepcopy__(self, memo):
## Create a fresh decorator object
deep_copy = SettingOverrideDecorator()
@ -63,13 +76,13 @@ class SettingOverrideDecorator(SceneNodeDecorator):
instance_container = copy.deepcopy(self._stack.getContainer(0), memo)
# A unique name must be added, or replaceContainer will not replace it
instance_container.setMetaDataEntry("id", self._generateUniqueName)
instance_container.setMetaDataEntry("id", self._generateUniqueName())
## Set the copied instance as the first (and only) instance container of the stack.
deep_copy._stack.replaceContainer(0, instance_container)
# Properly set the right extruder on the copy
deep_copy.setActiveExtruder(self._extruder_stack)
deep_copy.setActiveExtruder(self._extruder_stack.getId())
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
# has not been updated yet.
@ -82,7 +95,7 @@ class SettingOverrideDecorator(SceneNodeDecorator):
#
# \return An extruder's container stack.
def getActiveExtruder(self):
return self._extruder_stack
return self._extruder_stack.getId()
## Gets the signal that emits if the active extruder changed.
#
@ -124,20 +137,16 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# kept up to date.
def _updateNextStack(self):
if self._extruder_stack:
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack)
if extruder_stack:
if self._stack.getNextStack():
old_extruder_stack_id = self._stack.getNextStack().getId()
else:
old_extruder_stack_id = ""
self._stack.setNextStack(extruder_stack[0])
# Trigger slice/need slicing if the extruder changed.
if self._stack.getNextStack().getId() != old_extruder_stack_id:
Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle()
if self._stack.getNextStack():
old_extruder_stack_id = self._stack.getNextStack().getId()
else:
Logger.log("e", "Extruder stack %s below per-object settings does not exist.", self._extruder_stack)
old_extruder_stack_id = ""
self._stack.setNextStack(self._extruder_stack)
# Trigger slice/need slicing if the extruder changed.
if self._stack.getNextStack().getId() != old_extruder_stack_id:
Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle()
else:
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
@ -145,7 +154,14 @@ class SettingOverrideDecorator(SceneNodeDecorator):
#
# \param extruder_stack_id The new extruder stack to print with.
def setActiveExtruder(self, extruder_stack_id):
self._extruder_stack = extruder_stack_id
if self._extruder_stack.getId() == extruder_stack_id:
return
global_stack = Application.getInstance().getMachineManager().activeMachine
for extruder in global_stack.extruders.values():
if extruder.getId() == extruder_stack_id:
self._extruder_stack = extruder
break
self._updateNextStack()
ExtruderManager.getInstance().resetSelectedObjectExtruders()
self.activeExtruderChanged.emit()

View file

@ -55,7 +55,7 @@ class ChangeLog(Extension, QObject,):
def loadChangeLogs(self):
self._change_logs = collections.OrderedDict()
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r",-1, "utf-8") as f:
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r", encoding = "utf-8") as f:
open_version = None
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
for line in f:

View file

@ -57,7 +57,7 @@ class GCodeProfileReader(ProfileReader):
# TODO: Consider moving settings to the start?
serialized = "" # Will be filled with the serialized profile.
try:
with open(file_name, "r") as f:
with open(file_name, "r", encoding = "utf-8") as f:
for line in f:
if line.startswith(prefix):
# Remove the prefix and the newline from the line and add it to the rest.

View file

@ -100,7 +100,7 @@ class LegacyProfileReader(ProfileReader):
return None
try:
with open(os.path.join(PluginRegistry.getInstance().getPluginPath("LegacyProfileReader"), "DictionaryOfDoom.json"), "r", -1, "utf-8") as f:
with open(os.path.join(PluginRegistry.getInstance().getPluginPath("LegacyProfileReader"), "DictionaryOfDoom.json"), "r", encoding = "utf-8") as f:
dict_of_doom = json.load(f) # Parse the Dictionary of Doom.
except IOError as e:
Logger.log("e", "Could not open DictionaryOfDoom.json for reading: %s", str(e))

View file

@ -158,4 +158,4 @@ class MachineSettingsAction(MachineAction):
@pyqtSlot(int)
def updateMaterialForDiameter(self, extruder_position: int):
# Updates the material container to a material that matches the material diameter set for the printer
self._application.getExtruderManager().updateMaterialForDiameter(extruder_position)
self._application.getMachineManager().updateMaterialWithVariant(extruder_position)

View file

@ -13,7 +13,7 @@ def readHex(filename):
"""
data = []
extra_addr = 0
f = io.open(filename, "r")
f = io.open(filename, "r", encoding = "utf-8")
for line in f:
line = line.strip()
if len(line) < 1:

View file

@ -94,7 +94,7 @@ class VersionUpgrade22to24(VersionUpgrade):
if variant_path.endswith("_variant.inst.cfg"):
variant_path = variant_path[:-len("_variant.inst.cfg")] + "_settings.inst.cfg"
with open(os.path.join(machine_instances_dir, os.path.basename(variant_path)), "w") as fp:
with open(os.path.join(machine_instances_dir, os.path.basename(variant_path)), "w", encoding = "utf-8") as fp:
variant_config.write(fp)
return config_name
@ -105,9 +105,9 @@ class VersionUpgrade22to24(VersionUpgrade):
result = []
for entry in os.scandir(variants_dir):
if entry.name.endswith('.inst.cfg') and entry.is_file():
if entry.name.endswith(".inst.cfg") and entry.is_file():
config = configparser.ConfigParser(interpolation = None)
with open(entry.path, "r") as fhandle:
with open(entry.path, "r", encoding = "utf-8") as fhandle:
config.read_file(fhandle)
if config.has_section("general") and config.has_option("general", "name"):
result.append( { "path": entry.path, "name": config.get("general", "name") } )

View file

@ -249,11 +249,11 @@ class VersionUpgrade25to26(VersionUpgrade):
definition_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.DefinitionChangesContainer)
user_settings_dir = Resources.getPath(CuraApplication.ResourceTypes.UserInstanceContainer)
with open(os.path.join(definition_changes_dir, definition_changes_filename), "w") as f:
with open(os.path.join(definition_changes_dir, definition_changes_filename), "w", encoding = "utf-8") as f:
f.write(definition_changes_output.getvalue())
with open(os.path.join(user_settings_dir, user_settings_filename), "w") as f:
with open(os.path.join(user_settings_dir, user_settings_filename), "w", encoding = "utf-8") as f:
f.write(user_settings_output.getvalue())
with open(os.path.join(extruder_stack_dir, extruder_filename), "w") as f:
with open(os.path.join(extruder_stack_dir, extruder_filename), "w", encoding = "utf-8") as f:
f.write(extruder_output.getvalue())
## Creates a definition changes container which doesn't contain anything for the Custom FDM Printers.

View file

@ -1018,7 +1018,7 @@ class XmlMaterialProfile(InstanceContainer):
@classmethod
def getProductIdMap(cls) -> Dict[str, List[str]]:
product_to_id_file = os.path.join(os.path.dirname(sys.modules[cls.__module__].__file__), "product_to_id.json")
with open(product_to_id_file) as f:
with open(product_to_id_file, encoding = "utf-8") as f:
product_to_id_map = json.load(f)
product_to_id_map = {key: [value] for key, value in product_to_id_map.items()}
return product_to_id_map

View file

@ -170,7 +170,7 @@ UM.PreferencesPage
append({ text: "日本語", code: "ja_JP" })
append({ text: "한국어", code: "ko_KR" })
append({ text: "Nederlands", code: "nl_NL" })
append({ text: "Polski", code: "pl_PL" })
//Polish is disabled for being incomplete: append({ text: "Polski", code: "pl_PL" })
append({ text: "Português do Brasil", code: "pt_BR" })
append({ text: "Português", code: "pt_PT" })
append({ text: "Русский", code: "ru_RU" })

View file

@ -19,7 +19,7 @@ import pytest
def test_ultimaker3extended_variants(um3_file, um3e_file):
directory = os.path.join(os.path.dirname(__file__), "..", "resources", "variants") #TODO: Hardcoded path relative to this test file.
um3 = configparser.ConfigParser()
um3.read_file(open(os.path.join(directory, um3_file)))
um3.read_file(open(os.path.join(directory, um3_file), encoding = "utf-8"))
um3e = configparser.ConfigParser()
um3e.read_file(open(os.path.join(directory, um3e_file)))
um3e.read_file(open(os.path.join(directory, um3e_file), encoding = "utf-8"))
assert um3["values"] == um3e["values"]