1 /*
2     Copyright © 2020, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module creator.actions.node;
8 import creator.core.actionstack;
9 import creator.actions;
10 import creator;
11 import inochi2d;
12 import std.format;
13 import i18n;
14 import std.exception;
15 
16 /**
17     An action that happens when a node is changed
18 */
19 class NodeMoveAction : Action {
20 public:
21 
22     /**
23         Descriptive name
24     */
25     string descrName;
26     
27     /**
28         Which index in to the parent the nodes should be placed
29     */
30     size_t parentOffset;
31 
32     /**
33         Previous parent of node
34     */
35     Node[uint] prevParents;
36     size_t[uint] prevOffsets;
37 
38     /**
39         Nodes that was moved
40     */
41     Node[] nodes;
42 
43     /**
44         New parent of node
45     */
46     Node newParent;
47 
48     /**
49         The original transform of the node
50     */
51     Transform originalTransform;
52 
53     /**
54         The new transform of the node
55     */
56     Transform newTransform;
57 
58     /**
59         Creates a new node change action
60     */
61     this(Node[] nodes, Node new_, size_t pOffset = 0) {
62         this.newParent = new_;
63         this.nodes = nodes;
64         this.parentOffset = pOffset;
65 
66         // Enforce reparenting rules
67         foreach(sn; nodes) enforce(sn.canReparent(new_), _("%s can not be reparented in to %s due to a circular dependency.").format(sn.name, new_.name));
68         
69         // Reparent
70         foreach(ref sn; nodes) {
71             
72             // Store ref to prev parent
73             if (sn.parent) {
74                 prevParents[sn.uuid] = sn.parent;
75                 prevOffsets[sn.uuid] = sn.getIndexInParent();
76             }
77 
78             // Set relative position
79             sn.setRelativeTo(new_);
80             sn.parent = new_;
81             sn.insertInto(new_, pOffset);
82         }
83         incActivePuppet().rescanNodes();
84     
85         // Set visual name
86         if (nodes.length == 1) descrName = nodes[0].name;
87         else descrName = _("nodes");
88     }
89 
90     /**
91         Rollback
92     */
93     void rollback() {
94         foreach(ref sn; nodes) {
95             sn.setRelativeTo(prevParents[sn.uuid]);
96             sn.insertInto(prevParents[sn.uuid], prevOffsets[sn.uuid]);
97         }
98         incActivePuppet().rescanNodes();
99     }
100 
101     /**
102         Redo
103     */
104     void redo() {
105         foreach(sn; nodes) {
106             sn.setRelativeTo(newParent);
107             sn.insertInto(newParent, parentOffset);
108         }
109         incActivePuppet().rescanNodes();
110     }
111 
112     /**
113         Describe the action
114     */
115     string describe() {
116         if (prevParents.length == 0) return _("Created %s").format(descrName);
117         if (newParent is null) return _("Deleted %s").format(descrName);
118         return _("Moved %s to %s").format(descrName, newParent.name);
119     }
120 
121     /**
122         Describe the action
123     */
124     string describeUndo() {
125         if (prevParents.length == 0) return _("Created %s").format(descrName);
126         if (nodes.length == 1 && prevParents.length == 1) return  _("Moved %s from %s").format(descrName, prevParents[nodes[0].uuid].name);
127         return _("Moved %s from origin").format(descrName);
128     }
129 
130     /**
131         Gets name of this action
132     */
133     string getName() {
134         return this.stringof;
135     }
136     
137     bool merge(Action other) { return false; }
138     bool canMerge(Action other) { return false; }
139 }
140 
141 /**
142     Action for whether a node was activated or deactivated
143 */
144 class NodeActiveAction : Action {
145 public:
146     Node self;
147     bool newState;
148 
149     /**
150         Rollback
151     */
152     void rollback() {
153         self.enabled = !newState;
154     }
155 
156     /**
157         Redo
158     */
159     void redo() {
160         self.enabled = newState;
161     }
162 
163     /**
164         Describe the action
165     */
166     string describe() {
167         return "%s %s".format(newState ? _("Enabled") : _("Disabled"), self.name);
168     }
169 
170     /**
171         Describe the action
172     */
173     string describeUndo() {
174         return _("%s was %s").format(self.name, !newState ? _("Enabled") : _("Disabled"));
175     }
176 
177     /**
178         Gets name of this action
179     */
180     string getName() {
181         return this.stringof;
182     }
183     
184     bool merge(Action other) { return false; }
185     bool canMerge(Action other) { return false; }
186 }
187 
188 /**
189     Moves multiple children with history
190 */
191 void incMoveChildrenWithHistory(Node[] n, Node to, size_t offset) {
192     // Push action to stack
193     incActionPush(new NodeMoveAction(
194         n,
195         to,
196         offset
197     ));
198 }
199 
200 /**
201     Moves child with history
202 */
203 void incMoveChildWithHistory(Node n, Node to, size_t offset) {
204     incMoveChildrenWithHistory([n], to, offset);
205 }
206 
207 /**
208     Adds child with history
209 */
210 void incAddChildWithHistory(Node n, Node to, string name=null) {
211     if (to is null) to = incActivePuppet().root;
212 
213     // Push action to stack
214     incActionPush(new NodeMoveAction(
215         [n],
216         to
217     ));
218 
219     n.insertInto(to, Node.OFFSET_START);
220     if (name is null) n.name = _("Unnamed ")~_(n.typeId());
221     else n.name = name;
222     incActivePuppet().rescanNodes();
223 }
224 
225 /**
226     Deletes child with history
227 */
228 void incDeleteChildWithHistory(Node n) {
229     // Push action to stack
230     incActionPush(new NodeMoveAction(
231         [n],
232         null
233     ));
234 
235     n.parent = null;
236     incActivePuppet().rescanNodes();
237 }
238 
239 /**
240     Node value changed action
241 */
242 class NodeValueChangeAction(TNode, T) : Action if (is(TNode : Node)) {
243 public:
244     alias TSelf = typeof(this);
245     TNode node;
246     T oldValue;
247     T newValue;
248     T* valuePtr;
249     string name;
250 
251     this(string name, TNode node, T oldValue, T newValue, T* valuePtr) {
252         this.name = name;
253         this.node = node;
254         this.oldValue = oldValue;
255         this.newValue = newValue;
256         this.valuePtr = valuePtr;
257     }
258 
259     /**
260         Rollback
261     */
262     void rollback() {
263         *valuePtr = oldValue;
264     }
265 
266     /**
267         Redo
268     */
269     void redo() {
270         *valuePtr = newValue;
271     }
272 
273     /**
274         Describe the action
275     */
276     string describe() {
277         return _("%s->%s changed to %s").format(node.name, name, newValue);
278     }
279 
280     /**
281         Describe the action
282     */
283     string describeUndo() {
284         return _("%s->%s changed from %s").format(node.name, name, oldValue);
285     }
286 
287     /**
288         Gets name of this action
289     */
290     string getName() {
291         return name;
292     }
293     
294     /**
295         Merge
296     */
297     bool merge(Action other) {
298         if (this.canMerge(other)) {
299             this.newValue = (cast(TSelf)other).newValue;
300             return true;
301         }
302         return false;
303     }
304 
305     /**
306         Gets whether this node can merge with an other
307     */
308     bool canMerge(Action other) {
309         TSelf otherChange = cast(TSelf) other;
310         return (otherChange !is null && otherChange.getName() == this.getName());
311     }
312 }