Browse Source

Merge branch 'master' into feature_setting_visibility_profiles

fieldOfView 7 years ago
parent
commit
97d7354839

+ 7 - 4
.github/ISSUE_TEMPLATE.md

@@ -1,9 +1,12 @@
 <!--
-The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out.
-Before filing, please check if the issue already exists (either open or closed).
+The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out, and issues which do not use this template will be removed.
 
-It is also helpful to attach a project (.3MF) 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.
+Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
+
+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.
 
 Thank you for using Cura!
 -->

+ 4 - 3
cura/BuildVolume.py

@@ -826,6 +826,7 @@ class BuildVolume(SceneNode):
             offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
             if offset_y is None:
                 offset_y = 0
+            offset_y = -offset_y #Y direction of g-code is the inverse of Y direction of Cura's scene space.
             result[extruder_id] = []
 
             for polygon in machine_disallowed_polygons:
@@ -936,8 +937,8 @@ class BuildVolume(SceneNode):
     #   stack.
     #
     #   \return A sequence of setting values, one for each extruder.
-    def _getSettingFromAllExtruders(self, setting_key, property = "value"):
-        all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, property)
+    def _getSettingFromAllExtruders(self, setting_key):
+        all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
         all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
         for i in range(len(all_values)):
             if not all_values[i] and (all_types[i] == "int" or all_types[i] == "float"):
@@ -950,7 +951,7 @@ class BuildVolume(SceneNode):
     #   not part of the collision radius, such as bed adhesion (skirt/brim/raft)
     #   and travel avoid distance.
     def _getEdgeDisallowedSize(self):
-        if not self._global_container_stack:
+        if not self._global_container_stack or not self._global_container_stack.extruders:
             return 0
 
         container_stack = self._global_container_stack

+ 13 - 2
cura/CrashHandler.py

@@ -13,16 +13,17 @@ import ssl
 import urllib.request
 import urllib.error
 import shutil
-import sys
 
-from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR
+from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
 from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
+from PyQt5.QtGui import QDesktopServices
 
 from UM.Application import Application
 from UM.Logger import Logger
 from UM.View.GL.OpenGL import OpenGL
 from UM.i18n import i18nCatalog
 from UM.Platform import Platform
+from UM.Resources import Resources
 
 catalog = i18nCatalog("cura")
 
@@ -91,6 +92,7 @@ class CrashHandler:
         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>
