Browse Source

Merge branch 'master' into mypy_fixes

Contributes to CURA-5330
Diego Prado Gesto 6 years ago
parent
commit
e5e96bc600

+ 8 - 6
cura/Arranging/Arrange.py

@@ -3,6 +3,7 @@
 
 
 from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
 from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
 from UM.Logger import Logger
 from UM.Logger import Logger
+from UM.Math.Polygon import Polygon
 from UM.Math.Vector import Vector
 from UM.Math.Vector import Vector
 from cura.Arranging.ShapeArray import ShapeArray
 from cura.Arranging.ShapeArray import ShapeArray
 from cura.Scene import ZOffsetDecorator
 from cura.Scene import ZOffsetDecorator
@@ -45,7 +46,7 @@ class Arrange:
     #   \param scene_root   Root for finding all scene nodes
     #   \param scene_root   Root for finding all scene nodes
     #   \param fixed_nodes  Scene nodes to be placed
     #   \param fixed_nodes  Scene nodes to be placed
     @classmethod
     @classmethod
-    def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250):
+    def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8):
         arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
         arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
         arranger.centerFirst()
         arranger.centerFirst()
 
 
@@ -58,9 +59,10 @@ class Arrange:
 
 
         # Place all objects fixed nodes
         # Place all objects fixed nodes
         for fixed_node in fixed_nodes:
         for fixed_node in fixed_nodes:
-            vertices = fixed_node.callDecoration("getConvexHull")
+            vertices = fixed_node.callDecoration("getConvexHullHead") or fixed_node.callDecoration("getConvexHull")
             if not vertices:
             if not vertices:
                 continue
                 continue
+            vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
             points = copy.deepcopy(vertices._points)
             points = copy.deepcopy(vertices._points)
             shape_arr = ShapeArray.fromPolygon(points, scale = scale)
             shape_arr = ShapeArray.fromPolygon(points, scale = scale)
             arranger.place(0, 0, shape_arr)
             arranger.place(0, 0, shape_arr)
@@ -81,12 +83,12 @@ class Arrange:
     ##  Find placement for a node (using offset shape) and place it (using hull shape)
     ##  Find placement for a node (using offset shape) and place it (using hull shape)
     #   return the nodes that should be placed
     #   return the nodes that should be placed
     #   \param node
     #   \param node
-    #   \param offset_shape_arr ShapeArray with offset, used to find location
-    #   \param hull_shape_arr ShapeArray without offset, for placing the shape
+    #   \param offset_shape_arr ShapeArray with offset, for placing the shape
+    #   \param hull_shape_arr ShapeArray without offset, used to find location
     def findNodePlacement(self, node, offset_shape_arr, hull_shape_arr, step = 1):
     def findNodePlacement(self, node, offset_shape_arr, hull_shape_arr, step = 1):
         new_node = copy.deepcopy(node)
         new_node = copy.deepcopy(node)
         best_spot = self.bestSpot(
         best_spot = self.bestSpot(
-            offset_shape_arr, start_prio = self._last_priority, step = step)
+            hull_shape_arr, start_prio = self._last_priority, step = step)
         x, y = best_spot.x, best_spot.y
         x, y = best_spot.x, best_spot.y
 
 
         # Save the last priority.
         # Save the last priority.
@@ -102,7 +104,7 @@ class Arrange:
         if x is not None:  # We could find a place
         if x is not None:  # We could find a place
             new_node.setPosition(Vector(x, center_y, y))
             new_node.setPosition(Vector(x, center_y, y))
             found_spot = True
             found_spot = True
-            self.place(x, y, hull_shape_arr)  # place the object in arranger
+            self.place(x, y, offset_shape_arr)  # place the object in arranger
         else:
         else:
             Logger.log("d", "Could not find spot!"),
             Logger.log("d", "Could not find spot!"),
             found_spot = False
             found_spot = False

+ 2 - 2
cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py

@@ -110,7 +110,7 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
                     arrange_array.add()
                     arrange_array.add()
                 arranger = arrange_array.get(current_build_plate_number)
                 arranger = arrange_array.get(current_build_plate_number)
 
 
