Browse Source

Merge branch 'master' into WIP_improve_initialization

Diego Prado Gesto 6 years ago
parent
commit
8ad409ff55

+ 1 - 0
.gitignore

@@ -14,6 +14,7 @@ CuraEngine.exe
 LC_MESSAGES
 LC_MESSAGES
 .cache
 .cache
 *.qmlc
 *.qmlc
+.mypy_cache
 
 
 #MacOS
 #MacOS
 .DS_Store
 .DS_Store

+ 7 - 1
cmake/CuraTests.cmake

@@ -1,4 +1,4 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
 enable_testing()
 enable_testing()
@@ -53,3 +53,9 @@ foreach(_plugin ${_plugins})
         cura_add_test(NAME pytest-${_plugin_name} DIRECTORY ${_plugin_directory} PYTHONPATH "${_plugin_directory}|${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
         cura_add_test(NAME pytest-${_plugin_name} DIRECTORY ${_plugin_directory} PYTHONPATH "${_plugin_directory}|${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
     endif()
     endif()
 endforeach()
 endforeach()
+
+#Add code style test.
+add_test(
+    NAME "code-style"
+    COMMAND ${PYTHON_EXECUTABLE} run_mypy.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+)

+ 12 - 36
cura/CuraApplication.py

@@ -295,7 +295,9 @@ class CuraApplication(QtApplication):
 
 
         Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
         Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
         if not hasattr(sys, "frozen"):
         if not hasattr(sys, "frozen"):
-            Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
+            resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
+            Resources.addSearchPath(resource_path)
+            Resources.setBundledResourcesPath(resource_path)
 
 
     # Adds custom property types, settings types, and extra operators (functions) that need to be registered in
     # Adds custom property types, settings types, and extra operators (functions) that need to be registered in
     # SettingDefinition and SettingFunction.
     # SettingDefinition and SettingFunction.
@@ -439,7 +441,7 @@ class CuraApplication(QtApplication):
             "RotateTool",
             "RotateTool",
             "ScaleTool",
             "ScaleTool",
             "SelectionTool",
             "SelectionTool",
-            "TranslateTool"
+            "TranslateTool",
         ])
         ])
         self._i18n_catalog = i18nCatalog("cura")
         self._i18n_catalog = i18nCatalog("cura")
 
 
@@ -986,6 +988,8 @@ class CuraApplication(QtApplication):
         scene_bounding_box = None
         scene_bounding_box = None
         is_block_slicing_node = False
         is_block_slicing_node = False
         active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
         active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
