diff --git a/cura/CuraApplication2.py b/cura/CuraApplication2.py new file mode 100644 index 0000000000..85c269fc8b --- /dev/null +++ b/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 diff --git a/cura_app.py b/cura_app.py index 3bcce18fb5..4f61ef415d 100755 --- a/cura_app.py +++ b/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")