-                best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
+                best_spot = arranger.bestSpot(hull_shape_arr, start_prio=start_priority)
                 x, y = best_spot.x, best_spot.y
                 x, y = best_spot.x, best_spot.y
                 node.removeDecorator(ZOffsetDecorator)
                 node.removeDecorator(ZOffsetDecorator)
                 if node.getBoundingBox():
                 if node.getBoundingBox():
@@ -118,7 +118,7 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
                 else:
                 else:
                     center_y = 0
                     center_y = 0
                 if x is not None:  # We could find a place
                 if x is not None:  # We could find a place
-                    arranger.place(x, y, hull_shape_arr)  # place the object in the arranger
+                    arranger.place(x, y, offset_shape_arr)  # place the object in the arranger
 
 
                     node.callDecoration("setBuildPlateNumber", current_build_plate_number)
                     node.callDecoration("setBuildPlateNumber", current_build_plate_number)
                     grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
                     grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))

+ 3 - 3
cura/Arranging/ArrangeObjectsJob.py

@@ -37,7 +37,7 @@ class ArrangeObjectsJob(Job):
         machine_width = global_container_stack.getProperty("machine_width", "value")
         machine_width = global_container_stack.getProperty("machine_width", "value")
         machine_depth = global_container_stack.getProperty("machine_depth", "value")
         machine_depth = global_container_stack.getProperty("machine_depth", "value")
 
 
-        arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes)
+        arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset)
 
 
         # Collect nodes to be placed
         # Collect nodes to be placed
         nodes_arr = []  # fill with (size, node, offset_shape_arr, hull_shape_arr)
         nodes_arr = []  # fill with (size, node, offset_shape_arr, hull_shape_arr)
@@ -66,7 +66,7 @@ class ArrangeObjectsJob(Job):
                 start_priority = last_priority
                 start_priority = last_priority
             else:
             else:
                 start_priority = 0
                 start_priority = 0
-            best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
+            best_spot = arranger.bestSpot(hull_shape_arr, start_prio = start_priority)
             x, y = best_spot.x, best_spot.y
             x, y = best_spot.x, best_spot.y
             node.removeDecorator(ZOffsetDecorator)
             node.removeDecorator(ZOffsetDecorator)
             if node.getBoundingBox():
             if node.getBoundingBox():
@@ -77,7 +77,7 @@ class ArrangeObjectsJob(Job):
                 last_size = size
                 last_size = size
                 last_priority = best_spot.priority
                 last_priority = best_spot.priority
 
 
-                arranger.place(x, y, hull_shape_arr)  # take place before the next one
+                arranger.place(x, y, offset_shape_arr)  # take place before the next one
                 grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
                 grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
             else:
             else:
                 Logger.log("d", "Arrange all: could not find spot!")
                 Logger.log("d", "Arrange all: could not find spot!")

+ 40 - 11
cura/CuraApplication.py

@@ -199,7 +199,6 @@ class CuraApplication(QtApplication):
         self._platform_activity = False
         self._platform_activity = False
         self._scene_bounding_box = AxisAlignedBox.Null
         self._scene_bounding_box = AxisAlignedBox.Null
 
 
-        self._job_name = None
         self._center_after_select = False
         self._center_after_select = False
         self._camera_animation = None
         self._camera_animation = None
         self._cura_actions = None
         self._cura_actions = None
@@ -261,20 +260,16 @@ class CuraApplication(QtApplication):
             self._files_to_open.append(os.path.abspath(filename))
             self._files_to_open.append(os.path.abspath(filename))
 
 
     def initialize(self) -> None:
     def initialize(self) -> None:
+        self.__addExpectedResourceDirsAndSearchPaths()  # Must be added before init of super
+
         super().initialize()
         super().initialize()
 
 
         self.__sendCommandToSingleInstance()
         self.__sendCommandToSingleInstance()
-        self.__addExpectedResourceDirsAndSearchPaths()
         self.__initializeSettingDefinitionsAndFunctions()
         self.__initializeSettingDefinitionsAndFunctions()
         self.__addAllResourcesAndContainerResources()
         self.__addAllResourcesAndContainerResources()
         self.__addAllEmptyContainers()
         self.__addAllEmptyContainers()
         self.__setLatestResouceVersionsForVersionUpgrade()
         self.__setLatestResouceVersionsForVersionUpgrade()
 
 