+
+        print_information = self.getPrintInformation()
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
             if (
             if (
                 not issubclass(type(node), CuraSceneNode) or
                 not issubclass(type(node), CuraSceneNode) or
@@ -997,6 +1001,11 @@ class CuraApplication(QtApplication):
                 is_block_slicing_node = True
                 is_block_slicing_node = True
 
 
             count += 1
             count += 1
+
+            # After clicking the Undo button, if the build plate empty the project name needs to be set
+            if print_information.baseName == '':
+                print_information.setBaseName(node.getName())
+
             if not scene_bounding_box:
             if not scene_bounding_box:
                 scene_bounding_box = node.getBoundingBox()
                 scene_bounding_box = node.getBoundingBox()
             else:
             else:
@@ -1004,7 +1013,7 @@ class CuraApplication(QtApplication):
                 if other_bb is not None:
                 if other_bb is not None:
                     scene_bounding_box = scene_bounding_box + node.getBoundingBox()
                     scene_bounding_box = scene_bounding_box + node.getBoundingBox()
 
 
-        print_information = self.getPrintInformation()
+
         if print_information:
         if print_information:
             print_information.setPreSliced(is_block_slicing_node)
             print_information.setPreSliced(is_block_slicing_node)
 
 
@@ -1121,39 +1130,6 @@ class CuraApplication(QtApplication):
 
 
             Selection.add(node)
             Selection.add(node)
 
 
-    ##  Delete all nodes containing mesh data in the scene.
-    #   \param only_selectable. Set this to False to delete objects from all build plates
-    @pyqtSlot()
-    def deleteAll(self, only_selectable = True):
-        Logger.log("i", "Clearing scene")
-        if not self.getController().getToolsEnabled():
-            return
-
-        nodes = []
-        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
-            if not isinstance(node, SceneNode):
-                continue
-            if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
-                continue  # Node that doesnt have a mesh and is not a group.
-            if only_selectable and not node.isSelectable():
-                continue
-            if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not node.callDecoration("isGroup"):
-                continue  # Only remove nodes that are selectable.
-            if node.getParent() and node.getParent().callDecoration("isGroup"):
-                continue  # Grouped nodes don't need resetting as their parent (the group) is resetted)
-            nodes.append(node)
-        if nodes:
-            op = GroupedOperation()
-
-            for node in nodes:
-                op.addOperation(RemoveSceneNodeOperation(node))
-
-                # Reset the print information
-                self.getController().getScene().sceneChanged.emit(node)
-
-            op.push()
-            Selection.clear()
-
     ## Reset all translation on nodes with mesh data.
     ## Reset all translation on nodes with mesh data.
     @pyqtSlot()
     @pyqtSlot()
     def resetAllTranslation(self):
     def resetAllTranslation(self):

+ 81 - 82
cura/CuraPackageManager.py

@@ -15,12 +15,10 @@ from UM.Logger import Logger
 from UM.Resources import Resources
 from UM.Resources import Resources
 from UM.Version import Version
 from UM.Version import Version
 
 
-
 class CuraPackageManager(QObject):
 class CuraPackageManager(QObject):
     Version = 1
     Version = 1
 
 
-    # The prefix that's added to all files for an installed package to avoid naming conflicts with user created
-    # files.
+    # The prefix that's added to all files for an installed package to avoid naming conflicts with user created files.
     PREFIX_PLACE_HOLDER = "-CP;"
     PREFIX_PLACE_HOLDER = "-CP;"
 
 
     def __init__(self, parent = None):
     def __init__(self, parent = None):
@@ -31,13 +29,21 @@ class CuraPackageManager(QObject):
         self._plugin_registry = self._application.getPluginRegistry()
         self._plugin_registry = self._application.getPluginRegistry()
 
 
         # JSON file that keeps track of all installed packages.
         # JSON file that keeps track of all installed packages.
-        self._package_management_file_path = os.path.join(os.path.abspath(Resources.getDataStoragePath()),
-                                                          "packages.json")
-        self._installed_package_dict = {}  # a dict of all installed packages
-        self._to_remove_package_set = set()  # a set of packages that need to be removed at the next start
-        self._to_install_package_dict = {}  # a dict of packages that need to be installed at the next start
-
-    installedPackagesChanged = pyqtSignal()  # Emitted whenever the installed packages collection have been changed.
+        self._bundled_package_management_file_path = os.path.join(
+            os.path.abspath(Resources.getBundledResourcesPath()),
+            "packages.json"
+        )
+        self._user_package_management_file_path = os.path.join(
+            os.path.abspath(Resources.getDataStoragePath()),
+            "packages.json"
+        )
+
+        self._bundled_package_dict = {}     # A dict of all bundled packages
+        self._installed_package_dict = {}   # A dict of all installed packages
+        self._to_remove_package_set = set() # A set of packages that need to be removed at the next start
+        self._to_install_package_dict = {}  # A dict of packages that need to be installed at the next start
+
+    installedPackagesChanged = pyqtSignal() # Emitted whenever the installed packages collection have been changed.
 
 
     def initialize(self):
     def initialize(self):
         self._loadManagementData()
         self._loadManagementData()
@@ -46,48 +52,62 @@ class CuraPackageManager(QObject):
 
 
     # (for initialize) Loads the package management file if exists
     # (for initialize) Loads the package management file if exists
     def _loadManagementData(self) -> None:
     def _loadManagementData(self) -> None:
-        if not os.path.exists(self._package_management_file_path):
-            Logger.log("i", "Package management file %s doesn't exist, do nothing", self._package_management_file_path)
+        # The bundles package management file should always be there
+        if not os.path.exists(self._bundled_package_management_file_path):
+            Logger.log("w", "Bundled package management file could not be found!")
+            return
+        # Load the bundled packages:
+        with open(self._bundled_package_management_file_path, "r", encoding = "utf-8") as f:
+            self._bundled_package_dict = json.load(f, encoding = "utf-8")
+            Logger.log("i", "Loaded bundled packages data from %s", self._bundled_package_management_file_path)
+
+        # Load the user package management file
+        if not os.path.exists(self._user_package_management_file_path):
+            Logger.log("i", "User package management file %s doesn't exist, do nothing", self._user_package_management_file_path)
             return
             return
 
 
         # Need to use the file lock here to prevent concurrent I/O from other processes/threads
         # Need to use the file lock here to prevent concurrent I/O from other processes/threads
         container_registry = self._application.getContainerRegistry()
         container_registry = self._application.getContainerRegistry()
         with container_registry.lockFile():
         with container_registry.lockFile():
-            with open(self._package_management_file_path, "r", encoding = "utf-8") as f:
-                management_dict = json.load(f, encoding = "utf-8")
 
 
+            # Load the user packages:
+            with open(self._user_package_management_file_path, "r", encoding="utf-8") as f:
+                management_dict = json.load(f, encoding="utf-8")
                 self._installed_package_dict = management_dict.get("installed", {})
                 self._installed_package_dict = management_dict.get("installed", {})
                 self._to_remove_package_set = set(management_dict.get("to_remove", []))
                 self._to_remove_package_set = set(management_dict.get("to_remove", []))
                 self._to_install_package_dict = management_dict.get("to_install", {})
                 self._to_install_package_dict = management_dict.get("to_install", {})
-
-                Logger.log("i", "Package management file %s is loaded", self._package_management_file_path)
+                Logger.log("i", "Loaded user packages management file from %s", self._user_package_management_file_path)
 
 
     def _saveManagementData(self) -> None:
     def _saveManagementData(self) -> None:
         # Need to use the file lock here to prevent concurrent I/O from other processes/threads
         # Need to use the file lock here to prevent concurrent I/O from other processes/threads
         container_registry = self._application.getContainerRegistry()
         container_registry = self._application.getContainerRegistry()
         with container_registry.lockFile():
         with container_registry.lockFile():
-            with open(self._package_management_file_path, "w", encoding = "utf-8") as f:
+            with open(self._user_package_management_file_path, "w", encoding = "utf-8") as f:
                 data_dict = {"version": CuraPackageManager.Version,
                 data_dict = {"version": CuraPackageManager.Version,
                              "installed": self._installed_package_dict,
                              "installed": self._installed_package_dict,
                              "to_remove": list(self._to_remove_package_set),
                              "to_remove": list(self._to_remove_package_set),
                              "to_install": self._to_install_package_dict}
                              "to_install": self._to_install_package_dict}
                 data_dict["to_remove"] = list(data_dict["to_remove"])
                 data_dict["to_remove"] = list(data_dict["to_remove"])
-                json.dump(data_dict, f)
-                Logger.log("i", "Package management file %s is saved", self._package_management_file_path)
+                json.dump(data_dict, f, sort_keys = True, indent = 4)
+                Logger.log("i", "Package management file %s was saved", self._user_package_management_file_path)
 
 
     # (for initialize) Removes all packages that have been scheduled to be removed.
     # (for initialize) Removes all packages that have been scheduled to be removed.
     def _removeAllScheduledPackages(self) -> None:
     def _removeAllScheduledPackages(self) -> None:
         for package_id in self._to_remove_package_set:
         for package_id in self._to_remove_package_set:
             self._purgePackage(package_id)
             self._purgePackage(package_id)
+            del self._installed_package_dict[package_id]
         self._to_remove_package_set.clear()
         self._to_remove_package_set.clear()
         self._saveManagementData()
         self._saveManagementData()
 
 
     # (for initialize) Installs all packages that have been scheduled to be installed.
     # (for initialize) Installs all packages that have been scheduled to be installed.
     def _installAllScheduledPackages(self) -> None:
     def _installAllScheduledPackages(self) -> None:
-        for package_id, installation_package_data in self._to_install_package_dict.items():
-            self._installPackage(installation_package_data)
-        self._to_install_package_dict.clear()
-        self._saveManagementData()
+
+        while self._to_install_package_dict:
+            package_id, package_info = list(self._to_install_package_dict.items())[0]
+            self._installPackage(package_info)
+            self._installed_package_dict[package_id] = self._to_install_package_dict[package_id]
+            del self._to_install_package_dict[package_id]
+            self._saveManagementData()
 
 
     # Checks the given package is installed. If so, return a dictionary that contains the package's information.
     # Checks the given package is installed. If so, return a dictionary that contains the package's information.
     def getInstalledPackageInfo(self, package_id: str) -> Optional[dict]:
     def getInstalledPackageInfo(self, package_id: str) -> Optional[dict]:
@@ -99,87 +119,66 @@ class CuraPackageManager(QObject):
             return package_info
             return package_info
 
 
         if package_id in self._installed_package_dict:
         if package_id in self._installed_package_dict:
-            package_info = self._installed_package_dict.get(package_id)
+            package_info = self._installed_package_dict[package_id]["package_info"]
             return package_info
             return package_info
 
 
-        for section, packages in self.getAllInstalledPackagesInfo().items():
-            for package in packages:
-                if package["package_id"] == package_id:
-                    return package
+        if package_id in self._bundled_package_dict:
+            package_info = self._bundled_package_dict[package_id]["package_info"]
+            return package_info
 
 
         return None
         return None
 
 
     def getAllInstalledPackagesInfo(self) -> dict:
     def getAllInstalledPackagesInfo(self) -> dict:
-        installed_package_id_set = set(self._installed_package_dict.keys()) | set(self._to_install_package_dict.keys())
-        installed_package_id_set = installed_package_id_set.difference(self._to_remove_package_set)
+        # Add bundled, installed, and to-install packages to the set of installed package IDs
+        all_installed_ids = set()
 
 
-        managed_package_id_set = installed_package_id_set | self._to_remove_package_set
-
-        # TODO: For absolutely no reason, this function seems to run in a loop
-        # even though no loop is ever called with it.
+        if self._bundled_package_dict.keys():
+            all_installed_ids = all_installed_ids.union(set(self._bundled_package_dict.keys()))
+        if self._installed_package_dict.keys():
+            all_installed_ids = all_installed_ids.union(set(self._installed_package_dict.keys()))
+        if self._to_install_package_dict.keys():
+            all_installed_ids = all_installed_ids.union(set(self._to_install_package_dict.keys()))
+        all_installed_ids = all_installed_ids.difference(self._to_remove_package_set)
 
 
         # map of <package_type> -> <package_id> -> <package_info>
         # map of <package_type> -> <package_id> -> <package_info>
         installed_packages_dict = {}
         installed_packages_dict = {}
-        for package_id in installed_package_id_set:
+        for package_id in all_installed_ids:
+
+            # Skip required plugins as they should not be tampered with
             if package_id in Application.getInstance().getRequiredPlugins():
             if package_id in Application.getInstance().getRequiredPlugins():
                 continue
                 continue
+
+            package_info = None
+            # Add bundled plugins
+            if package_id in self._bundled_package_dict:
+                package_info = self._bundled_package_dict[package_id]["package_info"]
+
+            # Add installed plugins
+            if package_id in self._installed_package_dict:
+                package_info = self._installed_package_dict[package_id]["package_info"]
+
+            # Add to install plugins
             if package_id in self._to_install_package_dict:
             if package_id in self._to_install_package_dict:
                 package_info = self._to_install_package_dict[package_id]["package_info"]
                 package_info = self._to_install_package_dict[package_id]["package_info"]
-            else:
-                package_info = self._installed_package_dict[package_id]
-            package_info["is_bundled"] = False
 
 
-            package_type = package_info["package_type"]
-            if package_type not in installed_packages_dict:
-                installed_packages_dict[package_type] = []
-            installed_packages_dict[package_type].append( package_info )
+            if package_info is None:
+                continue
 
 
             # We also need to get information from the plugin registry such as if a plugin is active
             # We also need to get information from the plugin registry such as if a plugin is active
             package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id)
             package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id)
 
 
