1# Copyright (c) 2018 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3
4from PyQt5.QtCore import QObject, QUrl
5from PyQt5.QtGui import QDesktopServices
6from typing import List, cast
7
8from UM.Event import CallFunctionEvent
9from UM.FlameProfiler import pyqtSlot
10from UM.Math.Vector import Vector
11from UM.Scene.Selection import Selection
12from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
13from UM.Operations.GroupedOperation import GroupedOperation
14from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
15from UM.Operations.TranslateOperation import TranslateOperation
16
17import cura.CuraApplication
18from cura.Operations.SetParentOperation import SetParentOperation
19from cura.MultiplyObjectsJob import MultiplyObjectsJob
20from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation
21from cura.Settings.ExtruderManager import ExtruderManager
22
23from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
24
25from UM.Logger import Logger
26from UM.Scene.SceneNode import SceneNode
27
28
29class CuraActions(QObject):
30    def __init__(self, parent: QObject = None) -> None:
31        super().__init__(parent)
32
33    @pyqtSlot()
34    def openDocumentation(self) -> None:
35        # Starting a web browser from a signal handler connected to a menu will crash on windows.
36        # So instead, defer the call to the next run of the event loop, since that does work.
37        # Note that weirdly enough, only signal handlers that open a web browser fail like that.
38        event = CallFunctionEvent(self._openUrl, [QUrl("https://ultimaker.com/en/resources/manuals/software")], {})
39        cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
40
41    @pyqtSlot()
42    def openBugReportPage(self) -> None:
43        event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues/new/choose")], {})
44        cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
45
46    @pyqtSlot()
47    def homeCamera(self) -> None:
48        """Reset camera position and direction to default"""
49
50        scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene()
51        camera = scene.getActiveCamera()
52        if camera:
53            diagonal_size = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getDiagonalSize()
54            camera.setPosition(Vector(-80, 250, 700) * diagonal_size / 375)
55            camera.setPerspective(True)
56            camera.lookAt(Vector(0, 0, 0))
57
58    @pyqtSlot()
59    def centerSelection(self) -> None:
60        """Center all objects in the selection"""
61
62        operation = GroupedOperation()
63        for node in Selection.getAllSelectedObjects():
64            current_node = node
65            parent_node = current_node.getParent()
66            while parent_node and parent_node.callDecoration("isGroup"):
67                current_node = parent_node
68                parent_node = current_node.getParent()
69
70            #   This was formerly done with SetTransformOperation but because of
71            #   unpredictable matrix deconstruction it was possible that mirrors
72            #   could manifest as rotations. Centering is therefore done by
73            #   moving the node to negative whatever its position is:
74            center_operation = TranslateOperation(current_node, -current_node._position)
75            operation.addOperation(center_operation)
76        operation.push()
77
78    @pyqtSlot(int)
79    def multiplySelection(self, count: int) -> None:
80        """Multiply all objects in the selection
81
82        :param count: The number of times to multiply the selection.
83        """
84
85        min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2  # Allow for some rounding errors
86        job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8))
87        job.start()
88
89    @pyqtSlot()
90    def deleteSelection(self) -> None:
91        """Delete all selected objects."""
92
93        if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled():
94            return
95
96        removed_group_nodes = [] #type: List[SceneNode]
97        op = GroupedOperation()
98        nodes = Selection.getAllSelectedObjects()
99        for node in nodes:
100            op.addOperation(RemoveSceneNodeOperation(node))
101            group_node = node.getParent()
102            if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
103                remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
104                if len(remaining_nodes_in_group) == 1:
105                    removed_group_nodes.append(group_node)
106                    op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
107                    op.addOperation(RemoveSceneNodeOperation(group_node))
108
109            # Reset the print information
110            cura.CuraApplication.CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
111
112        op.push()
113
114    @pyqtSlot(str)
115    def setExtruderForSelection(self, extruder_id: str) -> None:
116        """Set the extruder that should be used to print the selection.
117
118        :param extruder_id: The ID of the extruder stack to use for the selected objects.
119        """
120
121        operation = GroupedOperation()
122
123        nodes_to_change = []
124        for node in Selection.getAllSelectedObjects():
125            # If the node is a group, apply the active extruder to all children of the group.
126            if node.callDecoration("isGroup"):
127                for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
128                    if grouped_node.callDecoration("getActiveExtruder") == extruder_id:
129                        continue
130
131                    if grouped_node.callDecoration("isGroup"):
132                        continue
133
134                    nodes_to_change.append(grouped_node)
135                continue
136
137            # Do not change any nodes that already have the right extruder set.
138            if node.callDecoration("getActiveExtruder") == extruder_id:
139                continue
140
141            nodes_to_change.append(node)
142
143        if not nodes_to_change:
144            # If there are no changes to make, we still need to reset the selected extruders.
145            # This is a workaround for checked menu items being deselected while still being
146            # selected.
147            ExtruderManager.getInstance().resetSelectedObjectExtruders()
148            return
149
150        for node in nodes_to_change:
151            operation.addOperation(SetObjectExtruderOperation(node, extruder_id))
152        operation.push()
153
154    @pyqtSlot(int)
155    def setBuildPlateForSelection(self, build_plate_nr: int) -> None:
156        Logger.log("d", "Setting build plate number... %d" % build_plate_nr)
157        operation = GroupedOperation()
158
159        root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
160
161        nodes_to_change = []  # type: List[SceneNode]
162        for node in Selection.getAllSelectedObjects():
163            parent_node = node  # Find the parent node to change instead
164            while parent_node.getParent() != root:
165                parent_node = cast(SceneNode, parent_node.getParent())
166
167            for single_node in BreadthFirstIterator(parent_node):  # type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
168                nodes_to_change.append(single_node)
169
170        if not nodes_to_change:
171            Logger.log("d", "Nothing to change.")
172            return
173
174        for node in nodes_to_change:
175            operation.addOperation(SetBuildPlateNumberOperation(node, build_plate_nr))
176        operation.push()
177
178        Selection.clear()
179
180    def _openUrl(self, url: QUrl) -> None:
181        QDesktopServices.openUrl(url)
182