-        # Initialize the package manager to remove and install scheduled packages.
-        from cura.CuraPackageManager import CuraPackageManager
-        self._cura_package_manager = CuraPackageManager(self)
-        self._cura_package_manager.initialize()
-
         self._machine_action_manager = MachineActionManager.MachineActionManager(self)
         self._machine_action_manager = MachineActionManager.MachineActionManager(self)
         self._machine_action_manager.initialize()
         self._machine_action_manager.initialize()
 
 
@@ -325,6 +320,7 @@ class CuraApplication(QtApplication):
         SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
         SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
         SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
         SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
         SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
         SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
+        SettingFunction.registerOperator("defaultExtruderPosition", ExtruderManager.getDefaultExtruderPosition)
 
 
     # Adds all resources and container related resources.
     # Adds all resources and container related resources.
     def __addAllResourcesAndContainerResources(self) -> None:
     def __addAllResourcesAndContainerResources(self) -> None:
@@ -408,6 +404,43 @@ class CuraApplication(QtApplication):
             }
             }
         )
         )
 
 
+        """
+        self._currently_loading_files = []
+        self._non_sliceable_extensions = []
+
+        self._machine_action_manager = MachineActionManager.MachineActionManager()
+        self._machine_manager = None    # This is initialized on demand.
+        self._extruder_manager = None
+        self._material_manager = None
+        self._quality_manager = None
+        self._object_manager = None
+        self._build_plate_model = None
+        self._multi_build_plate_model = None
+        self._setting_visibility_presets_model = None
+        self._setting_inheritance_manager = None
+        self._simple_mode_settings_manager = None
+        self._cura_scene_controller = None
+        self._machine_error_checker = None
+        self._auto_save = None
+        self._save_data_enabled = True
+
+        self._additional_components = {} # Components to add to certain areas in the interface
+
+        super().__init__(name = "cura",
+                         version = CuraVersion,
+                         buildtype = CuraBuildType,
+                         is_debug_mode = CuraDebugMode,
+                         tray_icon_name = "cura-icon-32.png",
+                         **kwargs)
+
+        # FOR TESTING ONLY
+        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"
+        """
     # Runs preparations that needs to be done before the starting process.
     # Runs preparations that needs to be done before the starting process.
     def startSplashWindowPhase(self):
     def startSplashWindowPhase(self):
         super().startSplashWindowPhase()
         super().startSplashWindowPhase()
@@ -788,10 +821,6 @@ class CuraApplication(QtApplication):
             self._extruder_manager = ExtruderManager()
             self._extruder_manager = ExtruderManager()
         return self._extruder_manager
         return self._extruder_manager
 
 
-    @pyqtSlot(result = QObject)
-    def getCuraPackageManager(self, *args):
-        return self._cura_package_manager
-
     def getVariantManager(self, *args):
     def getVariantManager(self, *args):
         return self._variant_manager
         return self._variant_manager
 
 

+ 6 - 355
cura/CuraPackageManager.py

@@ -1,363 +1,14 @@
 # Copyright (c) 2018 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.
 
 
-from typing import Optional, Dict, Any
-import json
-import os
-import shutil
-import zipfile
-import tempfile
+from cura.CuraApplication import CuraApplication #To find some resource types.
+from UM.PackageManager import PackageManager #The class we're extending.
+from UM.Resources import Resources #To find storage paths for some resource types.
 
 
-from PyQt5.QtCore import pyqtSlot, QObject, pyqtSignal, QUrl
-
-from UM.Application import Application
-from UM.Logger import Logger
-from UM.Resources import Resources
-from UM.Version import Version
-
-
-class CuraPackageManager(QObject):
-    Version = 1
 
 
+class CuraPackageManager(PackageManager):
     def __init__(self, parent = None):
     def __init__(self, parent = None):
         super().__init__(parent)
         super().__init__(parent)
 
 
