Browse Source

Merge branch 'master' into feature_support_eraser_ux

fieldOfView 7 years ago
parent
commit
11be8f158f

+ 2 - 2
cura/BuildVolume.py

@@ -136,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.
@@ -150,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:
@@ -239,7 +239,7 @@ class BuildVolume(SceneNode):
         # 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.setOutsideBuildArea(group_node.isOutsideBuildArea)
+                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):

+ 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:

+ 41 - 110
cura/CuraApplication.py

@@ -4,7 +4,7 @@
 #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
 
@@ -60,18 +60,20 @@ 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.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
@@ -88,8 +90,8 @@ 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.Settings.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
 
 from cura.ObjectsModel import ObjectsModel
 
@@ -139,15 +141,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"]:
@@ -192,6 +189,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")
@@ -224,12 +222,14 @@ 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_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
 
@@ -286,10 +286,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")
@@ -376,19 +381,9 @@ 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)
+        default_visibility_profile = SettingVisibilityPresetsModel.getInstance().getItem(0)
 
-        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)
+        preferences.setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
 
         self.applicationShuttingDown.connect(self.saveSettings)
         self.engineCreatedSignal.connect(self._onEngineCreated)
@@ -405,91 +400,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())
@@ -743,19 +653,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()
 
@@ -781,8 +700,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
@@ -847,6 +769,9 @@ class CuraApplication(QtApplication):
     def hasGui(self):
         return self._use_gui
 
+    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)
@@ -961,6 +886,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)
@@ -973,6 +899,7 @@ class CuraApplication(QtApplication):
         qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
         qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
         qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager)
+        qmlRegisterSingletonType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel", SettingVisibilityPresetsModel.createSettingVisibilityPresetsModel)
 
         # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
         actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
@@ -1048,6 +975,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

+ 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)

+ 2 - 1
cura/Machines/MaterialGroup.py

@@ -16,10 +16,11 @@ from cura.Machines.MaterialNode import MaterialNode #For type checking.
 #                                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, root_material_node: MaterialNode):
         self.name = name
+        self.is_read_only = False
         self.root_material_node = root_material_node
         self.derived_material_node_list = [] #type: List[MaterialNode]
 

+ 30 - 7
cura/Machines/MaterialManager.py

@@ -86,6 +86,7 @@ class MaterialManager(QObject):
             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, 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]
 
             #Store this material in the group of the appropriate root material.
@@ -100,13 +101,6 @@ class MaterialManager(QObject):
         #    GUID -> material group list
         self._guid_material_groups_map = defaultdict(list)
         for root_material_id, material_group in self._material_group_map.items():
-            # This can happen when we are updating with incomplete data.
-            if material_group.root_material_node is None:
-                Logger.log("e", "Missing root material node for [%s]. Probably caused by update using incomplete data."
-                           " Check all related signals for further debugging.",
-                           material_group.name)
-                self._update_timer.start()
-                return
             guid = material_group.root_material_node.metadata["GUID"]
             self._guid_material_groups_map[guid].append(material_group)
 
@@ -331,6 +325,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

+ 2 - 2
cura/Machines/Models/BrandMaterialsModel.py

@@ -53,8 +53,8 @@ class BrandMaterialsModel(ListModel):
         self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
         self._material_manager = CuraApplication.getInstance().getMaterialManager()
 
-        self._machine_manager.globalContainerChanged.connect(self._update)
-        self._material_manager.materialsUpdated.connect(self._update)
+        self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
+        self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
         self._update()
 
     def setExtruderPosition(self, position: int):

+ 2 - 2
cura/Machines/Models/GenericMaterialsModel.py

@@ -15,8 +15,8 @@ class GenericMaterialsModel(BaseMaterialsModel):
         self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
         self._material_manager = CuraApplication.getInstance().getMaterialManager()
 
-        self._machine_manager.globalContainerChanged.connect(self._update)
-        self._material_manager.materialsUpdated.connect(self._update)
+        self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
+        self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
         self._update()
 
     def _update(self):