-        # Also get all bundled plugins
-        all_metadata = self._plugin_registry.getAllMetaData()
-        for item in all_metadata:
-            if item == {}:
-                continue
-
-            plugin_package_info = self.__convertPluginMetadataToPackageMetadata(item)
-            # Only gather the bundled plugins here.
-            package_id = plugin_package_info["package_id"]
-            if package_id in managed_package_id_set:
-                continue
-            if package_id in Application.getInstance().getRequiredPlugins():
-                continue
+            # If the package ID is in bundled, label it as such
+            package_info["is_bundled"] = package_info["package_id"] in self._bundled_package_dict.keys()
 
 
-            plugin_package_info["is_bundled"] = True if plugin_package_info["author"]["display_name"] == "Ultimaker B.V." else False
-            plugin_package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id)
-            package_type = "plugin"
-            if package_type not in installed_packages_dict:
-                installed_packages_dict[package_type] = []
-            installed_packages_dict[package_type].append( plugin_package_info )
+            # If there is not a section in the dict for this type, add it
+            if package_info["package_type"] not in installed_packages_dict:
+                installed_packages_dict[package_info["package_type"]] = []
+                
+            # Finally, add the data
+            installed_packages_dict[package_info["package_type"]].append(package_info)
 
 
         return installed_packages_dict
         return installed_packages_dict
 
 