-        self._application = Application.getInstance()
-        self._container_registry = self._application.getContainerRegistry()
-        self._plugin_registry = self._application.getPluginRegistry()
-
-        #JSON files that keep track of all installed packages.
-        self._user_package_management_file_path = None #type: str
-        self._bundled_package_management_file_path = None #type: str
-        for search_path in Resources.getSearchPaths():
-            candidate_bundled_path = os.path.join(search_path, "bundled_packages.json")
-            if os.path.exists(candidate_bundled_path):
-                self._bundled_package_management_file_path = candidate_bundled_path
-        for search_path in (Resources.getDataStoragePath(), Resources.getConfigStoragePath()):
-            candidate_user_path = os.path.join(search_path, "packages.json")
-            if os.path.exists(candidate_user_path):
-                self._user_package_management_file_path = candidate_user_path
-        if self._user_package_management_file_path is None: #Doesn't exist yet.
-            self._user_package_management_file_path = os.path.join(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):
-        self._loadManagementData()
-        self._removeAllScheduledPackages()
-        self._installAllScheduledPackages()
-
-    # (for initialize) Loads the package management file if exists
-    def _loadManagementData(self) -> None:
-        # 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
-
-        # Need to use the file lock here to prevent concurrent I/O from other processes/threads
-        container_registry = self._application.getContainerRegistry()
-        with container_registry.lockFile():
-
-            # 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._to_remove_package_set = set(management_dict.get("to_remove", []))
-                self._to_install_package_dict = management_dict.get("to_install", {})
-                Logger.log("i", "Loaded user packages management file from %s", self._user_package_management_file_path)
-
-    def _saveManagementData(self) -> None:
-        # Need to use the file lock here to prevent concurrent I/O from other processes/threads
-        container_registry = self._application.getContainerRegistry()
-        with container_registry.lockFile():
-            with open(self._user_package_management_file_path, "w", encoding = "utf-8") as f:
-                data_dict = {"version": CuraPackageManager.Version,
-                             "installed": self._installed_package_dict,
-                             "to_remove": list(self._to_remove_package_set),
-                             "to_install": self._to_install_package_dict}
-                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.
-    def _removeAllScheduledPackages(self) -> None:
-        for package_id in self._to_remove_package_set:
-            self._purgePackage(package_id)
-            del self._installed_package_dict[package_id]
-        self._to_remove_package_set.clear()
-        self._saveManagementData()
-
-    # (for initialize) Installs all packages that have been scheduled to be installed.
-    def _installAllScheduledPackages(self) -> None:
-        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()
-
-    def getBundledPackageInfo(self, package_id: str) -> Optional[dict]:
-        package_info = None
-        if package_id in self._bundled_package_dict:
-            package_info = self._bundled_package_dict[package_id]["package_info"]
-        return package_info
-
-    # 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]:
-        if package_id in self._to_remove_package_set:
-            return None
-
-        if package_id in self._to_install_package_dict:
-            package_info = self._to_install_package_dict[package_id]["package_info"]
-            return package_info
-
-        if package_id in self._installed_package_dict:
-            package_info = self._installed_package_dict[package_id]["package_info"]
-            return package_info
-
-        if package_id in self._bundled_package_dict:
-            package_info = self._bundled_package_dict[package_id]["package_info"]
-            return package_info
-
-        return None
-
-    def getAllInstalledPackagesInfo(self) -> dict:
-        # Add bundled, installed, and to-install packages to the set of installed package IDs
-        all_installed_ids = set()
-
-        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()))
-        all_installed_ids = all_installed_ids.difference(self._to_remove_package_set)
-        # If it's going to be installed and to be removed, then the package is being updated and it should be listed.
-        if self._to_install_package_dict.keys():
-            all_installed_ids = all_installed_ids.union(set(self._to_install_package_dict.keys()))
-
-        # map of <package_type> -> <package_id> -> <package_info>
-        installed_packages_dict = {}
-        for package_id in all_installed_ids:
-            # Skip required plugins as they should not be tampered with
-            if package_id in Application.getInstance().getRequiredPlugins():
-                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:
-                package_info = self._to_install_package_dict[package_id]["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
-            package_info["is_active"] = self._plugin_registry.isActivePlugin(package_id)
-
-            # 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() and not self.isUserInstalledPackage(package_info["package_id"])
-
-            # 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
-
-    # Checks if the given package is installed (at all).
-    def isPackageInstalled(self, package_id: str) -> bool:
-        return self.getInstalledPackageInfo(package_id) is not None
-
-    # This is called by drag-and-dropping curapackage files.
-    @pyqtSlot(QUrl)
-    def installPackageViaDragAndDrop(self, file_url: str) -> None:
-        filename = QUrl(file_url).toLocalFile()
-        return self.installPackage(filename)
-
-    # Schedules the given package file to be installed upon the next start.
-    @pyqtSlot(str)
-    def installPackage(self, filename: str) -> None:
-        has_changes = False
-        try:
-            # Get package information
-            package_info = self.getPackageInfo(filename)
-            if not package_info:
-                return
-            package_id = package_info["package_id"]
-
-            # Check if it is installed
-            installed_package_info = self.getInstalledPackageInfo(package_info["package_id"])
-            to_install_package = installed_package_info is None  # Install if the package has not been installed
-            if installed_package_info is not None:
-                # Compare versions and only schedule the installation if the given package is newer
-                new_version = package_info["package_version"]
-                installed_version = installed_package_info["package_version"]
-                if Version(new_version) > Version(installed_version):
-                    Logger.log("i", "Package [%s] version [%s] is newer than the installed version [%s], update it.",
-                               package_id, new_version, installed_version)
-                    to_install_package = True
-
-            if to_install_package:
-                # Need to use the lock file to prevent concurrent I/O issues.
-                with self._container_registry.lockFile():
-                    Logger.log("i", "Package [%s] version [%s] is scheduled to be installed.",
-                               package_id, package_info["package_version"])
-                    # Copy the file to cache dir so we don't need to rely on the original file to be present
-                    package_cache_dir = os.path.join(os.path.abspath(Resources.getCacheStoragePath()), "cura_packages")
-                    if not os.path.exists(package_cache_dir):
-                        os.makedirs(package_cache_dir, exist_ok=True)
-
-                    target_file_path = os.path.join(package_cache_dir, package_id + ".curapackage")
-                    shutil.copy2(filename, target_file_path)
-
-                    self._to_install_package_dict[package_id] = {"package_info": package_info,
-                                                                 "filename": target_file_path}
-                    has_changes = True
-        except:
-            Logger.logException("c", "Failed to install package file '%s'", filename)
-        finally:
-            self._saveManagementData()
-            if has_changes:
-                self.installedPackagesChanged.emit()
-
-    # Schedules the given package to be removed upon the next start.
-    # \param package_id id of the package
-    # \param force_add is used when updating. In that case you actually want to uninstall & install
-    @pyqtSlot(str)
-    def removePackage(self, package_id: str, force_add: bool = False) -> None:
-        # Check the delayed installation and removal lists first
-        if not self.isPackageInstalled(package_id):
-            Logger.log("i", "Attempt to remove package [%s] that is not installed, do nothing.", package_id)
-            return
-
-        # Extra safety check
-        if package_id not in self._installed_package_dict and package_id in self._bundled_package_dict:
-            Logger.log("i", "Not uninstalling [%s] because it is a bundled package.")
-            return
-
-        if package_id not in self._to_install_package_dict or force_add:
-            # Schedule for a delayed removal:
-            self._to_remove_package_set.add(package_id)
-        else:
-            if package_id in self._to_install_package_dict:
-                # Remove from the delayed installation list if present
-                del self._to_install_package_dict[package_id]
-
-        self._saveManagementData()
-        self.installedPackagesChanged.emit()
-
-    ##  Is the package an user installed package?
-    def isUserInstalledPackage(self, package_id: str):
-        return package_id in self._installed_package_dict
-
-    # Removes everything associated with the given package ID.
-    def _purgePackage(self, package_id: str) -> None:
-        # Iterate through all directories in the data storage directory and look for sub-directories that belong to
-        # the package we need to remove, that is the sub-dirs with the package_id as names, and remove all those dirs.
-        data_storage_dir = os.path.abspath(Resources.getDataStoragePath())
-
-        for root, dir_names, _ in os.walk(data_storage_dir):
-            for dir_name in dir_names:
-                package_dir = os.path.join(root, dir_name, package_id)
-                if os.path.exists(package_dir):
-                    Logger.log("i", "Removing '%s' for package [%s]", package_dir, package_id)
-                    shutil.rmtree(package_dir)
-            break
-
-    # Installs all files associated with the given package.
-    def _installPackage(self, installation_package_data: dict):
-        package_info = installation_package_data["package_info"]
-        filename = installation_package_data["filename"]
-
-        package_id = package_info["package_id"]
-
-        if not os.path.exists(filename):
-            Logger.log("w", "Package [%s] file '%s' is missing, cannot install this package", package_id, filename)
-            return
-
-        Logger.log("i", "Installing package [%s] from file [%s]", package_id, filename)
-
-        # If it's installed, remove it first and then install
-        if package_id in self._installed_package_dict:
-            self._purgePackage(package_id)
-
-        # Install the package
-        with zipfile.ZipFile(filename, "r") as archive:
-
-            temp_dir = tempfile.TemporaryDirectory()
-            archive.extractall(temp_dir.name)
-
-            from cura.CuraApplication import CuraApplication
-            installation_dirs_dict = {
-                "materials": Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer),
-                "qualities": Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer),
-                "plugins": os.path.abspath(Resources.getStoragePath(Resources.Plugins)),
-            }
-
-            for sub_dir_name, installation_root_dir in installation_dirs_dict.items():
-                src_dir_path = os.path.join(temp_dir.name, "files", sub_dir_name)
-                dst_dir_path = os.path.join(installation_root_dir, package_id)
-
-                if not os.path.exists(src_dir_path):
-                    continue
-                self.__installPackageFiles(package_id, src_dir_path, dst_dir_path)
-
-        # Remove the file
-        os.remove(filename)
-
-    def __installPackageFiles(self, package_id: str, src_dir: str, dst_dir: str) -> None:
-        Logger.log("i", "Moving package {package_id} from {src_dir} to {dst_dir}".format(package_id=package_id, src_dir=src_dir, dst_dir=dst_dir))
-        shutil.move(src_dir, dst_dir)
-
-    # Gets package information from the given file.
-    def getPackageInfo(self, filename: str) -> Dict[str, Any]:
-        with zipfile.ZipFile(filename) as archive:
-            try:
-                # All information is in package.json
-                with archive.open("package.json") as f:
-                    package_info_dict = json.loads(f.read().decode("utf-8"))
-                    return package_info_dict
-            except Exception as e:
-                Logger.logException("w", "Could not get package information from file '%s': %s" % (filename, e))
-                return {}
-
-    # Gets the license file content if present in the given package file.
-    # Returns None if there is no license file found.
-    def getPackageLicense(self, filename: str) -> Optional[str]:
-        license_string = None
-        with zipfile.ZipFile(filename) as archive:
-            # Go through all the files and use the first successful read as the result
-            for file_info in archive.infolist():
-                if file_info.filename.endswith("LICENSE"):
-                    Logger.log("d", "Found potential license file '%s'", file_info.filename)
-                    try:
-                        with archive.open(file_info.filename, "r") as f:
-                            data = f.read()
-                        license_string = data.decode("utf-8")
-                        break
-                    except:
-                        Logger.logException("e", "Failed to load potential license file '%s' as text file.",
-                                            file_info.filename)
-                        license_string = None
-        return license_string
+        self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
+        self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)

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

