Просмотр исходного кода

Using platform.system() for Windows detection

This is just cosmetics. It works the same as system.platform, but looks
better. Additionally "win32" might be misleading, as it sounds like we
are checking for Windows 32bit here.
Thomas Karl Pietrowski 8 лет назад
Родитель
Сommit
0d8292df49
2 измененных файлов с 786 добавлено и 1 удалено
  1. 784 0
      cura/CuraApplication2.py
  2. 2 1
      cura_app.py

+ 784 - 0
cura/CuraApplication2.py

@@ -0,0 +1,784 @@
+# Copyright (c) 2015 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
+from memory_profiler import profile
+
+from UM.Qt.QtApplication import QtApplication
+from UM.Scene.SceneNode import SceneNode
+from UM.Scene.Camera import Camera
+from UM.Scene.Platform import Platform
+from UM.Math.Vector import Vector
+from UM.Math.Quaternion import Quaternion
+from UM.Math.AxisAlignedBox import AxisAlignedBox
+from UM.Resources import Resources
+from UM.Scene.ToolHandle import ToolHandle
+from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
+from UM.Mesh.ReadMeshJob import ReadMeshJob
+from UM.Logger import Logger
+from UM.Preferences import Preferences
+from UM.JobQueue import JobQueue
+from UM.SaveFile import SaveFile
+from UM.Scene.Selection import Selection
+from UM.Scene.GroupDecorator import GroupDecorator
+import UM.Settings.Validator
+
+from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
+from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
+from UM.Operations.GroupedOperation import GroupedOperation
+from UM.Operations.SetTransformOperation import SetTransformOperation
+from cura.SetParentOperation import SetParentOperation
+
+from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
+from UM.Settings.ContainerRegistry import ContainerRegistry
+
+from UM.i18n import i18nCatalog
+
+from . import ExtruderManager
+from . import ExtrudersModel
+from . import PlatformPhysics
+from . import BuildVolume
+from . import CameraAnimation
+from . import PrintInformation
+from . import CuraActions
+from . import MultiMaterialDecorator
+from . import ZOffsetDecorator
+from . import CuraSplashScreen
+from . import MachineManagerModel
+
+from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
+from PyQt5.QtGui import QColor, QIcon
+from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
+
+import platform
+import sys
+import os.path
+import numpy
+import copy
+import urllib
+numpy.seterr(all="ignore")
+
+#WORKAROUND: GITHUB-88 GITHUB-385 GITHUB-612
+if platform.system() == "Linux": # Needed for platform.linux_distribution, which is not available on Windows and OSX
+    # For Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
+    if platform.linux_distribution()[0] in ("Ubuntu", ): # TODO: Needs a "if X11_GFX == 'nvidia'" here. The workaround is only needed on Ubuntu+NVidia drivers. Other drivers are not affected, but fine with this fix.
+        import ctypes
+        from ctypes.util import find_library
+        ctypes.CDLL(find_library('GL'), ctypes.RTLD_GLOBAL)
+
+try:
+    from cura.CuraVersion import CuraVersion, CuraBuildType
+except ImportError:
+    CuraVersion = "master"  # [CodeStyle: Reflecting imported value]
+    CuraBuildType = ""
+
+@profile
+class CuraApplication(QtApplication):
+    class ResourceTypes:
+        QmlFiles = Resources.UserType + 1
+        Firmware = Resources.UserType + 2
+        QualityInstanceContainer = Resources.UserType + 3
+        MaterialInstanceContainer = Resources.UserType + 4
+        VariantInstanceContainer = Resources.UserType + 5
+        UserInstanceContainer = Resources.UserType + 6
+        MachineStack = Resources.UserType + 7
+        ExtruderStack = Resources.UserType + 8
+
+    Q_ENUMS(ResourceTypes)
+
+    def __init__(self):
+        Resources.addSearchPath(os.path.join(QtApplication.getInstallPrefix(), "share", "cura", "resources"))
+        if not hasattr(sys, "frozen"):
+            Resources.addSearchPath(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources"))
+
+        self._open_file_queue = []  # Files to open when plug-ins are loaded.
+
+        # Need to do this before ContainerRegistry tries to load the machines
+        SettingDefinition.addSupportedProperty("global_only", DefinitionPropertyType.Function, default = False)
+        SettingDefinition.addSettingType("extruder", int, str, UM.Settings.Validator)
+
+        super().__init__(name = "cura", version = CuraVersion, buildtype = CuraBuildType)
+
+        self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
+
+        self.setRequiredPlugins([
+            "CuraEngineBackend",
+            "MeshView",
+            "LayerView",
+            "STLReader",
+            "SelectionTool",
+            "CameraTool",
+            "GCodeWriter",
+            "LocalFileOutputDevice"
+        ])
+        self._physics = None
+        self._volume = None
+        self._platform = None
+        self._output_devices = {}
+        self._print_information = None
+        self._i18n_catalog = None
+        self._previous_active_tool = None
+        self._platform_activity = False
+        self._scene_bounding_box = AxisAlignedBox()
+        self._job_name = None
+        self._center_after_select = False
+        self._camera_animation = None
+        self._cura_actions = None
+        self._started = False
+
+        self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
+        self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
+
+        Resources.addType(self.ResourceTypes.QmlFiles, "qml")
+        Resources.addType(self.ResourceTypes.Firmware, "firmware")
+
+        ## Add the 4 types of profiles to storage.
+        Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
+        Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
+        Resources.addStorageType(self.ResourceTypes.MaterialInstanceContainer, "materials")
+        Resources.addStorageType(self.ResourceTypes.UserInstanceContainer, "user")
+        Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
+        Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
+
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
+        ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
+
+        # Add empty variant, material and quality containers.
+        # 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()
+        empty_variant_container = copy.deepcopy(empty_container)
+        empty_variant_container._id = "empty_variant"
+        empty_variant_container.addMetaDataEntry("type", "variant")
+        ContainerRegistry.getInstance().addContainer(empty_variant_container)
+        empty_material_container = copy.deepcopy(empty_container)
+        empty_material_container._id = "empty_material"
+        empty_material_container.addMetaDataEntry("type", "material")
+        ContainerRegistry.getInstance().addContainer(empty_material_container)
+        empty_quality_container = copy.deepcopy(empty_container)
+        empty_quality_container._id = "empty_quality"
+        empty_quality_container.addMetaDataEntry("type", "quality")
+        ContainerRegistry.getInstance().addContainer(empty_quality_container)
+
+        ContainerRegistry.getInstance().load()
+
+        Preferences.getInstance().addPreference("cura/active_mode", "simple")
+        Preferences.getInstance().addPreference("cura/recent_files", "")
+        Preferences.getInstance().addPreference("cura/categories_expanded", "")
+        Preferences.getInstance().addPreference("cura/jobname_prefix", True)
+        Preferences.getInstance().addPreference("view/center_on_select", True)
+        Preferences.getInstance().addPreference("mesh/scale_to_fit", True)
+        Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True)
+        Preferences.getInstance().setDefault("local_file/last_used_type", "text/x-gcode")
+
+        Preferences.getInstance().setDefault("general/visible_settings", """
+            machine_settings
+                resolution
+                layer_height
+            shell
+                wall_thickness
+                top_bottom_thickness
+            infill
+                infill_sparse_density
+            material
+                material_print_temperature
+                material_bed_temperature
+                material_diameter
+                material_flow
+                retraction_enable
+            speed
+                speed_print
+                speed_travel
+            travel
+            cooling
+                cool_fan_enabled
+            support
+                support_enable
+                support_type
+                support_roof_density
+            platform_adhesion
+                adhesion_type
+                brim_width
+                raft_airgap
+                layer_0_z_overlap
+                raft_surface_layers
+            meshfix
+            blackmagic
+                print_sequence
+                dual
+            experimental
+        """.replace("\n", ";").replace(" ", ""))
+
+        JobQueue.getInstance().jobFinished.connect(self._onJobFinished)
+
+        self.applicationShuttingDown.connect(self.saveSettings)
+
+        self._recent_files = []
+        files = Preferences.getInstance().getValue("cura/recent_files").split(";")
+        for f in files:
+            if not os.path.isfile(f):
+                continue
+
+            self._recent_files.append(QUrl.fromLocalFile(f))
+
+    ##  Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
+    #
+    #   Note that the AutoSave plugin also calls this method.
+    def saveSettings(self):
+        if not self._started: # Do not do saving during application start
+            return
+
+        for instance in ContainerRegistry.getInstance().findInstanceContainers():
+            if not instance.isDirty():
+                continue
+
+            try:
+                data = instance.serialize()
+            except NotImplementedError:
+                continue
+            except Exception:
+                Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
+                continue
+
+            file_name = urllib.parse.quote_plus(instance.getId()) + ".inst.cfg"
+            instance_type = instance.getMetaDataEntry("type")
+            path = None
+            if instance_type == "material":
+                path = Resources.getStoragePath(self.ResourceTypes.MaterialInstanceContainer, file_name)
+            elif instance_type == "quality":
+                path = Resources.getStoragePath(self.ResourceTypes.QualityInstanceContainer, file_name)
+            elif instance_type == "user":
+                path = Resources.getStoragePath(self.ResourceTypes.UserInstanceContainer, file_name)
+            elif instance_type == "variant":
+                path = Resources.getStoragePath(self.ResourceTypes.VariantInstanceContainer, file_name)
+
+            if path:
+                with SaveFile(path, "wt", -1, "utf-8") as f:
+                    f.write(data)
+
+        for stack in ContainerRegistry.getInstance().findContainerStacks():
+            if not stack.isDirty():
+                continue
+
+            try:
+                data = stack.serialize()
+            except NotImplementedError:
+                continue
+            except Exception:
+                Logger.logException("e", "An exception occurred when serializing container %s", instance.getId())
+                continue
+
+            file_name = urllib.parse.quote_plus(stack.getId()) + ".stack.cfg"
+            stack_type = stack.getMetaDataEntry("type", None)
+            path = None
+            if not stack_type or stack_type == "machine":
+                path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
+            elif stack_type == "extruder":
+                path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name)
+            if path:
+                with SaveFile(path, "wt", -1, "utf-8") as f:
+                    f.write(data)
+
+
+    @pyqtSlot(result = QUrl)
+    def getDefaultPath(self):
+        return QUrl.fromLocalFile(os.path.expanduser("~/"))
+    
+    ##  Handle loading of all plugin types (and the backend explicitly)
+    #   \sa PluginRegistery
+    def _loadPlugins(self):
+        self._plugin_registry.addType("profile_reader", self._addProfileReader)
+        self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib", "cura"))
+        if not hasattr(sys, "frozen"):
+            self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
+            self._plugin_registry.loadPlugin("ConsoleLogger")
+            self._plugin_registry.loadPlugin("CuraEngineBackend")
+
+        self._plugin_registry.loadPlugins()
+
+        if self.getBackend() == None:
+            raise RuntimeError("Could not load the backend plugin!")
+
+        self._plugins_loaded = True
+
+    def addCommandLineOptions(self, parser):
+        super().addCommandLineOptions(parser)
+        parser.add_argument("file", nargs="*", help="Files to load after starting the application.")
+        parser.add_argument("--debug", dest="debug-mode", action="store_true", default=False, help="Enable detailed crash reports.")
+
+    def run(self):
+        self._i18n_catalog = i18nCatalog("cura");
+
+        i18nCatalog.setTagReplacements({
+            "filename": "font color=\"black\"",
+            "message": "font color=UM.Theme.colors.message_text;",
+        })
+
+        self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
+
+        controller = self.getController()
+
+        controller.setActiveView("SolidView")
+        controller.setCameraTool("CameraTool")
+        controller.setSelectionTool("SelectionTool")
+
+        t = controller.getTool("TranslateTool")
+        if t:
+            t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis,ToolHandle.ZAxis])
+
+        Selection.selectionChanged.connect(self.onSelectionChanged)
+
+        root = controller.getScene().getRoot()
+        self._platform = Platform(root)
+
+        self._volume = BuildVolume.BuildVolume(root)
+
+        self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
+
+        self._physics = PlatformPhysics.PlatformPhysics(controller, self._volume)
+
+        camera = Camera("3d", root)
+        camera.setPosition(Vector(-80, 250, 700))
+        camera.setPerspective(True)
+        camera.lookAt(Vector(0, 0, 0))
+        controller.getScene().setActiveCamera("3d")
+
+        self.getController().getTool("CameraTool").setOrigin(Vector(0, 100, 0))
+
+        self._camera_animation = CameraAnimation.CameraAnimation()
+        self._camera_animation.setCameraTool(self.getController().getTool("CameraTool"))
+
+        self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
+
+        qmlRegisterSingletonType(MachineManagerModel.MachineManagerModel, "Cura", 1, 0, "MachineManager",
+                                 MachineManagerModel.createMachineManagerModel)
+
+        self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml"))
+        self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles))
+        self.initializeEngine()
+
+        if self._engine.rootObjects:
+            self.closeSplash()
+
+            for file in self.getCommandLineOption("file", []):
+                self._openFile(file)
+            for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading.
+                self._openFile(file_name)
+
+            self._started = True
+
+            self.exec_()
+
+    ##   Handle Qt events
+    def event(self, event):
+        if event.type() == QEvent.FileOpen:
+            if self._plugins_loaded:
+                self._openFile(event.file())
+            else:
+                self._open_file_queue.append(event.file())
+
+        return super().event(event)
+
+    ##  Get print information (duration / material used)
+    def getPrintInformation(self):
+        return self._print_information
+
+    ##  Registers objects for the QML engine to use.
+    #
+    #   \param engine The QML engine.
+    def registerObjects(self, engine):
+        engine.rootContext().setContextProperty("Printer", self)
+        self._print_information = PrintInformation.PrintInformation()
+        engine.rootContext().setContextProperty("PrintInformation", self._print_information)
+        self._cura_actions = CuraActions.CuraActions(self)
+        engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
+
+        qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
+
+        qmlRegisterType(ExtrudersModel.ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
+
+        qmlRegisterSingletonType(QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")), "Cura", 1, 0, "Actions")
+
+        engine.rootContext().setContextProperty("ExtruderManager", ExtruderManager.ExtruderManager.getInstance())
+
+        for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
+            type_name = os.path.splitext(os.path.basename(path))[0]
+            if type_name in ("Cura", "Actions"):
+                continue
+
+            qmlRegisterType(QUrl.fromLocalFile(path), "Cura", 1, 0, type_name)
+
+    def onSelectionChanged(self):
+        if Selection.hasSelection():
+            if not self.getController().getActiveTool():
+                if self._previous_active_tool:
+                    self.getController().setActiveTool(self._previous_active_tool)
+                    self._previous_active_tool = None
+                else:
+                    self.getController().setActiveTool("TranslateTool")
+            if Preferences.getInstance().getValue("view/center_on_select"):
+                self._center_after_select = True
+        else:
+            if self.getController().getActiveTool():
+                self._previous_active_tool = self.getController().getActiveTool().getPluginId()
+                self.getController().setActiveTool(None)
+            else:
+                self._previous_active_tool = None
+
+    def _onToolOperationStopped(self, event):
+        if self._center_after_select:
+            self._center_after_select = False
+            self._camera_animation.setStart(self.getController().getTool("CameraTool").getOrigin())
+            self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
+            self._camera_animation.start()
+
+    requestAddPrinter = pyqtSignal()
+    activityChanged = pyqtSignal()
+    sceneBoundingBoxChanged = pyqtSignal()
+
+    @pyqtProperty(bool, notify = activityChanged)
+    def getPlatformActivity(self):
+        return self._platform_activity
+
+    @pyqtProperty(str, notify = sceneBoundingBoxChanged)
+    def getSceneBoundingBoxString(self):
+        return self._i18n_catalog.i18nc("@info", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()}
+
+    def updatePlatformActivity(self, node = None):
+        count = 0
+        scene_bounding_box = None
+        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
+            if type(node) is not SceneNode or not node.getMeshData():
+                continue
+
+            count += 1
+            if not scene_bounding_box:
+                scene_bounding_box = copy.deepcopy(node.getBoundingBox())
+            else:
+                scene_bounding_box += node.getBoundingBox()
+
+        if not scene_bounding_box:
+            scene_bounding_box = AxisAlignedBox()
+
+        if repr(self._scene_bounding_box) != repr(scene_bounding_box):
+            self._scene_bounding_box = scene_bounding_box
+            self.sceneBoundingBoxChanged.emit()
+
+        self._platform_activity = True if count > 0 else False
+        self.activityChanged.emit()
+
+    # Remove all selected objects from the scene.
+    @pyqtSlot()
+    def deleteSelection(self):
+        if not self.getController().getToolsEnabled():
+            return
+
+        op = GroupedOperation()
+        nodes = Selection.getAllSelectedObjects()
+        for node in nodes:
+            op.addOperation(RemoveSceneNodeOperation(node))
+
+        op.push()
+
+        pass
+
+    ##  Remove an object from the scene.
+    #   Note that this only removes an object if it is selected.
+    @pyqtSlot("quint64")
+    def deleteObject(self, object_id):
+        if not self.getController().getToolsEnabled():
+            return
+
+        node = self.getController().getScene().findObject(object_id)
+
+        if not node and object_id != 0:  # Workaround for tool handles overlapping the selected object
+            node = Selection.getSelectedObject(0)
+
+        if node:
+            if node.getParent():
+                group_node = node.getParent()
+                if not group_node.callDecoration("isGroup"):
+                    op = RemoveSceneNodeOperation(node)
+                else:
+                    while group_node.getParent().callDecoration("isGroup"):
+                        group_node = group_node.getParent()
+                    op = RemoveSceneNodeOperation(group_node)
+            op.push()
+
+    ##  Create a number of copies of existing object.
+    @pyqtSlot("quint64", int)
+    def multiplyObject(self, object_id, count):
+        node = self.getController().getScene().findObject(object_id)
+
+        if not node and object_id != 0:  # Workaround for tool handles overlapping the selected object
+            node = Selection.getSelectedObject(0)
+
+        if node:
+            op = GroupedOperation()
+            for _ in range(count):
+                if node.getParent() and node.getParent().callDecoration("isGroup"):
+                    new_node = copy.deepcopy(node.getParent()) #Copy the group node.
+                    new_node.callDecoration("setConvexHull",None)
+
+                    op.addOperation(AddSceneNodeOperation(new_node,node.getParent().getParent()))
+                else:
+                    new_node = copy.deepcopy(node)
+                    new_node.callDecoration("setConvexHull", None)
+                    op.addOperation(AddSceneNodeOperation(new_node, node.getParent()))
+
+            op.push()
+
+    ##  Center object on platform.
+    @pyqtSlot("quint64")
+    def centerObject(self, object_id):
+        node = self.getController().getScene().findObject(object_id)
+        if not node and object_id != 0:  # Workaround for tool handles overlapping the selected object
+            node = Selection.getSelectedObject(0)
+
+        if not node:
+            return
+
+        if node.getParent() and node.getParent().callDecoration("isGroup"):
+            node = node.getParent()
+
+        if node:
+            op = SetTransformOperation(node, Vector())
+            op.push()
+    
+    ##  Delete all nodes containing mesh data in the scene.
+    @pyqtSlot()
+    def deleteAll(self):
+        if not self.getController().getToolsEnabled():
+            return
+
+        nodes = []
+        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
+            if type(node) is not SceneNode:
+                continue
+            if not node.getMeshData() and not node.callDecoration("isGroup"):
+                continue  # Node that doesnt have a mesh and is not a group.
+            if node.getParent() and node.getParent().callDecoration("isGroup"):
+                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))
+
+            op.push()
+
+    ## Reset all translation on nodes with mesh data. 
+    @pyqtSlot()
+    def resetAllTranslation(self):
+        nodes = []
+        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
+            if type(node) is not SceneNode:
+                continue
+            if not node.getMeshData() and not node.callDecoration("isGroup"):
+                continue  # Node that doesnt have a mesh and is not a group.
+            if node.getParent() and node.getParent().callDecoration("isGroup"):
+                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:
+                node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
+                op.addOperation(SetTransformOperation(node, Vector(0,0,0)))
+
+            op.push()
+    
+    ## Reset all transformations on nodes with mesh data. 
+    @pyqtSlot()
+    def resetAll(self):
+        nodes = []
+        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
+            if type(node) is not SceneNode:
+                continue
+            if not node.getMeshData() and not node.callDecoration("isGroup"):
+                continue  # Node that doesnt have a mesh and is not a group.
+            if node.getParent() and node.getParent().callDecoration("isGroup"):
+                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:
+                # Ensure that the object is above the build platform
+                node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
+                op.addOperation(SetTransformOperation(node, Vector(0,0,0), Quaternion(), Vector(1, 1, 1)))
+
+            op.push()
+            
+    ##  Reload all mesh data on the screen from file.
+    @pyqtSlot()
+    def reloadAll(self):
+        nodes = []
+        for node in DepthFirstIterator(self.getController().getScene().getRoot()):
+            if type(node) is not SceneNode or not node.getMeshData():
+                continue
+
+            nodes.append(node)
+
+        if not nodes:
+            return
+
+        for node in nodes:
+            if not node.getMeshData():
+                continue
+
+            file_name = node.getMeshData().getFileName()
+            if file_name:
+                job = ReadMeshJob(file_name)
+                job._node = node
+                job.finished.connect(self._reloadMeshFinished)
+                job.start()
+    
+    ##  Get logging data of the backend engine
+    #   \returns \type{string} Logging data
+    @pyqtSlot(result = str)
+    def getEngineLog(self):
+        log = ""
+
+        for entry in self.getBackend().getLog():
+            log += entry.decode()
+
+        return log
+
+    recentFilesChanged = pyqtSignal()
+
+    @pyqtProperty("QVariantList", notify = recentFilesChanged)
+    def recentFiles(self):
+        return self._recent_files
+
+    @pyqtSlot("QStringList")
+    def setExpandedCategories(self, categories):
+        categories = list(set(categories))
+        categories.sort()
+        joined = ";".join(categories)
+        if joined != Preferences.getInstance().getValue("cura/categories_expanded"):
+            Preferences.getInstance().setValue("cura/categories_expanded", joined)
+            self.expandedCategoriesChanged.emit()
+
+    expandedCategoriesChanged = pyqtSignal()
+
+    @pyqtProperty("QStringList", notify = expandedCategoriesChanged)
+    def expandedCategories(self):
+        return Preferences.getInstance().getValue("cura/categories_expanded").split(";")
+
+    @pyqtSlot()
+    def mergeSelected(self):
+        self.groupSelected()
+        try:
+            group_node = Selection.getAllSelectedObjects()[0]
+        except Exception as e:
+            Logger.log("d", "mergeSelected: Exception:", e)
+            return
+        multi_material_decorator = MultiMaterialDecorator.MultiMaterialDecorator()
+        group_node.addDecorator(multi_material_decorator)
+        # Reset the position of each node
+        for node in group_node.getChildren():
+            new_position = node.getMeshData().getCenterPosition()
+            new_position = new_position.scale(node.getScale())
+            node.setPosition(new_position)
+        
+        # Use the previously found center of the group bounding box as the new location of the group
+        group_node.setPosition(group_node.getBoundingBox().center)
+
+    @pyqtSlot()
+    def groupSelected(self):
+        # Create a group-node
+        group_node = SceneNode()
+        group_decorator = GroupDecorator()
+        group_node.addDecorator(group_decorator)
+        group_node.setParent(self.getController().getScene().getRoot())
+        group_node.setSelectable(True)
+        center = Selection.getSelectionCenter()
+        group_node.setPosition(center)
+        group_node.setCenterPosition(center)
+
+        # Move selected nodes into the group-node
+        Selection.applyOperation(SetParentOperation, group_node)
+
+        # Deselect individual nodes and select the group-node instead
+        for node in group_node.getChildren():
+            Selection.remove(node)
+        Selection.add(group_node)
+
+    @pyqtSlot()
+    def ungroupSelected(self):
+        selected_objects = Selection.getAllSelectedObjects().copy()
+        for node in selected_objects:
+            if node.callDecoration("isGroup"):
+                op = GroupedOperation()
+
+                group_parent = node.getParent()
+                children = node.getChildren().copy()
+                for child in children:
+                    # Set the parent of the children to the parent of the group-node
+                    op.addOperation(SetParentOperation(child, group_parent))
+
+                    # Add all individual nodes to the selection
+                    Selection.add(child)
+                    child.callDecoration("setConvexHull", None)
+
+                op.push()
+                # Note: The group removes itself from the scene once all its children have left it,
+                # see GroupDecorator._onChildrenChanged
+
+    def _createSplashScreen(self):
+        return CuraSplashScreen.CuraSplashScreen()
+
+    def _onActiveMachineChanged(self):
+        pass
+
+    fileLoaded = pyqtSignal(str)
+
+    def _onFileLoaded(self, job):
+        node = job.getResult()
+        if node != None:
+            self.fileLoaded.emit(job.getFileName())
+            node.setSelectable(True)
+            node.setName(os.path.basename(job.getFileName()))
+            op = AddSceneNodeOperation(node, self.getController().getScene().getRoot())
+            op.push()
+
+            self.getController().getScene().sceneChanged.emit(node) #Force scene change.
+
+    def _onJobFinished(self, job):
+        if type(job) is not ReadMeshJob or not job.getResult():
+            return
+
+        f = QUrl.fromLocalFile(job.getFileName())
+        if f in self._recent_files:
+            self._recent_files.remove(f)
+
+        self._recent_files.insert(0, f)
+        if len(self._recent_files) > 10:
+            del self._recent_files[10]
+
+        pref = ""
+        for path in self._recent_files:
+            pref += path.toLocalFile() + ";"
+
+        Preferences.getInstance().setValue("cura/recent_files", pref)
+        self.recentFilesChanged.emit()
+
+    def _reloadMeshFinished(self, job):
+        # TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
+        job._node.setMeshData(job.getResult().getMeshData())
+
+    def _openFile(self, file):
+        job = ReadMeshJob(os.path.abspath(file))
+        job.finished.connect(self._onFileLoaded)
+        job.start()
+
+    def _addProfileReader(self, profile_reader):
+        # TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
+        pass

+ 2 - 1
cura_app.py

@@ -4,6 +4,7 @@
 # Cura is released under the terms of the AGPLv3 or higher.
 
 import os
+import platform
 import sys
 
 #WORKAROUND: GITHUB-704 GITHUB-708
@@ -35,7 +36,7 @@ sys.excepthook = exceptHook
 import Arcus #@UnusedImport
 import cura.CuraApplication
 
-if sys.platform == "win32" and hasattr(sys, "frozen"):
+if platform.system() == "Windows" and hasattr(sys, "frozen"):
     dirpath = os.path.expanduser("~/AppData/Local/cura/")
     os.makedirs(dirpath, exist_ok = True)
     sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w")