-    def __convertPluginMetadataToPackageMetadata(self, plugin_metadata: dict) -> dict:
-        package_metadata = {
-            "package_id": plugin_metadata["id"],
-            "package_type": "plugin",
-            "display_name": plugin_metadata["plugin"]["name"],
-            "description": plugin_metadata["plugin"].get("description"),
-            "package_version": plugin_metadata["plugin"]["version"],
-            "cura_version": int(plugin_metadata["plugin"]["api"]),
-            "website": "",
-            "author_id": plugin_metadata["plugin"].get("author", "UnknownID"),
-            "author": {
-                "author_id": plugin_metadata["plugin"].get("author", "UnknownID"),
-                "display_name": plugin_metadata["plugin"].get("author", ""),
-                "email": "",
-                "website": "",
-            },
-            "tags": ["plugin"],
-        }
-        return package_metadata
-
     # Checks if the given package is installed.
     # Checks if the given package is installed.
     def isPackageInstalled(self, package_id: str) -> bool:
     def isPackageInstalled(self, package_id: str) -> bool:
         return self.getInstalledPackageInfo(package_id) is not None
         return self.getInstalledPackageInfo(package_id) is not None
@@ -293,7 +292,7 @@ class CuraPackageManager(QObject):
             from cura.CuraApplication import CuraApplication
             from cura.CuraApplication import CuraApplication
             installation_dirs_dict = {
             installation_dirs_dict = {
                 "materials": Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer),
                 "materials": Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer),
