Browse Source

Merge branch 'master' into feature_xmlmaterials_cura_settings

fieldOfView 7 years ago
parent
commit
a85c42c246

+ 4 - 0
.dockerignore

@@ -0,0 +1,4 @@
+.git
+.github
+resources/materials
+CuraEngine

+ 4 - 1
.github/ISSUE_TEMPLATE.md

@@ -6,7 +6,7 @@ Before filing, PLEASE check if the issue already exists (either open or closed)
 Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write thigns like "Request:" or "[BUG]" in the title; this is what labels are for.
 
 It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker.
-Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
+Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that github accepts uploading the file. Otherwise we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
 
 Thank you for using Cura!
 -->
@@ -26,6 +26,9 @@ Thank you for using Cura!
 **Display Driver**
 <!--  Video driver name and version -->
 
+**Printer**
+<!-- Which printer was selected in Cura. Please attach project file as .curaproject.3mf.zip -->
+
 **Steps to Reproduce**
 <!-- Add the steps needed that lead up to the issue (replace this text) -->
 

+ 45 - 0
Dockerfile

@@ -0,0 +1,45 @@
+FROM ultimaker/cura-build-environment:1
+
+# Environment vars for easy configuration
+ENV CURA_APP_DIR=/srv/cura
+
+# Ensure our sources dir exists
+RUN mkdir $CURA_APP_DIR
+
+# Setup CuraEngine
+ENV CURA_ENGINE_BRANCH=master
+WORKDIR $CURA_APP_DIR
+RUN git clone -b $CURA_ENGINE_BRANCH --depth 1 https://github.com/Ultimaker/CuraEngine
+WORKDIR $CURA_APP_DIR/CuraEngine
+RUN mkdir build
+WORKDIR $CURA_APP_DIR/CuraEngine/build
+RUN cmake3 ..
+RUN make
+RUN make install
+
+# TODO: setup libCharon
+
+# Setup Uranium
+ENV URANIUM_BRANCH=master
+WORKDIR $CURA_APP_DIR
+RUN git clone -b $URANIUM_BRANCH --depth 1 https://github.com/Ultimaker/Uranium
+
+# Setup materials
+ENV MATERIALS_BRANCH=master
+WORKDIR $CURA_APP_DIR
+RUN git clone -b $MATERIALS_BRANCH --depth 1 https://github.com/Ultimaker/fdm_materials materials
+
+# Setup Cura
+WORKDIR $CURA_APP_DIR/Cura
+ADD . .
+RUN mv $CURA_APP_DIR/materials resources/materials
+
+# Make sure Cura can find CuraEngine
+RUN ln -s /usr/local/bin/CuraEngine $CURA_APP_DIR/Cura
+
+# Run Cura
+WORKDIR $CURA_APP_DIR/Cura
+ENV PYTHONPATH=${PYTHONPATH}:$CURA_APP_DIR/Uranium
+RUN chmod +x ./CuraEngine
+RUN chmod +x ./run_in_docker.sh
+CMD "./run_in_docker.sh"

+ 37 - 33
cura/BuildVolume.py

@@ -111,6 +111,9 @@ class BuildVolume(SceneNode):
         # but it does not update the disallowed areas after material change
         Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
 
+        # Enable and disable extruder
+        Application.getInstance().getMachineManager().extruderChanged.connect(self.updateNodeBoundaryCheck)
+
         # list of settings which were updated
         self._changed_settings_since_last_rebuild = []
 
@@ -133,6 +136,7 @@ class BuildVolume(SceneNode):
                 if active_extruder_changed is not None:
                     node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
                 node.decoratorsChanged.disconnect(self._updateNodeListeners)
+            self._updateDisallowedAreasAndRebuild()  # make sure we didn't miss anything before we updated the node listeners
 
             self._scene_objects = new_scene_objects
             self._onSettingPropertyChanged("print_sequence", "value")  # Create fake event, so right settings are triggered.
@@ -147,7 +151,6 @@ class BuildVolume(SceneNode):
         active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal")
         if active_extruder_changed is not None:
             active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild)
-            self._updateDisallowedAreasAndRebuild()
 
     def setWidth(self, width):
         if width is not None:
@@ -217,30 +220,35 @@ class BuildVolume(SceneNode):
                 group_nodes.append(node)  # Keep list of affected group_nodes
 
             if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
-                node._outside_buildarea = False
-                bbox = node.getBoundingBox()
+                if node.collidesWithBbox(build_volume_bounding_box):
+                    node.setOutsideBuildArea(True)
+                    continue
 
-                # Mark the node as outside the build volume if the bounding box test fails.
-                if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
-                    node._outside_buildarea = True
+                if node.collidesWithArea(self.getDisallowedAreas()):
+                    node.setOutsideBuildArea(True)
                     continue
 
-                convex_hull = node.callDecoration("getConvexHull")
-                if convex_hull:
-                    if not convex_hull.isValid():
-                        return
-                    # Check for collisions between disallowed areas and the object
-                    for area in self.getDisallowedAreas():
-                        overlap = convex_hull.intersectsPolygon(area)
-                        if overlap is None:
-                            continue
-                        node._outside_buildarea = True
-                        continue
+                # Mark the node as outside build volume if the set extruder is disabled
+                extruder_position = node.callDecoration("getActiveExtruderPosition")
+                if not self._global_container_stack.extruders[extruder_position].isEnabled:
+                    node.setOutsideBuildArea(True)
+                    continue
+
+                node.setOutsideBuildArea(False)
 
         # Group nodes should override the _outside_buildarea property of their children.
         for group_node in group_nodes:
-            for child_node in group_node.getAllChildren():
-                child_node._outside_buildarea = group_node._outside_buildarea
+            children = group_node.getAllChildren()
+
+            # Check if one or more children are non-printable and if so, set the parent as non-printable:
+            for child_node in children:
+                if child_node.isOutsideBuildArea():
+                    group_node.setOutsideBuildArea(True)
+                    break
+
+            # Apply results of the check to all children of the group:
+            for child_node in children:
+                child_node.setOutsideBuildArea(group_node.isOutsideBuildArea())
 
     ##  Update the outsideBuildArea of a single node, given bounds or current build volume
     def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None):
@@ -260,24 +268,20 @@ class BuildVolume(SceneNode):
             build_volume_bounding_box = bounds
 
         if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
-            bbox = node.getBoundingBox()
+            if node.collidesWithBbox(build_volume_bounding_box):
+                node.setOutsideBuildArea(True)
+                return
 
-            # Mark the node as outside the build volume if the bounding box test fails.
-            if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
+            if node.collidesWithArea(self.getDisallowedAreas()):
+                node.setOutsideBuildArea(True)
+                return
+
+            # Mark the node as outside build volume if the set extruder is disabled
+            extruder_position = node.callDecoration("getActiveExtruderPosition")
+            if not self._global_container_stack.extruders[extruder_position].isEnabled:
                 node.setOutsideBuildArea(True)
                 return
 
-            convex_hull = self.callDecoration("getConvexHull")
-            if convex_hull:
-                if not convex_hull.isValid():
-                    return
-                # Check for collisions between disallowed areas and the object
-                for area in self.getDisallowedAreas():
-                    overlap = convex_hull.intersectsPolygon(area)
-                    if overlap is None:
-                        continue
-                    node.setOutsideBuildArea(True)
-                    return
             node.setOutsideBuildArea(False)
 
     ##  Recalculates the build volume & disallowed areas.

+ 8 - 7
cura/CrashHandler.py

@@ -1,4 +1,4 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 import platform
@@ -14,10 +14,11 @@ import urllib.request
 import urllib.error
 import shutil
 
-from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
+from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QUrl
 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
 from PyQt5.QtGui import QDesktopServices
 
+from UM.Resources import Resources
 from UM.Application import Application
 from UM.Logger import Logger
 from UM.View.GL.OpenGL import OpenGL
@@ -84,14 +85,14 @@ class CrashHandler:
         dialog = QDialog()
         dialog.setMinimumWidth(500)
         dialog.setMinimumHeight(170)
-        dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura Crashed"))
+        dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura can't startup"))
         dialog.finished.connect(self._closeEarlyCrashDialog)
 
         layout = QVBoxLayout(dialog)
 
         label = QLabel()
-        label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred.</p></b>
-                    <p>Unfortunately, Cura encountered an unrecoverable error during start up. It was possibly caused by some incorrect configuration files. We suggest to backup and reset your configuration.</p>
+        label.setText(catalog.i18nc("@label crash message", """<p><b>Oops, Ultimaker Cura has encountered something that doesn't seem right.</p></b>
+                    <p>We encountered an unrecoverable error during start up. It was possibly caused by some incorrect configuration files. We suggest to backup and reset your configuration.</p>
                     <p>Backups can be found in the configuration folder.</p>
                     <p>Please send us this Crash Report to fix the problem.</p>
                 """))
@@ -219,7 +220,7 @@ class CrashHandler:
 
     def _messageWidget(self):
         label = QLabel()