+ 82 - 0
cura/Machines/Models/MachineManagementModel.py

@@ -0,0 +1,82 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from UM.Qt.ListModel import ListModel
+
+from PyQt5.QtCore import Qt
+
+from UM.Settings.ContainerRegistry import ContainerRegistry
+from UM.Settings.ContainerStack import ContainerStack
+
+from UM.i18n import i18nCatalog
+catalog = i18nCatalog("cura")
+
+
+#
+# This the QML model for the quality management page.
+#
+class MachineManagementModel(ListModel):
+    NameRole = Qt.UserRole + 1
+    IdRole = Qt.UserRole + 2
+    MetaDataRole = Qt.UserRole + 3
+    GroupRole = Qt.UserRole + 4
+
+    def __init__(self, parent = None):
+        super().__init__(parent)
+        self.addRoleName(self.NameRole, "name")
+        self.addRoleName(self.IdRole, "id")
+        self.addRoleName(self.MetaDataRole, "metadata")
+        self.addRoleName(self.GroupRole, "group")
+        self._local_container_stacks = []
+        self._network_container_stacks = []
+
+        # Listen to changes
+        ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
+        ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
+        ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
+        self._filter_dict = {}
+        self._update()
+
+    ##  Handler for container added/removed events from registry
+    def _onContainerChanged(self, container):
+        # We only need to update when the added / removed container is a stack.
+        if isinstance(container, ContainerStack) and container.getMetaDataEntry("type") == "machine":
+            self._update()
+
+    ##  Private convenience function to reset & repopulate the model.
+    def _update(self):
+        items = []
+
+        # Get first the network enabled printers
+        network_filter_printers = {"type": "machine",
+                                   "um_network_key": "*",
+                                   "hidden": "False"}
+        self._network_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**network_filter_printers)
+        self._network_container_stacks.sort(key = lambda i: i.getMetaDataEntry("connect_group_name"))
+
+        for container in self._network_container_stacks:
+            metadata = container.getMetaData().copy()
+            if container.getBottom():
+                metadata["definition_name"] = container.getBottom().getName()
+
+            items.append({"name": metadata["connect_group_name"],
+                          "id": container.getId(),
+                          "metadata": metadata,
+                          "group": catalog.i18nc("@info:title", "Network enabled printers")})
+
+        # Get now the local printers
+        local_filter_printers = {"type": "machine", "um_network_key": None}
+        self._local_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**local_filter_printers)
+        self._local_container_stacks.sort(key = lambda i: i.getName())
+
+        for container in self._local_container_stacks:
+            metadata = container.getMetaData().copy()
+            if container.getBottom():
+                metadata["definition_name"] = container.getBottom().getName()
+
+            items.append({"name": container.getName(),
+                          "id": container.getId(),
+                          "metadata": metadata,
+                          "group": catalog.i18nc("@info:title", "Local printers")})
+
+        self.setItems(items)

+ 10 - 2
cura/Machines/Models/MultiBuildPlateModel.py

@@ -1,7 +1,7 @@
 # Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
-from PyQt5.QtCore import pyqtSignal, pyqtProperty
+from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty
 
 from UM.Application import Application
 from UM.Scene.Selection import Selection
@@ -21,8 +21,13 @@ class MultiBuildPlateModel(ListModel):
     def __init__(self, parent = None):
         super().__init__(parent)
 
+        self._update_timer = QTimer()
+        self._update_timer.setInterval(100)
+        self._update_timer.setSingleShot(True)
+        self._update_timer.timeout.connect(self._updateSelectedObjectBuildPlateNumbers)
+
         self._application = Application.getInstance()
-        self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
+        self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbersDelayed)
         Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers)
 
         self._max_build_plate = 1  # default
@@ -45,6 +50,9 @@ class MultiBuildPlateModel(ListModel):
     def activeBuildPlate(self):
         return self._active_build_plate
 
+    def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
+        self._update_timer.start()
+
     def _updateSelectedObjectBuildPlateNumbers(self, *args):
         result = set()
         for node in Selection.getAllSelectedObjects():

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