@@ -47,22 +47,38 @@ class BrandMaterialsModel(ListModel):
         self.addRoleName(self.MaterialsRole, "materials")
         self.addRoleName(self.MaterialsRole, "materials")
 
 
         self._extruder_position = 0
         self._extruder_position = 0
+        self._extruder_stack = None
 
 
         from cura.CuraApplication import CuraApplication
         from cura.CuraApplication import CuraApplication
         self._machine_manager = CuraApplication.getInstance().getMachineManager()
         self._machine_manager = CuraApplication.getInstance().getMachineManager()
         self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
         self._extruder_manager = CuraApplication.getInstance().getExtruderManager()
         self._material_manager = CuraApplication.getInstance().getMaterialManager()
         self._material_manager = CuraApplication.getInstance().getMaterialManager()
 
 
+        self._machine_manager.globalContainerChanged.connect(self._updateExtruderStack)
         self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
         self._machine_manager.activeStackChanged.connect(self._update) #Update when switching machines.
         self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
         self._material_manager.materialsUpdated.connect(self._update) #Update when the list of materials changes.
         self._update()
         self._update()
 
 
+    def _updateExtruderStack(self):
+        global_stack = self._machine_manager.activeMachine
+        if global_stack is None:
+            return
+
+        if self._extruder_stack is not None:
+            self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
+        self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
+        if self._extruder_stack is not None:
+            self._extruder_stack.pyqtContainersChanged.connect(self._update)
+        # Force update the model when the extruder stack changes
+        self._update()
+
     def setExtruderPosition(self, position: int):
     def setExtruderPosition(self, position: int):