+                    <p>Backups can be found in the configuration folder.</p>
                     <p>Please send us this Crash Report to fix the problem.</p>
                 """))
         label.setWordWrap(True)
@@ -104,8 +106,13 @@ class CrashHandler:
         show_details_button.setMaximumWidth(200)
         show_details_button.clicked.connect(self._showDetailedReport)
 
+        show_configuration_folder_button = QPushButton(catalog.i18nc("@action:button", "Show configuration folder"), dialog)
+        show_configuration_folder_button.setMaximumWidth(200)
+        show_configuration_folder_button.clicked.connect(self._showConfigurationFolder)
+
         layout.addWidget(self._send_report_checkbox)
         layout.addWidget(show_details_button)
+        layout.addWidget(show_configuration_folder_button)
 
         # "backup and start clean" and "close" buttons
         buttons = QDialogButtonBox()
@@ -181,6 +188,10 @@ class CrashHandler:
 
         self.early_crash_dialog.close()
 
+    def _showConfigurationFolder(self):
+        path = Resources.getConfigStoragePath();
+        QDesktopServices.openUrl(QUrl.fromLocalFile( path ))
+
     def _showDetailedReport(self):
         self.dialog.exec_()
 

+ 102 - 33
cura/CuraApplication.py

@@ -2,6 +2,8 @@
 # Cura is released under the terms of the LGPLv3 or higher.
 #Type hinting.
 from typing import Dict
+
+from PyQt5.QtCore import QObject
 from PyQt5.QtNetwork import QLocalServer
 from PyQt5.QtNetwork import QLocalSocket
 
@@ -52,14 +54,24 @@ from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyT
 from UM.Settings.ContainerRegistry import ContainerRegistry
 from UM.Settings.SettingFunction import SettingFunction
 from cura.Settings.MachineNameValidator import MachineNameValidator
-from cura.Settings.ProfilesModel import ProfilesModel
-from cura.Settings.MaterialsModel import MaterialsModel
-from cura.Settings.QualityAndUserProfilesModel import QualityAndUserProfilesModel
+
+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.Settings.SettingInheritanceManager import SettingInheritanceManager
-from cura.Settings.UserProfilesModel import UserProfilesModel
 from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
 from cura.Settings.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
 
+from cura.Machines.VariantManager import VariantManager
+from cura.Machines.Models.QualityManagementModel import QualityManagementModel
 
 from . import PlatformPhysics
 from . import BuildVolume
@@ -72,18 +84,15 @@ from . import CameraImageProvider
 from . import MachineActionManager
 
 from cura.Settings.MachineManager import MachineManager
-from cura.Settings.MaterialManager import MaterialManager
 from cura.Settings.ExtruderManager import ExtruderManager
 from cura.Settings.UserChangesModel import UserChangesModel
 from cura.Settings.ExtrudersModel import ExtrudersModel
-from cura.Settings.ContainerSettingsModel import ContainerSettingsModel
 from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
-from cura.Settings.QualitySettingsModel import QualitySettingsModel
+from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
 from cura.Settings.ContainerManager import ContainerManager
 from cura.Settings.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
 
 from cura.ObjectsModel import ObjectsModel
-from cura.BuildPlateModel import BuildPlateModel
 
 from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 from UM.FlameProfiler import pyqtSlot
@@ -220,6 +229,7 @@ class CuraApplication(QtApplication):
         self._material_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
@@ -237,6 +247,8 @@ class CuraApplication(QtApplication):
         if kwargs["parsed_command_line"].get("trigger_early_crash", False):
             assert not "This crash is triggered by the trigger_early_crash command line argument."
 
+        self._variant_manager = None
+
         self.default_theme = "cura-light"
 
         self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@@ -291,21 +303,25 @@ class CuraApplication(QtApplication):
         # Since they are empty, they should never be serialized and instead just programmatically created.
         # We need them to simplify the switching between materials.
         empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
+        self.empty_container = empty_container
 
         empty_definition_changes_container = copy.deepcopy(empty_container)
         empty_definition_changes_container.setMetaDataEntry("id", "empty_definition_changes")
         empty_definition_changes_container.addMetaDataEntry("type", "definition_changes")
         ContainerRegistry.getInstance().addContainer(empty_definition_changes_container)
+        self.empty_definition_changes_container = empty_definition_changes_container
 
         empty_variant_container = copy.deepcopy(empty_container)
         empty_variant_container.setMetaDataEntry("id", "empty_variant")
         empty_variant_container.addMetaDataEntry("type", "variant")
         ContainerRegistry.getInstance().addContainer(empty_variant_container)
+        self.empty_variant_container = empty_variant_container
 
         empty_material_container = copy.deepcopy(empty_container)
         empty_material_container.setMetaDataEntry("id", "empty_material")
         empty_material_container.addMetaDataEntry("type", "material")
         ContainerRegistry.getInstance().addContainer(empty_material_container)
+        self.empty_material_container = empty_material_container
 
         empty_quality_container = copy.deepcopy(empty_container)
         empty_quality_container.setMetaDataEntry("id", "empty_quality")
@@ -314,12 +330,14 @@ class CuraApplication(QtApplication):
         empty_quality_container.addMetaDataEntry("type", "quality")
         empty_quality_container.addMetaDataEntry("supported", False)
         ContainerRegistry.getInstance().addContainer(empty_quality_container)
+        self.empty_quality_container = empty_quality_container
 
         empty_quality_changes_container = copy.deepcopy(empty_container)
         empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes")
         empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
         empty_quality_changes_container.addMetaDataEntry("quality_type", "not_supported")
         ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
+        self.empty_quality_changes_container = empty_quality_changes_container
 
         with ContainerRegistry.getInstance().lockFile():
             ContainerRegistry.getInstance().loadAllMetadata()
@@ -375,6 +393,9 @@ class CuraApplication(QtApplication):
 
         self.getCuraSceneController().setActiveBuildPlate(0)  # Initialize
 
+        self._quality_profile_drop_down_menu_model = None
+        self._custom_quality_profile_drop_down_menu_model = None
+
         CuraApplication.Created = True
 
 
@@ -432,8 +453,6 @@ class CuraApplication(QtApplication):
             has_user_interaction = True
         return has_user_interaction
 
-    onDiscardOrKeepProfileChangesClosed = pyqtSignal()  # Used to notify other managers that the dialog was closed
-
     @pyqtSlot(str)
     def discardOrKeepProfileChangesClosed(self, option):
         if option == "discard":
@@ -456,7 +475,6 @@ class CuraApplication(QtApplication):
                 user_global_container.update()
 
         # notify listeners that quality has changed (after user selected discard or keep)
-        self.onDiscardOrKeepProfileChangesClosed.emit()
         self.getMachineManager().activeQualityChanged.emit()
 
     @pyqtSlot(int)
@@ -632,6 +650,20 @@ class CuraApplication(QtApplication):
     def run(self):
         self.preRun()
 
+        container_registry = ContainerRegistry.getInstance()
+        self._variant_manager = VariantManager(container_registry)
+        self._variant_manager.initialize()
+
+        from cura.Machines.MaterialManager import MaterialManager
+        self._material_manager = MaterialManager(container_registry, parent = self)
+        self._material_manager.initialize()
+
+        from cura.Machines.QualityManager import QualityManager
+        self._quality_manager = QualityManager(container_registry, parent = self)
+        self._quality_manager.initialize()
+
+        self._machine_manager = MachineManager(self)
+
         # Check if we should run as single instance or not
         self._setUpSingleInstanceServer()
 
@@ -725,7 +757,7 @@ class CuraApplication(QtApplication):
 
     def getMachineManager(self, *args) -> MachineManager:
         if self._machine_manager is None:
-            self._machine_manager = MachineManager.createMachineManager()
+            self._machine_manager = MachineManager(self)
         return self._machine_manager
 
     def getExtruderManager(self, *args):
@@ -733,20 +765,32 @@ class CuraApplication(QtApplication):
             self._extruder_manager = ExtruderManager.createExtruderManager()
         return self._extruder_manager
 
+    def getVariantManager(self, *args):
+        return self._variant_manager
+
+    @pyqtSlot(result = QObject)
     def getMaterialManager(self, *args):
-        if self._material_manager is None:
-            self._material_manager = MaterialManager.createMaterialManager()
         return self._material_manager
 
+    @pyqtSlot(result = QObject)
+    def getQualityManager(self, *args):
+        return self._quality_manager
+
     def getObjectsModel(self, *args):
         if self._object_manager is None:
             self._object_manager = ObjectsModel.createObjectsModel()
         return self._object_manager
 
+    @pyqtSlot(result = QObject)
+    def getMultiBuildPlateModel(self, *args):
+        if self._multi_build_plate_model is None:
+            self._multi_build_plate_model = MultiBuildPlateModel(self)
+        return self._multi_build_plate_model
+
+    @pyqtSlot(result = QObject)
     def getBuildPlateModel(self, *args):
         if self._build_plate_model is None:
-            self._build_plate_model = BuildPlateModel.createBuildPlateModel()
-
+            self._build_plate_model = BuildPlateModel(self)
         return self._build_plate_model
 
     def getCuraSceneController(self, *args):
@@ -784,6 +828,16 @@ class CuraApplication(QtApplication):
     def getPrintInformation(self):
         return self._print_information
 
+    def getQualityProfilesDropDownMenuModel(self, *args, **kwargs):
+        if self._quality_profile_drop_down_menu_model is None:
+            self._quality_profile_drop_down_menu_model = QualityProfilesDropDownMenuModel(self)
+        return self._quality_profile_drop_down_menu_model
+
+    def getCustomQualityProfilesDropDownMenuModel(self, *args, **kwargs):
+        if self._custom_quality_profile_drop_down_menu_model is None:
+            self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
+        return self._custom_quality_profile_drop_down_menu_model
+
     ##  Registers objects for the QML engine to use.
     #
     #   \param engine The QML engine.
@@ -798,27 +852,34 @@ class CuraApplication(QtApplication):
 
         qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
 
-        qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 2, "SceneController", self.getCuraSceneController)
+        qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, "SceneController", self.getCuraSceneController)
         qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
         qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
-        qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
         qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
-        qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
+        qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
         qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
 
-        qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 2, "ObjectsModel", self.getObjectsModel)
-        qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel)
+        qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
+        qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
+        qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
         qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
         qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
-        qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
-        qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
-        qmlRegisterType(MaterialsModel, "Cura", 1, 0, "MaterialsModel")
-        qmlRegisterType(QualityAndUserProfilesModel, "Cura", 1, 0, "QualityAndUserProfilesModel")
-        qmlRegisterType(UserProfilesModel, "Cura", 1, 0, "UserProfilesModel")
+
+        qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
+        qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel")
+        qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
+        qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
+
+        qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
+                                 "QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
+        qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,
+                                 "CustomQualityProfilesDropDownMenuModel", self.getCustomQualityProfilesDropDownMenuModel)
+        qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
+
         qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
         qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
         qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
-        qmlRegisterType(UserChangesModel, "Cura", 1, 1, "UserChangesModel")
+        qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
         qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager)
         qmlRegisterSingletonType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel", SettingVisibilityPresetsModel.createSettingVisibilityPresetsModel)
 
@@ -901,7 +962,7 @@ class CuraApplication(QtApplication):
         count = 0
         scene_bounding_box = None
         is_block_slicing_node = False
-        active_build_plate = self.getBuildPlateModel().activeBuildPlate
+        active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
             if (
                 not issubclass(type(node), CuraSceneNode) or
@@ -1150,7 +1211,7 @@ class CuraApplication(QtApplication):
     @pyqtSlot()
     def arrangeAll(self):
         nodes = []
-        active_build_plate = self.getBuildPlateModel().activeBuildPlate
+        active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
             if not isinstance(node, SceneNode):
                 continue
@@ -1299,7 +1360,7 @@ class CuraApplication(QtApplication):
         group_decorator = GroupDecorator()
         group_node.addDecorator(group_decorator)
         group_node.addDecorator(ConvexHullDecorator())
-        group_node.addDecorator(BuildPlateDecorator(self.getBuildPlateModel().activeBuildPlate))
+        group_node.addDecorator(BuildPlateDecorator(self.getMultiBuildPlateModel().activeBuildPlate))
         group_node.setParent(self.getController().getScene().getRoot())
         group_node.setSelectable(True)
         center = Selection.getSelectionCenter()
@@ -1444,7 +1505,7 @@ class CuraApplication(QtApplication):
         arrange_objects_on_load = (
             not Preferences.getInstance().getValue("cura/use_multi_build_plate") or
             not Preferences.getInstance().getValue("cura/not_arrange_objects_on_load"))
-        target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
+        target_build_plate = self.getMultiBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
 
         root = self.getController().getScene().getRoot()
         fixed_nodes = []
@@ -1457,8 +1518,16 @@ class CuraApplication(QtApplication):
         for original_node in nodes:
 
             # Create a CuraSceneNode just if the original node is not that type
-            node = original_node if isinstance(original_node, CuraSceneNode) else CuraSceneNode()
-            node.setMeshData(original_node.getMeshData())
+            if isinstance(original_node, CuraSceneNode):
+                node = original_node
+            else:
+                node = CuraSceneNode()
+                node.setMeshData(original_node.getMeshData())
+
+                #Setting meshdata does not apply scaling.
+                if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
+                    node.scale(original_node.getScale())
+
 
             node.setSelectable(True)
             node.setName(os.path.basename(filename))

+ 49 - 0
cura/Machines/ContainerNode.py

@@ -0,0 +1,49 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from typing import Optional
+
+from collections import OrderedDict
+
+from UM.Logger import Logger
+from UM.Settings.InstanceContainer import InstanceContainer
+
+
+##
+# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
+#
+# ContainerNode is a multi-purpose class. It has two main purposes:
+#  1. It encapsulates an InstanceContainer. It contains that InstanceContainer's
+#          - metadata (Always)
+#          - container (lazy-loaded when needed)
+#  2. It also serves as a node in a hierarchical InstanceContainer lookup table/tree.
+#     This is used in Variant, Material, and Quality Managers.
+#
+class ContainerNode:
+    __slots__ = ("metadata", "container", "children_map")
+
+    def __init__(self, metadata: Optional[dict] = None):
+        self.metadata = metadata
+        self.container = None
+        self.children_map = OrderedDict()
+
+    def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
+        return self.children_map.get(child_key)
+
+    def getContainer(self) -> "InstanceContainer":
+        if self.metadata is None:
+            raise RuntimeError("Cannot get container for a ContainerNode without metadata")
+
+        if self.container is None:
+            container_id = self.metadata["id"]
+            Logger.log("i", "Lazy-loading container [%s]", container_id)
+            from UM.Settings.ContainerRegistry import ContainerRegistry
+            container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
+            if not container_list:
+                raise RuntimeError("Failed to lazy-load container [%s], cannot find it" % container_id)
+            self.container = container_list[0]
+
+        return self.container
+
+    def __str__(self) -> str:
+        return "%s[%s]" % (self.__class__.__name__, self.metadata.get("id"))

+ 26 - 0
cura/Machines/MaterialGroup.py

@@ -0,0 +1,26 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+
+#
+# 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".
+#
+# Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information:
+#  - name: "generic_abs", root_material_id
+#  - root_material_node: MaterialNode of "generic_abs"
+#  - derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs",
+#                                so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc.
+#
+class MaterialGroup:
+    __slots__ = ("name", "root_material_node", "derived_material_node_list")
+
+    def __init__(self, name: str):
+        self.name = name
+        self.root_material_node = None
+        self.derived_material_node_list = []
+
+    def __str__(self) -> str:
+        return "%s[%s]" % (self.__class__.__name__, self.name)

+ 479 - 0
cura/Machines/MaterialManager.py

@@ -0,0 +1,479 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from collections import defaultdict, OrderedDict
+import copy
+import uuid
+from typing import Optional, TYPE_CHECKING
+
+from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
+
+from UM.Application import Application
+from UM.Logger import Logger
+from UM.Settings.ContainerRegistry import ContainerRegistry
+from UM.Settings.SettingFunction import SettingFunction
+from UM.Util import parseBool
+
+from .MaterialNode import MaterialNode
+from .MaterialGroup import MaterialGroup
+
+if TYPE_CHECKING:
+    from cura.Settings.GlobalStack import GlobalStack
+
+
+#
+# MaterialManager maintains a number of maps and trees for material lookup.
+# The models GUI and QML use are now only dependent on the MaterialManager. That means as long as the data in
+# MaterialManager gets updated correctly, the GUI models should be updated correctly too, and the same goes for GUI.
+#
+# For now, updating the lookup maps and trees here is very simple: we discard the old data completely and recreate them
+# again. This means the update is exactly the same as initialization. There are performance concerns about this approach
+# but so far the creation of the tables and maps is very fast and there is no noticeable slowness, we keep it like this
+# because it's simple.
+#
+class MaterialManager(QObject):
+
+    materialsUpdated = pyqtSignal()  # Emitted whenever the material lookup tables are updated.
+
+    def __init__(self, container_registry, parent = None):
+        super().__init__(parent)
+        self._application = Application.getInstance()
+        self._container_registry = container_registry  # type: ContainerRegistry
+
+        self._fallback_materials_map = dict()  # material_type -> generic material metadata
+        self._material_group_map = dict()  # root_material_id -> MaterialGroup
+        self._diameter_machine_variant_material_map = dict()  # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
+
+        # We're using these two maps to convert between the specific diameter material id and the generic material id
+        # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
+        # i.e. generic_pla -> generic_pla_175
+        self._material_diameter_map = defaultdict(dict)  # root_material_id -> approximate diameter str -> root_material_id for that diameter
+        self._diameter_material_map = dict()  # material id including diameter (generic_pla_175) -> material root id (generic_pla)
+
+        # This is used in Legacy UM3 send material function and the material management page.
+        self._guid_material_groups_map = defaultdict(list)  # GUID -> a list of material_groups
+
+        # The machine definition ID for the non-machine-specific materials.
+        # This is used as the last fallback option if the given machine-specific material(s) cannot be found.
+        self._default_machine_definition_id = "fdmprinter"
+        self._default_approximate_diameter_for_quality_search = "3"
+
+        # When a material gets added/imported, there can be more than one InstanceContainers. In those cases, we don't
+        # want to react on every container/metadata changed signal. The timer here is to buffer it a bit so we don't
+        # react too many time.
+        self._update_timer = QTimer(self)
+        self._update_timer.setInterval(300)
+        self._update_timer.setSingleShot(True)
+        self._update_timer.timeout.connect(self._updateMaps)
+
+        self._container_registry.containerMetaDataChanged.connect(self._onContainerMetadataChanged)
+        self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
+        self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
+
+    def initialize(self):
+        # Find all materials and put them in a matrix for quick search.
+        material_metadata_list = 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"]
+            # 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)
+            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:
+                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]))
+
+        # Map #1.5
+        #    GUID -> material group list
+        self._guid_material_groups_map = defaultdict(list)
+        for root_material_id, material_group in self._material_group_map.items():
+            guid = material_group.root_material_node.metadata["GUID"]
+            self._guid_material_groups_map[guid].append(material_group)
+
+        # Map #2
+        # Lookup table for material type -> fallback material metadata, only for read-only materials
+        grouped_by_type_dict = dict()
+        for root_material_id, material_node in self._material_group_map.items():
+            if not self._container_registry.isReadOnly(root_material_id):
+                continue
+            material_type = material_node.root_material_node.metadata["material"]
+            if material_type not in grouped_by_type_dict:
+                grouped_by_type_dict[material_type] = {"generic": None,
+                                                       "others": []}
+            brand = material_node.root_material_node.metadata["brand"]
+            if brand.lower() == "generic":
+                to_add = True
+                if material_type in grouped_by_type_dict:
+                    diameter = material_node.root_material_node.metadata.get("approximate_diameter")
+                    if diameter != self._default_approximate_diameter_for_quality_search:
+                        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
+        self._fallback_materials_map = grouped_by_type_dict
+
+        # Map #3
+        # There can be multiple material profiles for the same material with different diameters, such as "generic_pla"
+        # and "generic_pla_175". This is inconvenient when we do material-specific quality lookup because a quality can
+        # be for either "generic_pla" or "generic_pla_175", but not both. This map helps to get the correct material ID
+        # for quality search.
+        self._material_diameter_map = defaultdict(dict)
+        self._diameter_material_map = dict()
+
+        # Group the material IDs by the same name, material, brand, and color but with different diameters.
+        material_group_dict = dict()
+        keys_to_fetch = ("name", "material", "brand", "color")
+        for root_material_id, machine_node in self._material_group_map.items():
+            if not self._container_registry.isReadOnly(root_material_id):
+                continue
+
+            root_material_metadata = machine_node.root_material_node.metadata
+
+            key_data = []
+            for key in keys_to_fetch:
+                key_data.append(root_material_metadata.get(key))
+            key_data = tuple(key_data)
+
+            if key_data not in material_group_dict:
+                material_group_dict[key_data] = dict()
+            approximate_diameter = root_material_metadata.get("approximate_diameter")
+            material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"]
+
+        # Map [root_material_id][diameter] -> root_material_id for this diameter
+        for data_dict in material_group_dict.values():
+            for root_material_id1 in data_dict.values():
+                if root_material_id1 in self._material_diameter_map:
+                    continue
+                diameter_map = data_dict
+                for root_material_id2 in data_dict.values():
+                    self._material_diameter_map[root_material_id2] = diameter_map
+
+            default_root_material_id = data_dict.get(self._default_approximate_diameter_for_quality_search)
+            if default_root_material_id is None:
+                default_root_material_id = list(data_dict.values())[0]  # no default diameter present, just take "the" only one
+            for root_material_id in data_dict.values():
+                self._diameter_material_map[root_material_id] = default_root_material_id
+
+        # Map #4
+        #    "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:
+            # We don't store empty material in the lookup tables
+            if material_metadata["id"] == "empty_material":
+                continue
+
+            root_material_id = material_metadata["base_file"]
+            definition = material_metadata["definition"]
+            approximate_diameter = material_metadata["approximate_diameter"]
+
+            if approximate_diameter not in self._diameter_machine_variant_material_map:
+                self._diameter_machine_variant_material_map[approximate_diameter] = {}
+
+            machine_variant_material_map = self._diameter_machine_variant_material_map[approximate_diameter]
+            if definition not in machine_variant_material_map:
+                machine_variant_material_map[definition] = MaterialNode()
+
+            machine_node = machine_variant_material_map[definition]
+            variant_name = material_metadata.get("variant_name")
+            if not variant_name:
+                # if there is no variant, this material is for the machine, so put its metadata in the machine node.
+                machine_node.material_map[root_material_id] = MaterialNode(material_metadata)
+            else:
+                # this material is variant-specific, so we save it in a variant-specific node under the
+                # machine-specific node
+                if variant_name not in machine_node.children_map:
+                    machine_node.children_map[variant_name] = MaterialNode()
+
+                variant_node = machine_node.children_map[variant_name]
+                if root_material_id not in variant_node.material_map:
+                    variant_node.material_map[root_material_id] = MaterialNode(material_metadata)
+                else:
+                    # Sanity check: make sure we don't have duplicated variant-specific materials for the same machine
+                    raise RuntimeError("Found duplicate variant name [%s] for machine [%s] in material [%s]" %
+                                       (variant_name, definition, material_metadata["id"]))
+
+        self.materialsUpdated.emit()
+
+    def _updateMaps(self):
+        self.initialize()
+
+    def _onContainerMetadataChanged(self, container):
+        self._onContainerChanged(container)
+
+    def _onContainerChanged(self, container):
+        container_type = container.getMetaDataEntry("type")
+        if container_type != "material":
+            return
+
+        # update the maps
+        self._update_timer.start()
+
+    def getMaterialGroup(self, root_material_id: str) -> Optional[MaterialGroup]:
+        return self._material_group_map.get(root_material_id)
+
+    def getRootMaterialIDForDiameter(self, root_material_id: str, approximate_diameter: str) -> str:
+        return self._material_diameter_map.get(root_material_id).get(approximate_diameter, root_material_id)
+
+    def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
+        return self._diameter_material_map.get(root_material_id)
+
+    def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
+        return self._guid_material_groups_map.get(guid)
+
+    #
+    # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
+    #
+    def getAvailableMaterials(self, machine_definition_id: str, extruder_variant_name: Optional[str],
+                              diameter: float) -> dict:
+        # round the diameter to get the approximate diameter
+        rounded_diameter = str(round(diameter))
+        if rounded_diameter not in self._diameter_machine_variant_material_map:
+            Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s])", diameter, rounded_diameter)
+            return dict()
+
+        # If there are variant materials, get the variant material
+        machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
+        machine_node = machine_variant_material_map.get(machine_definition_id)
+        default_machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
+        variant_node = None
+        if extruder_variant_name is not None and machine_node is not None:
+            variant_node = machine_node.getChildNode(extruder_variant_name)
+
+        nodes_to_check = [variant_node, machine_node, default_machine_node]
+
+        # Fallback mechanism of finding materials:
+        #  1. variant-specific material
+        #  2. machine-specific material
+        #  3. generic material (for fdmprinter)
+        material_id_metadata_dict = dict()
+        for node in nodes_to_check:
+            if node is not None:
+                for material_id, node in node.material_map.items():
+                    if material_id not in material_id_metadata_dict:
+                        material_id_metadata_dict[material_id] = node
+
+        return material_id_metadata_dict
+
+    #
+    # A convenience function to get available materials for the given machine with the extruder position.
+    #
+    def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
+                                                extruder_stack: "ExtruderStack") -> Optional[dict]:
+        machine_definition_id = machine.definition.getId()
+        variant_name = None
+        if extruder_stack.variant.getId() != "empty_variant":
+            variant_name = extruder_stack.variant.getName()
+        diameter = extruder_stack.approximateMaterialDiameter
+
+        # Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
+        return self.getAvailableMaterials(machine_definition_id, variant_name, diameter)
+
+    #
+    # Gets MaterialNode for the given extruder and machine with the given material name.
+    # Returns None if:
+    #  1. the given machine doesn't have materials;
+    #  2. cannot find any material InstanceContainers with the given settings.
+    #
+    def getMaterialNode(self, machine_definition_id: str, extruder_variant_name: Optional[str],
+                        diameter: float, root_material_id: str) -> Optional["InstanceContainer"]:
+        # round the diameter to get the approximate diameter
+        rounded_diameter = str(round(diameter))
+        if rounded_diameter not in self._diameter_machine_variant_material_map:
+            Logger.log("i", "Cannot find materials with diameter [%s] (rounded to [%s]) for root material id [%s]",
+                       diameter, rounded_diameter, root_material_id)
+            return None
+
+        # If there are variant materials, get the variant material
+        machine_variant_material_map = self._diameter_machine_variant_material_map[rounded_diameter]
+        machine_node = machine_variant_material_map.get(machine_definition_id)
+        variant_node = None
+
+        # Fallback for "fdmprinter" if the machine-specific materials cannot be found
+        if machine_node is None:
+            machine_node = machine_variant_material_map.get(self._default_machine_definition_id)
+        if machine_node is not None and extruder_variant_name is not None:
+            variant_node = machine_node.getChildNode(extruder_variant_name)
+
+        # Fallback mechanism of finding materials:
+        #  1. variant-specific material
+        #  2. machine-specific material
+        #  3. generic material (for fdmprinter)
+        nodes_to_check = [variant_node, machine_node,
+                          machine_variant_material_map.get(self._default_machine_definition_id)]
+
+        material_node = None
+        for node in nodes_to_check:
+            if node is not None:
+                material_node = node.material_map.get(root_material_id)
+                if material_node:
+                    break
+
+        return material_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
+    # the generic material IDs to search for qualities.
+    #
+    # An example would be, suppose we have machine with preferred material set to "filo3d_pla" (1.75mm), but its
+    # extruders only use 2.85mm materials, then we won't be able to find the preferred material for this machine.
+    # A fallback would be to fetch a generic material of the same type "PLA" as "filo3d_pla", and in this case it will
+    # be "generic_pla". This function is intended to get a generic fallback material for the given material type.
+    #
+    # This function returns the generic root material ID for the given material type, where material types are "PLA",
+    # "ABS", etc.
+    #
+    def getFallbackMaterialIdByMaterialType(self, material_type: str) -> Optional[str]:
+        # For safety
+        if material_type not in self._fallback_materials_map:
+            Logger.log("w", "The material type [%s] does not have a fallback material" % material_type)
+            return None
+        fallback_material = self._fallback_materials_map[material_type]
+        if fallback_material:
+            return self.getRootMaterialIDWithoutDiameter(fallback_material["id"])
+        else:
+            return None
+
+    def getDefaultMaterial(self, global_stack: "GlobalStack", extruder_variant_name: 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)
+            approximate_material_diameter = str(round(material_diameter))
+            root_material_id = machine_definition.getMetaDataEntry("preferred_material")
+            root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_material_diameter)
+            node = self.getMaterialNode(machine_definition.getId(), extruder_variant_name,
+                                        material_diameter, root_material_id)
+        return node
+
+    #
+    # Methods for GUI
+    #
+
+    #
+    # Sets the new name for the given material.
+    #
+    @pyqtSlot("QVariant", str)
+    def setMaterialName(self, material_node: "MaterialNode", name: str):
+        root_material_id = material_node.metadata["base_file"]
+        if self._container_registry.isReadOnly(root_material_id):
+            Logger.log("w", "Cannot set name of read-only container %s.", root_material_id)
+            return
+
+        material_group = self.getMaterialGroup(root_material_id)
+        material_group.root_material_node.getContainer().setName(name)
+
+    #
+    # Removes the given material.
+    #
+    @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"])
+
+    #
+    # Creates a duplicate of a material, which has the same GUID and base_file metadata.
+    # Returns the root material ID of the duplicated material if successful.
+    #
+    @pyqtSlot("QVariant", result = str)
+    def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]:
+        root_material_id = material_node.metadata["base_file"]
+
+        material_group = self.getMaterialGroup(root_material_id)
+        if not material_group:
+            Logger.log("i", "Unable to duplicate the material with id %s, because it doesn't exist.", root_material_id)
+            return None
+
+        base_container = material_group.root_material_node.getContainer()
+
+        # Ensure all settings are saved.
+        self._application.saveSettings()
+
+        # Create a new ID & container to hold the data.
+        new_containers = []
+        if new_base_id is None:
+            new_base_id = self._container_registry.uniqueName(base_container.getId())
+        new_base_container = copy.deepcopy(base_container)
+        new_base_container.getMetaData()["id"] = new_base_id
+        new_base_container.getMetaData()["base_file"] = new_base_id
+        if new_metadata is not None:
+            for key, value in new_metadata.items():
+                new_base_container.getMetaData()[key] = value
+        new_containers.append(new_base_container)
+
+        # Clone all of them.
+        for node in material_group.derived_material_node_list:
+            container_to_copy = node.getContainer()
+            # Create unique IDs for every clone.
+            new_id = new_base_id
+            if container_to_copy.getMetaDataEntry("definition") != "fdmprinter":
+                new_id += "_" + container_to_copy.getMetaDataEntry("definition")
+                if container_to_copy.getMetaDataEntry("variant_name"):
+                    variant_name = container_to_copy.getMetaDataEntry("variant_name")
+                    new_id += "_" + variant_name.replace(" ", "_")
+
+            new_container = copy.deepcopy(container_to_copy)
+            new_container.getMetaData()["id"] = new_id
+            new_container.getMetaData()["base_file"] = new_base_id
+            if new_metadata is not None:
+                for key, value in new_metadata.items():
+                    new_container.getMetaData()[key] = value
+
+            new_containers.append(new_container)
+
+        for container_to_add in new_containers:
+            container_to_add.setDirty(True)
+            self._container_registry.addContainer(container_to_add)
+        return new_base_id
+
+    #
+    # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
+    #
+    @pyqtSlot(result = str)
+    def createMaterial(self) -> str:
+        from UM.i18n import i18nCatalog
+        catalog = i18nCatalog("cura")
+        # Ensure all settings are saved.
+        self._application.saveSettings()
+
+        global_stack = self._application.getGlobalContainerStack()
+        approximate_diameter = str(round(global_stack.getProperty("material_diameter", "value")))
+        root_material_id = "generic_pla"
+        root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
+        material_group = self.getMaterialGroup(root_material_id)
+
+        # Create a new ID & container to hold the data.
+        new_id = self._container_registry.uniqueName("custom_material")
+        new_metadata = {"name": catalog.i18nc("@label", "Custom Material"),
+                        "brand": catalog.i18nc("@label", "Custom"),
+                        "GUID": str(uuid.uuid4()),
+                        }
+
+        self.duplicateMaterial(material_group.root_material_node,
+                               new_base_id = new_id,
+                               new_metadata = new_metadata)
+        return new_id

+ 21 - 0
cura/Machines/MaterialNode.py

@@ -0,0 +1,21 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from typing import Optional
+
+from .ContainerNode import ContainerNode
+
+
+#
+# A MaterialNode is a node in the material lookup tree/map/table. It contains 2 (extra) fields:
+#  - material_map: a one-to-one map of "material_root_id" to material_node.
+#  - children_map: the key-value map for child nodes of this node. This is used in a lookup tree.
+#
+#
+class MaterialNode(ContainerNode):
+    __slots__ = ("material_map", "children_map")
+
+    def __init__(self, metadata: Optional[dict] = None):
+        super().__init__(metadata = metadata)
+        self.material_map = {}  # material_root_id -> material_node
+        self.children_map = {}  # mapping for the child nodes

+ 46 - 0
cura/Machines/Models/BaseMaterialsModel.py

@@ -0,0 +1,46 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
+
+from UM.Qt.ListModel import ListModel
+
+
+#
+# This is the base model class for GenericMaterialsModel and BrandMaterialsModel
+# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
+# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
+# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
+#
+class BaseMaterialsModel(ListModel):
+    RootMaterialIdRole = Qt.UserRole + 1
+    IdRole = Qt.UserRole + 2
+    NameRole = Qt.UserRole + 3
+    BrandRole = Qt.UserRole + 4
+    MaterialRole = Qt.UserRole + 5
+    ColorRole = Qt.UserRole + 6
+    ContainerNodeRole = Qt.UserRole + 7
+
+    extruderPositionChanged = pyqtSignal()
+
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self.addRoleName(self.RootMaterialIdRole, "root_material_id")
+        self.addRoleName(self.IdRole, "id")
+        self.addRoleName(self.NameRole, "name")
+        self.addRoleName(self.BrandRole, "brand")
+        self.addRoleName(self.MaterialRole, "material")
+        self.addRoleName(self.ColorRole, "color_name")
+        self.addRoleName(self.ContainerNodeRole, "container_node")
+
+        self._extruder_position = 0
+
+    def setExtruderPosition(self, position: int):
+        if self._extruder_position != position:
+            self._extruder_position = position
+            self.extruderPositionChanged.emit()
+
+    @pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged)
+    def extruderPosition(self) -> int:
+        return self._extruder_positoin

+ 131 - 0
cura/Machines/Models/BrandMaterialsModel.py

@@ -0,0 +1,131 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
+
+from UM.Qt.ListModel import ListModel
+
+from .BaseMaterialsModel import BaseMaterialsModel
+
+
+#
+# This is an intermediate model to group materials with different colours for a same brand and type.
+#
+class MaterialsModelGroupedByType(ListModel):
+    NameRole = Qt.UserRole + 1
+    ColorsRole = Qt.UserRole + 2
+
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self.addRoleName(self.NameRole, "name")
+        self.addRoleName(self.ColorsRole, "colors")
+
+
+#
+# This model is used to show branded materials in the material drop down menu.
+# The structure of the menu looks like this:
+#       Brand -> Material Type -> list of materials
+#
+# To illustrate, a branded material menu may look like this:
+#      Ultimaker -> PLA -> Yellow PLA
+#                       -> Black PLA
+#                       -> ...
+#                -> ABS -> White ABS
+#                          ...
+#
+class BrandMaterialsModel(ListModel):
+    NameRole = Qt.UserRole + 1
+    MaterialsRole = Qt.UserRole + 2
+
+    extruderPositionChanged = pyqtSignal()
+
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self.addRoleName(self.NameRole, "name")
+        self.addRoleName(self.MaterialsRole, "materials")
+
+        self._extruder_position = 0
+
+        from cura.CuraApplication import CuraApplication
+        self._machine_manager = CuraApplication.getInstance().getMachineManager()
+        self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
+        self._material_manager = CuraApplication.getInstance().getMaterialManager()
+
+        self._machine_manager.globalContainerChanged.connect(self._update)
+        self._extruder_manager.activeExtruderChanged.connect(self._update)
+        self._material_manager.materialsUpdated.connect(self._update)
+
+        self._update()
+
+    def setExtruderPosition(self, position: int):
+        if self._extruder_position != position:
+            self._extruder_position = position
+            self.extruderPositionChanged.emit()
+
+    @pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged)
+    def extruderPosition(self) -> int:
+        return self._extruder_position
+
+    def _update(self):
+        global_stack = self._machine_manager.activeMachine
+        if global_stack is None:
+            self.setItems([])
+            return
+        extruder_position = str(self._extruder_position)
+        if extruder_position not in global_stack.extruders:
+            self.setItems([])
+            return
+        extruder_stack = global_stack.extruders[str(self._extruder_position)]
+
+        available_material_dict = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack,
+                                                                                                 extruder_stack)
+        if available_material_dict is None:
+            self.setItems([])
+            return
+
+        brand_item_list = []
+        brand_group_dict = {}
+        for root_material_id, container_node in available_material_dict.items():
+            metadata = container_node.metadata
+            brand = metadata["brand"]
+            # Only add results for generic materials
+            if brand.lower() == "generic":
+                continue
+
+            if brand not in brand_group_dict:
+                brand_group_dict[brand] = {}
+
+            material_type = metadata["material"]
+            if material_type not in brand_group_dict[brand]:
+                brand_group_dict[brand][material_type] = []
+
+            item = {"root_material_id": root_material_id,
+                    "id": metadata["id"],
+                    "name": metadata["name"],
+                    "brand": metadata["brand"],
+                    "material": metadata["material"],
+                    "color_name": metadata["color_name"],
+                    "container_node": container_node
+                    }
+            brand_group_dict[brand][material_type].append(item)
+
+        for brand, material_dict in brand_group_dict.items():
+            brand_item = {"name": brand,
+                          "materials": MaterialsModelGroupedByType(self)}
+
+            material_type_item_list = []
+            for material_type, material_list in material_dict.items():
+                material_type_item = {"name": material_type,
+                                      "colors": BaseMaterialsModel(self)}
+                material_type_item["colors"].clear()
+                material_type_item["colors"].setItems(material_list)
+
+                material_type_item_list.append(material_type_item)
+
+            brand_item["materials"].setItems(material_type_item_list)
+
+            brand_item_list.append(brand_item)
+
+        self.setItems(brand_item_list)

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