-                "quality": Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer),
+                "qualities": Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer),
                 "plugins": os.path.abspath(Resources.getStoragePath(Resources.Plugins)),
                 "plugins": os.path.abspath(Resources.getStoragePath(Resources.Plugins)),
             }
             }
 
 

+ 16 - 14
cura/PrintInformation.py

@@ -14,6 +14,8 @@ from UM.i18n import i18nCatalog
 from UM.Logger import Logger
 from UM.Logger import Logger
 from UM.Qt.Duration import Duration
 from UM.Qt.Duration import Duration
 from UM.Scene.SceneNode import SceneNode
 from UM.Scene.SceneNode import SceneNode
+from UM.i18n import i18nCatalog
+from UM.MimeTypeDatabase import MimeTypeDatabase
 
 
 catalog = i18nCatalog("cura")
 catalog = i18nCatalog("cura")
 
 
@@ -321,7 +323,7 @@ class PrintInformation(QObject):
 
 
         # when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
         # when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
         # extension. This cuts the extension off if necessary.
         # extension. This cuts the extension off if necessary.
-        name = os.path.splitext(name)[0]
+        check_name = os.path.splitext(name)[0]
         filename_parts = os.path.basename(base_name).split(".")
         filename_parts = os.path.basename(base_name).split(".")
 
 
         # If it's a gcode, also always update the job name
         # If it's a gcode, also always update the job name