-        if self._extruder_position != position:
+        if self._extruder_stack is None or self._extruder_position != position:
             self._extruder_position = position
             self._extruder_position = position
+            self._updateExtruderStack()
             self.extruderPositionChanged.emit()
             self.extruderPositionChanged.emit()
 
 
-    @pyqtProperty(int, fset = setExtruderPosition, notify = extruderPositionChanged)
+    @pyqtProperty(int, fset=setExtruderPosition, notify=extruderPositionChanged)
     def extruderPosition(self) -> int:
     def extruderPosition(self) -> int:
         return self._extruder_position
         return self._extruder_position
 
 

+ 1 - 1
cura/MultiplyObjectsJob.py

@@ -38,7 +38,7 @@ class MultiplyObjectsJob(Job):
 
 
         root = scene.getRoot()
         root = scene.getRoot()
         scale = 0.5
         scale = 0.5
-        arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale)
+        arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
         processed_nodes = []
         processed_nodes = []
         nodes = []
         nodes = []
 
 

+ 4 - 4
cura/PrintInformation.py

@@ -299,7 +299,7 @@ class PrintInformation(QObject):
 
 
     def _updateJobName(self):
     def _updateJobName(self):
         if self._base_name == "":
         if self._base_name == "":
-            self._job_name = ""
+            self._job_name = "unnamed"
             self._is_user_specified_job_name = False
             self._is_user_specified_job_name = False
             self.jobNameChanged.emit()
             self.jobNameChanged.emit()
             return
             return
