1# Copyright (c) 2018 Ultimaker B.V.
2# Cura is released under the terms of the LGPLv3 or higher.
3
4from typing import TYPE_CHECKING, Optional, List, Set, Dict
5
6from PyQt5.QtCore import QObject
7
8from UM.FlameProfiler import pyqtSlot
9from UM.Logger import Logger
10from UM.PluginRegistry import PluginRegistry  # So MachineAction can be added as plugin type
11
12if TYPE_CHECKING:
13    from cura.CuraApplication import CuraApplication
14    from cura.Settings.GlobalStack import GlobalStack
15    from cura.MachineAction import MachineAction
16
17
18class UnknownMachineActionError(Exception):
19    """Raised when trying to add an unknown machine action as a required action"""
20
21    pass
22
23
24class NotUniqueMachineActionError(Exception):
25    """Raised when trying to add a machine action that does not have an unique key."""
26
27    pass
28
29
30class MachineActionManager(QObject):
31    def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
32        super().__init__(parent = parent)
33        self._application = application
34        self._container_registry = self._application.getContainerRegistry()
35
36        # Keeps track of which machines have already been processed so we don't do that again.
37        self._definition_ids_with_default_actions_added = set()  # type: Set[str]
38
39        # Dict of all known machine actions
40        self._machine_actions = {}  # type: Dict[str, MachineAction]
41        # Dict of all required actions by definition ID
42        self._required_actions = {}  # type: Dict[str, List[MachineAction]]
43        # Dict of all supported actions by definition ID
44        self._supported_actions = {}  # type: Dict[str, List[MachineAction]]
45        # Dict of all actions that need to be done when first added by definition ID
46        self._first_start_actions = {}  # type: Dict[str, List[MachineAction]]
47
48    def initialize(self) -> None:
49        # Add machine_action as plugin type
50        PluginRegistry.addType("machine_action", self.addMachineAction)
51
52    # Adds all default machine actions that are defined in the machine definition for the given machine.
53    def addDefaultMachineActions(self, global_stack: "GlobalStack") -> None:
54        definition_id = global_stack.definition.getId()
55
56        if definition_id in self._definition_ids_with_default_actions_added:
57            Logger.log("i", "Default machine actions have been added for machine definition [%s], do nothing.",
58                       definition_id)
59            return
60
61        supported_actions = global_stack.getMetaDataEntry("supported_actions", [])
62        for action_key in supported_actions:
63            self.addSupportedAction(definition_id, action_key)
64
65        required_actions = global_stack.getMetaDataEntry("required_actions", [])
66        for action_key in required_actions:
67            self.addRequiredAction(definition_id, action_key)
68
69        first_start_actions = global_stack.getMetaDataEntry("first_start_actions", [])
70        for action_key in first_start_actions:
71            self.addFirstStartAction(definition_id, action_key)
72
73        self._definition_ids_with_default_actions_added.add(definition_id)
74        Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id)
75
76    def addRequiredAction(self, definition_id: str, action_key: str) -> None:
77        """Add a required action to a machine
78
79        Raises an exception when the action is not recognised.
80        """
81        if action_key in self._machine_actions:
82            if definition_id in self._required_actions:
83                if self._machine_actions[action_key] not in self._required_actions[definition_id]:
84                    self._required_actions[definition_id].append(self._machine_actions[action_key])
85            else:
86                self._required_actions[definition_id] = [self._machine_actions[action_key]]
87        else:
88            raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id))
89
90    def addSupportedAction(self, definition_id: str, action_key: str) -> None:
91        """Add a supported action to a machine."""
92
93        if action_key in self._machine_actions:
94            if definition_id in self._supported_actions:
95                if self._machine_actions[action_key] not in self._supported_actions[definition_id]:
96                    self._supported_actions[definition_id].append(self._machine_actions[action_key])
97            else:
98                self._supported_actions[definition_id] = [self._machine_actions[action_key]]
99        else:
100            Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
101
102    def addFirstStartAction(self, definition_id: str, action_key: str) -> None:
103        """Add an action to the first start list of a machine."""
104
105        if action_key in self._machine_actions:
106            if definition_id in self._first_start_actions:
107                self._first_start_actions[definition_id].append(self._machine_actions[action_key])
108            else:
109                self._first_start_actions[definition_id] = [self._machine_actions[action_key]]
110        else:
111            Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
112
113    def addMachineAction(self, action: "MachineAction") -> None:
114        """Add a (unique) MachineAction
115
116        if the Key of the action is not unique, an exception is raised.
117        """
118        if action.getKey() not in self._machine_actions:
119            self._machine_actions[action.getKey()] = action
120        else:
121            raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey())
122
123    @pyqtSlot(str, result = "QVariantList")
124    def getSupportedActions(self, definition_id: str) -> List["MachineAction"]:
125        """Get all actions supported by given machine
126
127        :param definition_id: The ID of the definition you want the supported actions of
128        :returns: set of supported actions.
129        """
130        if definition_id in self._supported_actions:
131            return list(self._supported_actions[definition_id])
132        else:
133            return list()
134
135    def getRequiredActions(self, definition_id: str) -> List["MachineAction"]:
136        """Get all actions required by given machine
137
138        :param definition_id: The ID of the definition you want the required actions of
139        :returns: set of required actions.
140        """
141        if definition_id in self._required_actions:
142            return self._required_actions[definition_id]
143        else:
144            return list()
145
146    @pyqtSlot(str, result = "QVariantList")
147    def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
148        """Get all actions that need to be performed upon first start of a given machine.
149
150        Note that contrary to required / supported actions a list is returned (as it could be required to run the same
151        action multiple times).
152        :param definition_id: The ID of the definition that you want to get the "on added" actions for.
153        :returns: List of actions.
154        """
155        if definition_id in self._first_start_actions:
156            return self._first_start_actions[definition_id]
157        else:
158            return []
159
160    def removeMachineAction(self, action: "MachineAction") -> None:
161        """Remove Machine action from manager
162
163        :param action: to remove
164        """
165        try:
166            del self._machine_actions[action.getKey()]
167        except KeyError:
168            Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey())
169
170    def getMachineAction(self, key: str) -> Optional["MachineAction"]:
171        """Get MachineAction by key
172
173        :param key: String of key to select
174        :return: Machine action if found, None otherwise
175        """
176        if key in self._machine_actions:
177            return self._machine_actions[key]
178        else:
179            return None
180