@@ -332,21 +334,21 @@ class PrintInformation(QObject):
 
 
         # if this is a profile file, always update the job name
         # if this is a profile file, always update the job name
         # name is "" when I first had some meshes and afterwards I deleted them so the naming should start again
         # name is "" when I first had some meshes and afterwards I deleted them so the naming should start again
-        is_empty = name == ""
-        if is_gcode or is_project_file or (is_empty or (self._base_name == "" and self._base_name != name)):
+        is_empty = check_name == ""
+        if is_gcode or is_project_file or (is_empty or (self._base_name == "" and self._base_name != check_name)):
             # Only take the file name part, Note : file name might have 'dot' in name as well
             # Only take the file name part, Note : file name might have 'dot' in name as well
-            if is_project_file:
-                # This is for .curaproject
-                self._base_name = ".".join(filename_parts)
-            elif len(filename_parts) > 1:
-                if "gcode" in filename_parts:
-                    gcode_index = filename_parts.index('gcode')
-                    self._base_name = ".".join(filename_parts[0:gcode_index])
-                else:
-                    self._base_name = name
-            else:
-                self._base_name = name
 
 
+            data = ''
+            try:
+                mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
+                data = mime_type.stripExtension(name)
+            except:
+                Logger.log("w", "Unsupported Mime Type Database file extension")
+
+            if data is not None:
+                self._base_name = data
+            else:
+                self._base_name = ''
 
 
             self._updateJobName()
             self._updateJobName()
 
 

+ 2 - 1
cura/Settings/ExtruderManager.py

@@ -385,7 +385,8 @@ class ExtruderManager(QObject):
 
 
             # Register the extruder trains by position
             # Register the extruder trains by position
             for extruder_train in extruder_trains:
             for extruder_train in extruder_trains:
-                self._extruder_trains[global_stack_id][extruder_train.getMetaDataEntry("position")] = extruder_train
+                extruder_position = extruder_train.getMetaDataEntry("position")
+                self._extruder_trains[global_stack_id][extruder_position] = extruder_train
 
 
                 # regardless of what the next stack is, we have to set it again, because of signal routing. ???
                 # regardless of what the next stack is, we have to set it again, because of signal routing. ???
                 extruder_train.setParent(global_stack)
                 extruder_train.setParent(global_stack)

+ 1 - 1
cura/Settings/ExtruderStack.py

@@ -38,7 +38,7 @@ class ExtruderStack(CuraContainerStack):
     #
     #
     #   This will set the next stack and ensure that we register this stack as an extruder.
     #   This will set the next stack and ensure that we register this stack as an extruder.
     @override(ContainerStack)
     @override(ContainerStack)
-    def setNextStack(self, stack: CuraContainerStack) -> None:
+    def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
         super().setNextStack(stack)
         super().setNextStack(stack)
         stack.addExtruder(self)
         stack.addExtruder(self)
         self.addMetaDataEntry("machine", stack.id)
         self.addMetaDataEntry("machine", stack.id)

+ 18 - 1
cura/Settings/GlobalStack.py

@@ -125,7 +125,7 @@ class GlobalStack(CuraContainerStack):
     #
     #
     #   This will simply raise an exception since the Global stack cannot have a next stack.
     #   This will simply raise an exception since the Global stack cannot have a next stack.
     @override(ContainerStack)
     @override(ContainerStack)
-    def setNextStack(self, next_stack: ContainerStack) -> None:
+    def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None:
         raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
         raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!")
 
 
     # protected:
     # protected:
@@ -153,6 +153,23 @@ class GlobalStack(CuraContainerStack):
 
 
         return True
         return True
 
 
+    ##  Perform some sanity checks on the global stack
+    #   Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1
+    def isValid(self):
+        container_registry = ContainerRegistry.getInstance()
+        extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId())
+
+        machine_extruder_count = self.getProperty("machine_extruder_count", "value")
+        extruder_check_position = set()
+        for extruder_train in extruder_trains:
+            extruder_position = extruder_train.getMetaDataEntry("position")
+            extruder_check_position.add(extruder_position)
+
+        for check_position in range(machine_extruder_count):
+            if str(check_position) not in extruder_check_position:
+                return False
+        return True
+
 
 
 ## private:
 ## private:
 global_stack_mime = MimeType(
 global_stack_mime = MimeType(

+ 5 - 2
cura/Settings/MachineManager.py

@@ -6,6 +6,7 @@ import time
 #Type hinting.
 #Type hinting.
 from typing import List, Dict, TYPE_CHECKING, Optional
 from typing import List, Dict, TYPE_CHECKING, Optional
 
 
+from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
 from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
 from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
 from UM.Settings.InstanceContainer import InstanceContainer
 from UM.Settings.InstanceContainer import InstanceContainer
 from UM.Settings.Interfaces import ContainerInterface
 from UM.Settings.Interfaces import ContainerInterface
@@ -166,8 +167,6 @@ class MachineManager(QObject):
         if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacksMetadata(id = active_machine_id):
         if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacksMetadata(id = active_machine_id):
             # An active machine was saved, so restore it.
             # An active machine was saved, so restore it.
             self.setActiveMachine(active_machine_id)
             self.setActiveMachine(active_machine_id)
-            # Make sure _active_container_stack is properly initiated
-            ExtruderManager.getInstance().setActiveExtruderIndex(0)
 
 
     def _onOutputDevicesChanged(self) -> None:
     def _onOutputDevicesChanged(self) -> None:
         self._printer_output_devices = []
         self._printer_output_devices = []
@@ -358,6 +357,10 @@ class MachineManager(QObject):
             return
             return
 
 
         global_stack = containers[0]
         global_stack = containers[0]
+        if not global_stack.isValid():
+            # Mark global stack as invalid
+            ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
+            return  # We're done here
         ExtruderManager.getInstance().setActiveExtruderIndex(0)  # Switch to first extruder
         ExtruderManager.getInstance().setActiveExtruderIndex(0)  # Switch to first extruder
         self._global_container_stack = global_stack
         self._global_container_stack = global_stack
         self._application.setGlobalContainerStack(global_stack)
         self._application.setGlobalContainerStack(global_stack)

+ 10 - 0
plugins/3MFReader/ThreeMFReader.py

@@ -15,6 +15,7 @@ from UM.Math.Vector import Vector
 from UM.Mesh.MeshBuilder import MeshBuilder
 from UM.Mesh.MeshBuilder import MeshBuilder
 from UM.Mesh.MeshReader import MeshReader
 from UM.Mesh.MeshReader import MeshReader
 from UM.Scene.GroupDecorator import GroupDecorator
 from UM.Scene.GroupDecorator import GroupDecorator
+from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
 
 
 from cura.Settings.ExtruderManager import ExtruderManager
 from cura.Settings.ExtruderManager import ExtruderManager
 from cura.Scene.CuraSceneNode import CuraSceneNode
 from cura.Scene.CuraSceneNode import CuraSceneNode
@@ -25,6 +26,15 @@ from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
 
 
 MYPY = False
 MYPY = False
 
 
+
+MimeTypeDatabase.addMimeType(
+    MimeType(
+        name = "application/x-cura-project-file",
+        comment = "Cura Project File",
+        suffixes = ["curaproject.3mf"]
+    )
+)
+
 try:
 try:
     if not MYPY:
     if not MYPY:
         import xml.etree.cElementTree as ET
         import xml.etree.cElementTree as ET

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