@@ -351,17 +351,17 @@ class PrintInformation(QObject):
         if is_gcode or is_project_file or (is_empty or (self._base_name == "" and self._base_name != 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
 
 
-            data = ''
+            data = ""
             try:
             try:
                 mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
                 mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
                 data = mime_type.stripExtension(name)
                 data = mime_type.stripExtension(name)
             except:
             except:
-                Logger.log("w", "Unsupported Mime Type Database file extension")
+                Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
 
 
             if data is not None and check_name is not None:
             if data is not None and check_name is not None:
                 self._base_name = data
                 self._base_name = data
             else:
             else:
-                self._base_name = ''
+                self._base_name = ""
 
 
             self._updateJobName()
             self._updateJobName()
 
 

+ 2 - 2
cura/PrinterOutput/NetworkedPrinterOutputDevice.py

@@ -254,8 +254,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
         self._last_manager_create_time = time()
         self._last_manager_create_time = time()
         self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
         self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
 
 
-        machine_manager = CuraApplication.getInstance().getMachineManager()
-        machine_manager.checkCorrectGroupName(self.getId(), self.name)
+        if self._properties.get(b"temporary", b"false") != b"true":
+            Application.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name)
 
 
     def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
     def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
         if on_finished is not None:
         if on_finished is not None:

+ 1 - 1
cura/Scene/CuraSceneNode.py

@@ -20,7 +20,7 @@ class CuraSceneNode(SceneNode):
     def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
     def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
         super().__init__(parent = parent, visible = visible, name = name)
         super().__init__(parent = parent, visible = visible, name = name)
         if not no_setting_override:
         if not no_setting_override:
-            self.addDecorator(SettingOverrideDecorator())
+            self.addDecorator(SettingOverrideDecorator())  # now we always have a getActiveExtruderPosition, unless explicitly disabled
         self._outside_buildarea = False
         self._outside_buildarea = False
 
 
     def setOutsideBuildArea(self, new_value: bool) -> None:
     def setOutsideBuildArea(self, new_value: bool) -> None:

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