-        label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b>
+        label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred in Cura. Please send us this Crash Report to fix the problem</p></b>
             <p>Please use the "Send report" button to post a bug report automatically to our servers</p>
         """))
 
@@ -258,7 +259,7 @@ class CrashHandler:
         opengl_instance = OpenGL.getInstance()
         if not opengl_instance:
             self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
-            return catalog.i18nc("@label", "not yet initialised<br/>")
+            return catalog.i18nc("@label", "Not yet initialized<br/>")
 
         info = "<ul>"
         info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())

+ 4 - 4
cura/CuraActions.py

@@ -109,10 +109,6 @@ class CuraActions(QObject):
 
         nodes_to_change = []
         for node in Selection.getAllSelectedObjects():
-            # Do not change any nodes that already have the right extruder set.
-            if node.callDecoration("getActiveExtruder") == extruder_id:
-                continue
-
             # If the node is a group, apply the active extruder to all children of the group.
             if node.callDecoration("isGroup"):
                 for grouped_node in BreadthFirstIterator(node):
@@ -125,6 +121,10 @@ class CuraActions(QObject):
                     nodes_to_change.append(grouped_node)
                 continue
 
+            # Do not change any nodes that already have the right extruder set.
+            if node.callDecoration("getActiveExtruder") == extruder_id:
+                continue
+
             nodes_to_change.append(node)
 
         if not nodes_to_change:

+ 143 - 139
cura/CuraApplication.py

@@ -1,9 +1,7 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
-#Type hinting.
-from typing import Dict
 
-from PyQt5.QtCore import QObject
+from PyQt5.QtCore import QObject, QTimer
 from PyQt5.QtNetwork import QLocalServer
 from PyQt5.QtNetwork import QLocalSocket
 
@@ -59,18 +57,22 @@ from cura.Machines.Models.BuildPlateModel import BuildPlateModel
 from cura.Machines.Models.NozzleModel import NozzleModel
 from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
 from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
-
 from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
-
 from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
 from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
 from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel
+from cura.Machines.Models.QualityManagementModel import QualityManagementModel
+from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
+from cura.Machines.Models.MachineManagementModel import MachineManagementModel
+
+from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
+
+from cura.Machines.MachineErrorChecker import MachineErrorChecker
 
 from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
 from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
 
 from cura.Machines.VariantManager import VariantManager
-from cura.Machines.Models.QualityManagementModel import QualityManagementModel
 
 from . import PlatformPhysics
 from . import BuildVolume
@@ -87,7 +89,6 @@ from cura.Settings.ExtruderManager import ExtruderManager
 from cura.Settings.UserChangesModel import UserChangesModel
 from cura.Settings.ExtrudersModel import ExtrudersModel
 from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
-from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
 from cura.Settings.ContainerManager import ContainerManager
 
 from cura.ObjectsModel import ObjectsModel
@@ -98,7 +99,6 @@ from PyQt5.QtGui import QColor, QIcon
 from PyQt5.QtWidgets import QMessageBox
 from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
 
-from configparser import ConfigParser
 import sys
 import os.path
 import numpy
@@ -138,15 +138,10 @@ class CuraApplication(QtApplication):
         MachineStack = Resources.UserType + 7
         ExtruderStack = Resources.UserType + 8
         DefinitionChangesContainer = Resources.UserType + 9
+        SettingVisibilityPreset = Resources.UserType + 10
 
     Q_ENUMS(ResourceTypes)
 
-    # FIXME: This signal belongs to the MachineManager, but the CuraEngineBackend plugin requires on it.
-    #        Because plugins are initialized before the ContainerRegistry, putting this signal in MachineManager
-    #        will make it initialized before ContainerRegistry does, and it won't find the active machine, thus
-    #        Cura will always show the Add Machine Dialog upon start.
-    stacksValidationFinished = pyqtSignal()  # Emitted whenever a validation is finished
-
     def __init__(self, **kwargs):
         # this list of dir names will be used by UM to detect an old cura directory
         for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
@@ -191,6 +186,7 @@ class CuraApplication(QtApplication):
         Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
         Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
         Resources.addStorageType(self.ResourceTypes.DefinitionChangesContainer, "definition_changes")
+        Resources.addStorageType(self.ResourceTypes.SettingVisibilityPreset, "setting_visibility")
 
         ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality")
         ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer, "quality_changes")
@@ -208,10 +204,10 @@ class CuraApplication(QtApplication):
         UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
             {
                 ("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion):    (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
-                ("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
-                ("extruder_train", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
-                ("preferences", Preferences.Version * 1000000 + self.SettingVersion):               (Resources.Preferences, "application/x-uranium-preferences"),
-                ("user", InstanceContainer.Version * 1000000 + self.SettingVersion):       (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
+                ("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion):         (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
+                ("extruder_train", ContainerStack.Version * 1000000 + self.SettingVersion):        (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
+                ("preferences", Preferences.Version * 1000000 + self.SettingVersion):              (Resources.Preferences, "application/x-uranium-preferences"),
+                ("user", InstanceContainer.Version * 1000000 + self.SettingVersion):               (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
                 ("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
             }
         )
@@ -223,12 +219,15 @@ class CuraApplication(QtApplication):
         self._machine_manager = None    # This is initialized on demand.
         self._extruder_manager = None
         self._material_manager = None
+        self._quality_manager = None
         self._object_manager = None
         self._build_plate_model = None
         self._multi_build_plate_model = None
+        self._setting_visibility_presets_model = None
         self._setting_inheritance_manager = None
         self._simple_mode_settings_manager = None
         self._cura_scene_controller = None
+        self._machine_error_checker = None
 
         self._additional_components = {} # Components to add to certain areas in the interface
 
@@ -285,10 +284,15 @@ class CuraApplication(QtApplication):
         self._preferred_mimetype = ""
         self._i18n_catalog = i18nCatalog("cura")
 
-        self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
+        self._update_platform_activity_timer = QTimer()
+        self._update_platform_activity_timer.setInterval(500)
+        self._update_platform_activity_timer.setSingleShot(True)
+        self._update_platform_activity_timer.timeout.connect(self.updatePlatformActivity)
+
+        self.getController().getScene().sceneChanged.connect(self.updatePlatformActivityDelayed)
         self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
         self.getController().contextMenuRequested.connect(self._onContextMenuRequested)
-        self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity)
+        self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivityDelayed)
 
         Resources.addType(self.ResourceTypes.QmlFiles, "qml")
         Resources.addType(self.ResourceTypes.Firmware, "firmware")
@@ -375,20 +379,6 @@ class CuraApplication(QtApplication):
 
         preferences.setDefault("local_file/last_used_type", "text/x-gcode")
 
-        setting_visibily_preset_names = self.getVisibilitySettingPresetTypes()
-        preferences.setDefault("general/visible_settings_preset", setting_visibily_preset_names)
-
-        preset_setting_visibility_choice = Preferences.getInstance().getValue("general/preset_setting_visibility_choice")
-
-        default_preset_visibility_group_name = "Basic"
-        if preset_setting_visibility_choice == "" or preset_setting_visibility_choice is None:
-            if preset_setting_visibility_choice not in setting_visibily_preset_names:
-                preset_setting_visibility_choice = default_preset_visibility_group_name
-
-        visible_settings = self.getVisibilitySettingPreset(settings_preset_name = preset_setting_visibility_choice)
-        preferences.setDefault("general/visible_settings", visible_settings)
-        preferences.setDefault("general/preset_setting_visibility_choice", preset_setting_visibility_choice)
-
         self.applicationShuttingDown.connect(self.saveSettings)
         self.engineCreatedSignal.connect(self._onEngineCreated)
 
@@ -404,91 +394,6 @@ class CuraApplication(QtApplication):
 
         CuraApplication.Created = True
 
-    @pyqtSlot(str, result = str)
-    def getVisibilitySettingPreset(self, settings_preset_name) -> str:
-        result = self._loadPresetSettingVisibilityGroup(settings_preset_name)
-        formatted_preset_settings = self._serializePresetSettingVisibilityData(result)
-
-        return formatted_preset_settings
-
-    ## Serialise the given preset setting visibitlity group dictionary into a string which is concatenated by ";"
-    #
-    def _serializePresetSettingVisibilityData(self, settings_data: dict) -> str:
-        result_string = ""
-
-        for key in settings_data:
-            result_string += key + ";"
-            for value in settings_data[key]:
-                result_string += value + ";"
-
-        return result_string
-
-    ## Load the preset setting visibility group with the given name
-    #
-    def _loadPresetSettingVisibilityGroup(self, visibility_preset_name) -> Dict[str, str]:
-        preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
-
-        result = {}
-        right_preset_found = False
-
-        for item in os.listdir(preset_dir):
-            file_path = os.path.join(preset_dir, item)
-            if not os.path.isfile(file_path):
-                continue
-
-            parser = ConfigParser(allow_no_value = True)  # accept options without any value,
-
-            try:
-                parser.read([file_path])
-
-                if not parser.has_option("general", "name"):
-                    continue
-
-                if parser["general"]["name"] == visibility_preset_name:
-                    right_preset_found = True
-                    for section in parser.sections():
-                        if section == 'general':
-                            continue
-                        else:
-                            section_settings = []
-                            for option in parser[section].keys():
-                                section_settings.append(option)
-
-                            result[section] = section_settings
-
-                if right_preset_found:
-                    break
-
-            except Exception as e:
-                Logger.log("e", "Failed to load setting visibility preset %s: %s", file_path, str(e))
-
-        return result
-
-    ## Check visibility setting preset folder and returns available types
-    #
-    def getVisibilitySettingPresetTypes(self):
-        preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
-        result = {}
-
-        for item in os.listdir(preset_dir):
-            file_path = os.path.join(preset_dir, item)
-            if not os.path.isfile(file_path):
-                continue
-
-            parser = ConfigParser(allow_no_value=True)  # accept options without any value,
-
-            try:
-                parser.read([file_path])
-
-                if not parser.has_option("general", "name") and not parser.has_option("general", "weight"):
-                    continue
-
-                result[parser["general"]["weight"]] = parser["general"]["name"]
-
-            except Exception as e:
-                Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e))
-
-        return result
 
     def _onEngineCreated(self):
         self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
@@ -546,27 +451,18 @@ class CuraApplication(QtApplication):
 
     @pyqtSlot(str)
     def discardOrKeepProfileChangesClosed(self, option):
+        global_stack = self.getGlobalContainerStack()
         if option == "discard":
-            global_stack = self.getGlobalContainerStack()
-            for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()):
-                extruder.getTop().clear()
-            global_stack.getTop().clear()
+            for extruder in global_stack.extruders.values():
+                extruder.userChanges.clear()
+            global_stack.userChanges.clear()
 
         # if the user decided to keep settings then the user settings should be re-calculated and validated for errors
         # before slicing. To ensure that slicer uses right settings values
         elif option == "keep":
-            global_stack = self.getGlobalContainerStack()
-            for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()):
-                user_extruder_container = extruder.getTop()
-                if user_extruder_container:
-                    user_extruder_container.update()
-
-            user_global_container = global_stack.getTop()
-            if user_global_container:
-                user_global_container.update()
-
-        # notify listeners that quality has changed (after user selected discard or keep)
-        self.getMachineManager().activeQualityChanged.emit()
+            for extruder in global_stack.extruders.values():
+                extruder.userChanges.update()
+            global_stack.userChanges.update()
 
     @pyqtSlot(int)
     def messageBoxClosed(self, button):
@@ -602,8 +498,13 @@ class CuraApplication(QtApplication):
     def getStaticVersion(cls):
         return CuraVersion
 
+    ##  Handle removing the unneeded plugins
+    #   \sa PluginRegistry
+    def _removePlugins(self):
+        self._plugin_registry.removePlugins()
+
     ##  Handle loading of all plugin types (and the backend explicitly)
-    #   \sa PluginRegistery
+    #   \sa PluginRegistry
     def _loadPlugins(self):
         self._plugin_registry.addType("profile_reader", self._addProfileReader)
         self._plugin_registry.addType("profile_writer", self._addProfileWriter)
@@ -742,19 +643,28 @@ class CuraApplication(QtApplication):
         self.preRun()
 
         container_registry = ContainerRegistry.getInstance()
+
+        Logger.log("i", "Initializing variant manager")
         self._variant_manager = VariantManager(container_registry)
         self._variant_manager.initialize()
 
+        Logger.log("i", "Initializing material manager")
         from cura.Machines.MaterialManager import MaterialManager
         self._material_manager = MaterialManager(container_registry, parent = self)
         self._material_manager.initialize()
 
+        Logger.log("i", "Initializing quality manager")
         from cura.Machines.QualityManager import QualityManager
         self._quality_manager = QualityManager(container_registry, parent = self)
         self._quality_manager.initialize()
 
+        Logger.log("i", "Initializing machine manager")
         self._machine_manager = MachineManager(self)
 
+        Logger.log("i", "Initializing machine error checker")
+        self._machine_error_checker = MachineErrorChecker(self)
+        self._machine_error_checker.initialize()
+
         # Check if we should run as single instance or not
         self._setUpSingleInstanceServer()
 
@@ -767,6 +677,11 @@ class CuraApplication(QtApplication):
         self._print_information = PrintInformation.PrintInformation()
         self._cura_actions = CuraActions.CuraActions(self)
 
+        # Initialize setting visibility presets model
+        self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self)
+        default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
+        Preferences.getInstance().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
+
         # Detect in which mode to run and execute that mode
         if self.getCommandLineOption("headless", False):
             self.runWithoutGUI()
@@ -780,8 +695,11 @@ class CuraApplication(QtApplication):
             self._openFile(file_name)
 
         self.started = True
+        self.initializationFinished.emit()
         self.exec_()
 
+    initializationFinished = pyqtSignal()
+
     ##  Run Cura without GUI elements and interaction (server mode).
     def runWithoutGUI(self):
         self._use_gui = False
@@ -846,6 +764,13 @@ class CuraApplication(QtApplication):
     def hasGui(self):
         return self._use_gui
 
+    @pyqtSlot(result = QObject)
+    def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
+        return self._setting_visibility_presets_model
+
+    def getMachineErrorChecker(self, *args) -> MachineErrorChecker:
+        return self._machine_error_checker
+
     def getMachineManager(self, *args) -> MachineManager:
         if self._machine_manager is None:
             self._machine_manager = MachineManager(self)
@@ -960,6 +885,7 @@ class CuraApplication(QtApplication):
         qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel")
         qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
         qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
+        qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
 
         qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
                                  "QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
@@ -968,6 +894,7 @@ class CuraApplication(QtApplication):
         qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
 
         qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
+        qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
         qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
         qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
         qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
@@ -1047,6 +974,10 @@ class CuraApplication(QtApplication):
     def getSceneBoundingBoxString(self):
         return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
 
+    def updatePlatformActivityDelayed(self, node = None):
+        if node is not None and node.getMeshData() is not None:
+            self._update_platform_activity_timer.start()
+
     ##  Update scene bounding box for current build plate
     def updatePlatformActivity(self, node = None):
         count = 0
@@ -1179,7 +1110,7 @@ class CuraApplication(QtApplication):
                 continue
             if not node.getMeshData() and not node.callDecoration("isGroup"):
                 continue  # Node that doesnt have a mesh and is not a group.
-            if node.getParent() and node.getParent().callDecoration("isGroup"):
+            if node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent().callDecoration("isSliceable"):
                 continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
             if not node.isSelectable():
                 continue  # i.e. node with layer data
@@ -1354,8 +1285,11 @@ class CuraApplication(QtApplication):
     def reloadAll(self):
         Logger.log("i", "Reloading all loaded mesh data.")
         nodes = []
+        has_merged_nodes = False
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
-            if not isinstance(node, CuraSceneNode) or not node.getMeshData():
+            if not isinstance(node, CuraSceneNode) or not node.getMeshData() :
+                if node.getName() == 'MergedMesh':
+                    has_merged_nodes = True
                 continue
 
             nodes.append(node)
@@ -1369,10 +1303,14 @@ class CuraApplication(QtApplication):
                 job = ReadMeshJob(file_name)
                 job._node = node
                 job.finished.connect(self._reloadMeshFinished)
+                if has_merged_nodes:
+                    job.finished.connect(self.updateOriginOfMergedMeshes)
+
                 job.start()
             else:
                 Logger.log("w", "Unable to reload data because we don't have a filename.")
 
+
     ##  Get logging data of the backend engine
     #   \returns \type{string} Logging data
     @pyqtSlot(result = str)
@@ -1442,6 +1380,58 @@ class CuraApplication(QtApplication):
 
         # Use the previously found center of the group bounding box as the new location of the group
         group_node.setPosition(group_node.getBoundingBox().center)
+        group_node.setName("MergedMesh") # add a specific name to destinguis this node
+
+
+    ##  Updates origin position of all merged meshes
+    #   \param jobNode \type{Job} empty object which passed which is required by JobQueue
+    def updateOriginOfMergedMeshes(self, jobNode):
+        group_nodes = []
+        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
+            if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
+
+                #checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
+                for decorator in node.getDecorators():
+                    if isinstance(decorator, GroupDecorator):
+                        group_nodes.append(node)
+                        break
+
+        for group_node in group_nodes:
+            meshes = [node.getMeshData() for node in group_node.getAllChildren() if node.getMeshData()]
+
+            # Compute the center of the objects
+            object_centers = []
+            # Forget about the translation that the original objects have
+            zero_translation = Matrix(data=numpy.zeros(3))
+            for mesh, node in zip(meshes, group_node.getChildren()):
+                transformation = node.getLocalTransformation()
+                transformation.setTranslation(zero_translation)
+                transformed_mesh = mesh.getTransformed(transformation)
+                center = transformed_mesh.getCenterPosition()
+                if center is not None:
+                    object_centers.append(center)
+
+            if object_centers and len(object_centers) > 0:
+                middle_x = sum([v.x for v in object_centers]) / len(object_centers)
+                middle_y = sum([v.y for v in object_centers]) / len(object_centers)
+                middle_z = sum([v.z for v in object_centers]) / len(object_centers)
+                offset = Vector(middle_x, middle_y, middle_z)
+            else:
+                offset = Vector(0, 0, 0)
+
+            # Move each node to the same position.
+            for mesh, node in zip(meshes, group_node.getChildren()):
+                transformation = node.getLocalTransformation()
+                transformation.setTranslation(zero_translation)
+                transformed_mesh = mesh.getTransformed(transformation)
+
+                # Align the object around its zero position
+                # and also apply the offset to center it inside the group.
+                node.setPosition(-transformed_mesh.getZeroPosition() - offset)
+
+            # Use the previously found center of the group bounding box as the new location of the group
+            group_node.setPosition(group_node.getBoundingBox().center)
+
 
     @pyqtSlot()
     def groupSelected(self):
@@ -1457,6 +1447,12 @@ class CuraApplication(QtApplication):
         group_node.setPosition(center)
         group_node.setCenterPosition(center)
 
+        # Remove nodes that are directly parented to another selected node from the selection so they remain parented
+        selected_nodes = Selection.getAllSelectedObjects().copy()
+        for node in selected_nodes:
+            if node.getParent() in selected_nodes and not node.getParent().callDecoration("isGroup"):
+                Selection.remove(node)
+
         # Move selected nodes into the group-node
         Selection.applyOperation(SetParentOperation, group_node)
 
@@ -1475,6 +1471,10 @@ class CuraApplication(QtApplication):
                 group_parent = node.getParent()
                 children = node.getChildren().copy()
                 for child in children:
+                    # Ungroup only 1 level deep
+                    if child.getParent() != node:
+                        continue
+
                     # Set the parent of the children to the parent of the group-node
                     op.addOperation(SetParentOperation(child, group_parent))
 
@@ -1604,6 +1604,8 @@ class CuraApplication(QtApplication):
                 fixed_nodes.append(node_)
         arranger = Arrange.create(fixed_nodes = fixed_nodes)
         min_offset = 8
+        default_extruder_position = self.getMachineManager().defaultExtruderPosition
+        default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId()
 
         for original_node in nodes:
 
@@ -1669,6 +1671,8 @@ class CuraApplication(QtApplication):
 
             op = AddSceneNodeOperation(node, scene.getRoot())
             op.push()
+
+            node.callDecoration("setActiveExtruder", default_extruder_id)
             scene.sceneChanged.emit(node)
 
         self.fileCompleted.emit(filename)
@@ -1689,7 +1693,7 @@ class CuraApplication(QtApplication):
             result = workspace_reader.preRead(file_path, show_dialog=False)
             return result == WorkspaceReader.PreReadResult.accepted
         except Exception as e:
-            Logger.log("e", "Could not check file %s: %s", file_url, e)
+            Logger.logException("e", "Could not check file %s: %s", file_url)
             return False
 
     def _onContextMenuRequested(self, x: float, y: float) -> None:

+ 181 - 0
cura/Machines/MachineErrorChecker.py

@@ -0,0 +1,181 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import time
+
+from collections import deque
+
+from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
+
+from UM.Application import Application
+from UM.Logger import Logger
+from UM.Settings.SettingDefinition import SettingDefinition
+from UM.Settings.Validator import ValidatorState
+
+
+#
+# This class performs setting error checks for the currently active machine.
+#
+# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag.
+# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key
+# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should
+# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
+# for it to finish the complete work.
+#
+class MachineErrorChecker(QObject):
+
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self._global_stack = None
+
+        self._has_errors = True  # Result of the error check, indicating whether there are errors in the stack
+        self._error_keys = set()  # A set of settings keys that have errors
+        self._error_keys_in_progress = set()  # The variable that stores the results of the currently in progress check
+
+        self._stacks_and_keys_to_check = None  # a FIFO queue of tuples (stack, key) to check for errors
+
+        self._need_to_check = False  # Whether we need to schedule a new check or not. This flag is set when a new
+                                     # error check needs to take place while there is already one running at the moment.
+        self._check_in_progress = False  # Whether there is an error check running in progress at the moment.
+
+        self._application = Application.getInstance()
+        self._machine_manager = self._application.getMachineManager()
+
+        self._start_time = 0  # measure checking time
+
+        # This timer delays the starting of error check so we can react less frequently if the user is frequently
+        # changing settings.
+        self._error_check_timer = QTimer(self)
+        self._error_check_timer.setInterval(100)
+        self._error_check_timer.setSingleShot(True)
+
+    def initialize(self):
+        self._error_check_timer.timeout.connect(self._rescheduleCheck)
+
+        # Reconnect all signals when the active machine gets changed.
+        self._machine_manager.globalContainerChanged.connect(self._onMachineChanged)
+
+        # Whenever the machine settings get changed, we schedule an error check.
+        self._machine_manager.globalContainerChanged.connect(self.startErrorCheck)
+        self._machine_manager.globalValueChanged.connect(self.startErrorCheck)
+
+        self._onMachineChanged()
+
+    def _onMachineChanged(self):
+        if self._global_stack:
+            self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
+            self._global_stack.containersChanged.disconnect(self.startErrorCheck)
+
+            for extruder in self._global_stack.extruders.values():
+                extruder.propertyChanged.disconnect(self.startErrorCheck)
+                extruder.containersChanged.disconnect(self.startErrorCheck)
+
+        self._global_stack = self._machine_manager.activeMachine
+
+        if self._global_stack:
+            self._global_stack.propertyChanged.connect(self.startErrorCheck)
+            self._global_stack.containersChanged.connect(self.startErrorCheck)
+
+            for extruder in self._global_stack.extruders.values():
+                extruder.propertyChanged.connect(self.startErrorCheck)
+                extruder.containersChanged.connect(self.startErrorCheck)
+
+    hasErrorUpdated = pyqtSignal()
+    needToWaitForResultChanged = pyqtSignal()
+    errorCheckFinished = pyqtSignal()
+
+    @pyqtProperty(bool, notify = hasErrorUpdated)
+    def hasError(self) -> bool:
+        return self._has_errors
+
+    @pyqtProperty(bool, notify = needToWaitForResultChanged)
+    def needToWaitForResult(self) -> bool:
+        return self._need_to_check or self._check_in_progress
+
+    # Starts the error check timer to schedule a new error check.
+    def startErrorCheck(self, *args):
+        if not self._check_in_progress:
+            self._need_to_check = True
+            self.needToWaitForResultChanged.emit()
+        self._error_check_timer.start()
+
+    # This function is called by the timer to reschedule a new error check.
+    # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
+    # to notify the current check to stop and start a new one.
+    def _rescheduleCheck(self):
+        if self._check_in_progress and not self._need_to_check:
+            self._need_to_check = True
+            self.needToWaitForResultChanged.emit()
+            return
+
+        self._error_keys_in_progress = set()
+        self._need_to_check = False
+        self.needToWaitForResultChanged.emit()
+
+        global_stack = self._machine_manager.activeMachine
+        if global_stack is None:
+            Logger.log("i", "No active machine, nothing to check.")
+            return
+
+        # Populate the (stack, key) tuples to check
+        self._stacks_and_keys_to_check = deque()
+        for stack in [global_stack] + list(global_stack.extruders.values()):
+            for key in stack.getAllKeys():
+                self._stacks_and_keys_to_check.append((stack, key))
+
+        self._application.callLater(self._checkStack)
+        self._start_time = time.time()
+        Logger.log("d", "New error check scheduled.")
+
+    def _checkStack(self):
+        if self._need_to_check:
+            Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
+            self._check_in_progress = False
+            self._application.callLater(self.startErrorCheck)
+            return
+
+        self._check_in_progress = True
+
+        # If there is nothing to check any more, it means there is no error.
+        if not self._stacks_and_keys_to_check:
+            # Finish
+            self._setResult(False)
+            return
+
+        # Get the next stack and key to check
+        stack, key = self._stacks_and_keys_to_check.popleft()
+
+        enabled = stack.getProperty(key, "enabled")
+        if not enabled:
+            self._application.callLater(self._checkStack)
+            return
+
+        validation_state = stack.getProperty(key, "validationState")
+        if validation_state is None:
+            # Setting is not validated. This can happen if there is only a setting definition.
+            # We do need to validate it, because a setting definitions value can be set by a function, which could
+            # be an invalid setting.
+            definition = stack.getSettingDefinition(key)
+            validator_type = SettingDefinition.getValidatorForType(definition.type)
+            if validator_type:
+                validator = validator_type(key)
+                validation_state = validator(stack)
+        if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
+            # Finish
+            self._setResult(True)
+            return
+
+        # Schedule the check for the next key
+        self._application.callLater(self._checkStack)
+
+    def _setResult(self, result: bool):
+        if result != self._has_errors:
+            self._has_errors = result
+            self.hasErrorUpdated.emit()
+            self._machine_manager.stacksValidationChanged.emit()
+        self._need_to_check = False
+        self._check_in_progress = False
+        self.needToWaitForResultChanged.emit()
+        self.errorCheckFinished.emit()
+        Logger.log("i", "Error check finished, result = %s, time = %0.1fs", result, time.time() - self._start_time)

+ 8 - 6
cura/Machines/MaterialGroup.py

@@ -1,9 +1,10 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
+from typing import List
+from cura.Machines.MaterialNode import MaterialNode #For type checking.
 
-#
-# A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
+## A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile.
 # The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For
 # example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4",
 # and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs".
@@ -15,12 +16,13 @@
 #                                so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc.
 #
 class MaterialGroup:
-    __slots__ = ("name", "root_material_node", "derived_material_node_list")
+    __slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list")
 
-    def __init__(self, name: str):
+    def __init__(self, name: str, root_material_node: MaterialNode):
         self.name = name
-        self.root_material_node = None
-        self.derived_material_node_list = []
+        self.is_read_only = False
+        self.root_material_node = root_material_node
+        self.derived_material_node_list = [] #type: List[MaterialNode]
 
     def __str__(self) -> str:
         return "%s[%s]" % (self.__class__.__name__, self.name)

+ 61 - 21
cura/Machines/MaterialManager.py

@@ -72,29 +72,28 @@ class MaterialManager(QObject):
 
     def initialize(self):
         # Find all materials and put them in a matrix for quick search.
-        material_metadata_list = self._container_registry.findContainersMetadata(type = "material")
+        material_metadatas = {metadata["id"]: metadata for metadata in self._container_registry.findContainersMetadata(type = "material")}
 
         self._material_group_map = dict()
 
         # Map #1
         #    root_material_id -> MaterialGroup
-        for material_metadata in material_metadata_list:
-            material_id = material_metadata["id"]
+        for material_id, material_metadata in material_metadatas.items():
             # We don't store empty material in the lookup tables
             if material_id == "empty_material":
                 continue
 
             root_material_id = material_metadata.get("base_file")
             if root_material_id not in self._material_group_map:
-                self._material_group_map[root_material_id] = MaterialGroup(root_material_id)
+                self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
+                self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
             group = self._material_group_map[root_material_id]
 
-            # We only add root materials here
-            if material_id == root_material_id:
-                group.root_material_node = MaterialNode(material_metadata)
-            else:
+            #Store this material in the group of the appropriate root material.
+            if material_id != root_material_id:
                 new_node = MaterialNode(material_metadata)
                 group.derived_material_node_list.append(new_node)
+
         # Order this map alphabetically so it's easier to navigate in a debugger
         self._material_group_map = OrderedDict(sorted(self._material_group_map.items(), key = lambda x: x[0]))
 
@@ -108,6 +107,7 @@ class MaterialManager(QObject):
         # Map #2
         # Lookup table for material type -> fallback material metadata, only for read-only materials
         grouped_by_type_dict = dict()
+        material_types_without_fallback = set()
         for root_material_id, material_node in self._material_group_map.items():
             if not self._container_registry.isReadOnly(root_material_id):
                 continue
@@ -115,6 +115,7 @@ class MaterialManager(QObject):
             if material_type not in grouped_by_type_dict:
                 grouped_by_type_dict[material_type] = {"generic": None,
                                                        "others": []}
+                material_types_without_fallback.add(material_type)
             brand = material_node.root_material_node.metadata["brand"]
             if brand.lower() == "generic":
                 to_add = True
@@ -124,6 +125,10 @@ class MaterialManager(QObject):
                         to_add = False  # don't add if it's not the default diameter
                 if to_add:
                     grouped_by_type_dict[material_type] = material_node.root_material_node.metadata
+                    material_types_without_fallback.remove(material_type)
+        # Remove the materials that have no fallback materials
+        for material_type in material_types_without_fallback:
+            del grouped_by_type_dict[material_type]
         self._fallback_materials_map = grouped_by_type_dict
 
         # Map #3
@@ -172,7 +177,7 @@ class MaterialManager(QObject):
         #    "machine" -> "variant_name" -> "root material ID" -> specific material InstanceContainer
         # Construct the "machine" -> "variant" -> "root material ID" -> specific material InstanceContainer
         self._diameter_machine_variant_material_map = dict()
-        for material_metadata in material_metadata_list:
+        for material_metadata in material_metadatas.values():
             # We don't store empty material in the lookup tables
             if material_metadata["id"] == "empty_material":
                 continue
@@ -210,6 +215,7 @@ class MaterialManager(QObject):
         self.materialsUpdated.emit()
 
     def _updateMaps(self):
+        Logger.log("i", "Updating material lookup data ...")
         self.initialize()
 
     def _onContainerMetadataChanged(self, container):
@@ -325,6 +331,35 @@ class MaterialManager(QObject):
 
         return material_node
 
+    #
+    # Gets MaterialNode for the given extruder and machine with the given material type.
+    # Returns None if:
+    #  1. the given machine doesn't have materials;
+    #  2. cannot find any material InstanceContainers with the given settings.
+    #
+    def getMaterialNodeByType(self, global_stack: "GlobalStack", extruder_variant_name: str, material_guid: str) -> Optional["MaterialNode"]:
+        node = None
+        machine_definition = global_stack.definition
+        if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
+            material_diameter = machine_definition.getProperty("material_diameter", "value")
+            if isinstance(material_diameter, SettingFunction):
+                material_diameter = material_diameter(global_stack)
+
+            # Look at the guid to material dictionary
+            root_material_id = None
+            for material_group in self._guid_material_groups_map[material_guid]:
+                if material_group.is_read_only:
+                    root_material_id = material_group.root_material_node.metadata["id"]
+                    break
+
+            if not root_material_id:
+                Logger.log("i", "Cannot find materials with guid [%s] ", material_guid)
+                return None
+
+            node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name,
+                                        material_diameter, root_material_id)
+        return node
+
     #
     # Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
     # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
@@ -349,10 +384,10 @@ class MaterialManager(QObject):
         else:
             return None
 
-    def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: str) -> Optional["MaterialNode"]:
+    def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: Optional[str]) -> Optional["MaterialNode"]:
         node = None
         machine_definition = global_stack.definition
-        if parseBool(machine_definition.getMetaDataEntry("has_materials", False)):
+        if parseBool(global_stack.getMetaDataEntry("has_materials", False)):
             material_diameter = machine_definition.getProperty("material_diameter", "value")
             if isinstance(material_diameter, SettingFunction):
                 material_diameter = material_diameter(global_stack)
@@ -363,6 +398,16 @@ class MaterialManager(QObject):
                                         material_diameter, root_material_id)
         return node
 
+    def removeMaterialByRootId(self, root_material_id: str):
+        material_group = self.getMaterialGroup(root_material_id)
+        if not material_group:
+            Logger.log("i", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
+            return
+
+        nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
+        for node in nodes_to_remove:
+            self._container_registry.removeContainer(node.metadata["id"])
+
     #
     # Methods for GUI
     #
@@ -386,14 +431,7 @@ class MaterialManager(QObject):
     @pyqtSlot("QVariant")
     def removeMaterial(self, material_node: "MaterialNode"):
         root_material_id = material_node.metadata["base_file"]
-        material_group = self.getMaterialGroup(root_material_id)
-        if not material_group:
-            Logger.log("d", "Unable to remove the material with id %s, because it doesn't exist.", root_material_id)
-            return
-
-        nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
-        for node in nodes_to_remove:
-            self._container_registry.removeContainer(node.metadata["id"])
+        self.removeMaterialByRootId(root_material_id)
 
     #
     # Creates a duplicate of a material, which has the same GUID and base_file metadata.
@@ -460,8 +498,10 @@ class MaterialManager(QObject):
         # Ensure all settings are saved.
         self._application.saveSettings()
 
-        global_stack = self._application.getGlobalContainerStack()
-        approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
+        machine_manager = self._application.getMachineManager()
+        extruder_stack = machine_manager.activeStack
+
+        approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
         root_material_id = "generic_pla"
         root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
         material_group = self.getMaterialGroup(root_material_id)

Some files were not shown because too many files changed in this diff