1 | /**************************************************************************/ |
2 | /* undo_redo.cpp */ |
3 | /**************************************************************************/ |
4 | /* This file is part of: */ |
5 | /* GODOT ENGINE */ |
6 | /* https://godotengine.org */ |
7 | /**************************************************************************/ |
8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
10 | /* */ |
11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
12 | /* a copy of this software and associated documentation files (the */ |
13 | /* "Software"), to deal in the Software without restriction, including */ |
14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
17 | /* the following conditions: */ |
18 | /* */ |
19 | /* The above copyright notice and this permission notice shall be */ |
20 | /* included in all copies or substantial portions of the Software. */ |
21 | /* */ |
22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
29 | /**************************************************************************/ |
30 | |
31 | #include "undo_redo.h" |
32 | |
33 | #include "core/io/resource.h" |
34 | #include "core/os/os.h" |
35 | #include "core/templates/local_vector.h" |
36 | |
37 | void UndoRedo::Operation::delete_reference() { |
38 | if (type != Operation::TYPE_REFERENCE) { |
39 | return; |
40 | } |
41 | if (ref.is_valid()) { |
42 | ref.unref(); |
43 | } else { |
44 | Object *obj = ObjectDB::get_instance(object); |
45 | if (obj) { |
46 | memdelete(obj); |
47 | } |
48 | } |
49 | } |
50 | |
51 | void UndoRedo::_discard_redo() { |
52 | if (current_action == actions.size() - 1) { |
53 | return; |
54 | } |
55 | |
56 | for (int i = current_action + 1; i < actions.size(); i++) { |
57 | for (Operation &E : actions.write[i].do_ops) { |
58 | E.delete_reference(); |
59 | } |
60 | //ERASE do data |
61 | } |
62 | |
63 | actions.resize(current_action + 1); |
64 | } |
65 | |
66 | bool UndoRedo::_redo(bool p_execute) { |
67 | ERR_FAIL_COND_V(action_level > 0, false); |
68 | |
69 | if ((current_action + 1) >= actions.size()) { |
70 | return false; //nothing to redo |
71 | } |
72 | |
73 | current_action++; |
74 | if (p_execute) { |
75 | _process_operation_list(actions.write[current_action].do_ops.front()); |
76 | } |
77 | version++; |
78 | emit_signal(SNAME("version_changed" )); |
79 | |
80 | return true; |
81 | } |
82 | |
83 | void UndoRedo::create_action(const String &p_name, MergeMode p_mode, bool p_backward_undo_ops) { |
84 | uint64_t ticks = OS::get_singleton()->get_ticks_msec(); |
85 | |
86 | if (action_level == 0) { |
87 | _discard_redo(); |
88 | |
89 | // Check if the merge operation is valid |
90 | if (p_mode != MERGE_DISABLE && actions.size() && actions[actions.size() - 1].name == p_name && actions[actions.size() - 1].backward_undo_ops == p_backward_undo_ops && actions[actions.size() - 1].last_tick + 800 > ticks) { |
91 | current_action = actions.size() - 2; |
92 | |
93 | if (p_mode == MERGE_ENDS) { |
94 | // Clear all do ops from last action if they are not forced kept |
95 | LocalVector<List<Operation>::Element *> to_remove; |
96 | for (List<Operation>::Element *E = actions.write[current_action + 1].do_ops.front(); E; E = E->next()) { |
97 | if (!E->get().force_keep_in_merge_ends) { |
98 | to_remove.push_back(E); |
99 | } |
100 | } |
101 | |
102 | for (List<Operation>::Element *E : to_remove) { |
103 | // Delete all object references |
104 | E->get().delete_reference(); |
105 | E->erase(); |
106 | } |
107 | } |
108 | |
109 | actions.write[actions.size() - 1].last_tick = ticks; |
110 | |
111 | // Revert reverse from previous commit. |
112 | if (actions[actions.size() - 1].backward_undo_ops) { |
113 | actions.write[actions.size() - 1].undo_ops.reverse(); |
114 | } |
115 | |
116 | merge_mode = p_mode; |
117 | merging = true; |
118 | } else { |
119 | Action new_action; |
120 | new_action.name = p_name; |
121 | new_action.last_tick = ticks; |
122 | new_action.backward_undo_ops = p_backward_undo_ops; |
123 | actions.push_back(new_action); |
124 | |
125 | merge_mode = MERGE_DISABLE; |
126 | } |
127 | } |
128 | |
129 | action_level++; |
130 | |
131 | force_keep_in_merge_ends = false; |
132 | } |
133 | |
134 | void UndoRedo::add_do_method(const Callable &p_callable) { |
135 | ERR_FAIL_COND(p_callable.is_null()); |
136 | ERR_FAIL_COND(action_level <= 0); |
137 | ERR_FAIL_COND((current_action + 1) >= actions.size()); |
138 | |
139 | Object *object = p_callable.get_object(); |
140 | ERR_FAIL_NULL(object); |
141 | |
142 | Operation do_op; |
143 | do_op.callable = p_callable; |
144 | do_op.object = p_callable.get_object_id(); |
145 | if (Object::cast_to<RefCounted>(object)) { |
146 | do_op.ref = Ref<RefCounted>(Object::cast_to<RefCounted>(object)); |
147 | } |
148 | do_op.type = Operation::TYPE_METHOD; |
149 | do_op.name = p_callable.get_method(); |
150 | |
151 | actions.write[current_action + 1].do_ops.push_back(do_op); |
152 | } |
153 | |
154 | void UndoRedo::add_undo_method(const Callable &p_callable) { |
155 | ERR_FAIL_COND(p_callable.is_null()); |
156 | ERR_FAIL_COND(action_level <= 0); |
157 | ERR_FAIL_COND((current_action + 1) >= actions.size()); |
158 | |
159 | // No undo if the merge mode is MERGE_ENDS |
160 | if (!force_keep_in_merge_ends && merge_mode == MERGE_ENDS) { |
161 | return; |
162 | } |
163 | |
164 | Object *object = p_callable.get_object(); |
165 | ERR_FAIL_NULL(object); |
166 | |
167 | Operation undo_op; |
168 | undo_op.callable = p_callable; |
169 | undo_op.object = p_callable.get_object_id(); |
170 | if (Object::cast_to<RefCounted>(object)) { |
171 | undo_op.ref = Ref<RefCounted>(Object::cast_to<RefCounted>(object)); |
172 | } |
173 | undo_op.type = Operation::TYPE_METHOD; |
174 | undo_op.force_keep_in_merge_ends = force_keep_in_merge_ends; |
175 | undo_op.name = p_callable.get_method(); |
176 | |
177 | actions.write[current_action + 1].undo_ops.push_back(undo_op); |
178 | } |
179 | |
180 | void UndoRedo::add_do_property(Object *p_object, const StringName &p_property, const Variant &p_value) { |
181 | ERR_FAIL_NULL(p_object); |
182 | ERR_FAIL_COND(action_level <= 0); |
183 | ERR_FAIL_COND((current_action + 1) >= actions.size()); |
184 | Operation do_op; |
185 | do_op.object = p_object->get_instance_id(); |
186 | if (Object::cast_to<RefCounted>(p_object)) { |
187 | do_op.ref = Ref<RefCounted>(Object::cast_to<RefCounted>(p_object)); |
188 | } |
189 | |
190 | do_op.type = Operation::TYPE_PROPERTY; |
191 | do_op.name = p_property; |
192 | do_op.value = p_value; |
193 | actions.write[current_action + 1].do_ops.push_back(do_op); |
194 | } |
195 | |
196 | void UndoRedo::add_undo_property(Object *p_object, const StringName &p_property, const Variant &p_value) { |
197 | ERR_FAIL_NULL(p_object); |
198 | ERR_FAIL_COND(action_level <= 0); |
199 | ERR_FAIL_COND((current_action + 1) >= actions.size()); |
200 | |
201 | // No undo if the merge mode is MERGE_ENDS |
202 | if (!force_keep_in_merge_ends && merge_mode == MERGE_ENDS) { |
203 | return; |
204 | } |
205 | |
206 | Operation undo_op; |
207 | undo_op.object = p_object->get_instance_id(); |
208 | if (Object::cast_to<RefCounted>(p_object)) { |
209 | undo_op.ref = Ref<RefCounted>(Object::cast_to<RefCounted>(p_object)); |
210 | } |
211 | |
212 | undo_op.type = Operation::TYPE_PROPERTY; |
213 | undo_op.force_keep_in_merge_ends = force_keep_in_merge_ends; |
214 | undo_op.name = p_property; |
215 | undo_op.value = p_value; |
216 | actions.write[current_action + 1].undo_ops.push_back(undo_op); |
217 | } |
218 | |
219 | void UndoRedo::add_do_reference(Object *p_object) { |
220 | ERR_FAIL_NULL(p_object); |
221 | ERR_FAIL_COND(action_level <= 0); |
222 | ERR_FAIL_COND((current_action + 1) >= actions.size()); |
223 | Operation do_op; |
224 | do_op.object = p_object->get_instance_id(); |
225 | if (Object::cast_to<RefCounted>(p_object)) { |
226 | do_op.ref = Ref<RefCounted>(Object::cast_to<RefCounted>(p_object)); |
227 | } |
228 | |
229 | do_op.type = Operation::TYPE_REFERENCE; |
230 | actions.write[current_action + 1].do_ops.push_back(do_op); |
231 | } |
232 | |
233 | void UndoRedo::add_undo_reference(Object *p_object) { |
234 | ERR_FAIL_NULL(p_object); |
235 | ERR_FAIL_COND(action_level <= 0); |
236 | ERR_FAIL_COND((current_action + 1) >= actions.size()); |
237 | |
238 | // No undo if the merge mode is MERGE_ENDS |
239 | if (!force_keep_in_merge_ends && merge_mode == MERGE_ENDS) { |
240 | return; |
241 | } |
242 | |
243 | Operation undo_op; |
244 | undo_op.object = p_object->get_instance_id(); |
245 | if (Object::cast_to<RefCounted>(p_object)) { |
246 | undo_op.ref = Ref<RefCounted>(Object::cast_to<RefCounted>(p_object)); |
247 | } |
248 | |
249 | undo_op.type = Operation::TYPE_REFERENCE; |
250 | undo_op.force_keep_in_merge_ends = force_keep_in_merge_ends; |
251 | actions.write[current_action + 1].undo_ops.push_back(undo_op); |
252 | } |
253 | |
254 | void UndoRedo::start_force_keep_in_merge_ends() { |
255 | ERR_FAIL_COND(action_level <= 0); |
256 | ERR_FAIL_COND((current_action + 1) >= actions.size()); |
257 | |
258 | force_keep_in_merge_ends = true; |
259 | } |
260 | |
261 | void UndoRedo::end_force_keep_in_merge_ends() { |
262 | ERR_FAIL_COND(action_level <= 0); |
263 | ERR_FAIL_COND((current_action + 1) >= actions.size()); |
264 | |
265 | force_keep_in_merge_ends = false; |
266 | } |
267 | |
268 | void UndoRedo::_pop_history_tail() { |
269 | _discard_redo(); |
270 | |
271 | if (!actions.size()) { |
272 | return; |
273 | } |
274 | |
275 | for (Operation &E : actions.write[0].undo_ops) { |
276 | E.delete_reference(); |
277 | } |
278 | |
279 | actions.remove_at(0); |
280 | if (current_action >= 0) { |
281 | current_action--; |
282 | } |
283 | } |
284 | |
285 | bool UndoRedo::is_committing_action() const { |
286 | return committing > 0; |
287 | } |
288 | |
289 | void UndoRedo::commit_action(bool p_execute) { |
290 | ERR_FAIL_COND(action_level <= 0); |
291 | action_level--; |
292 | if (action_level > 0) { |
293 | return; //still nested |
294 | } |
295 | |
296 | if (merging) { |
297 | version--; |
298 | merging = false; |
299 | } |
300 | |
301 | if (actions[actions.size() - 1].backward_undo_ops) { |
302 | actions.write[actions.size() - 1].undo_ops.reverse(); |
303 | } |
304 | |
305 | committing++; |
306 | _redo(p_execute); // perform action |
307 | committing--; |
308 | |
309 | if (callback && actions.size() > 0) { |
310 | callback(callback_ud, actions[actions.size() - 1].name); |
311 | } |
312 | } |
313 | |
314 | void UndoRedo::_process_operation_list(List<Operation>::Element *E) { |
315 | const int PREALLOCATE_ARGS_COUNT = 16; |
316 | |
317 | LocalVector<const Variant *> args; |
318 | args.reserve(PREALLOCATE_ARGS_COUNT); |
319 | |
320 | for (; E; E = E->next()) { |
321 | Operation &op = E->get(); |
322 | |
323 | Object *obj = ObjectDB::get_instance(op.object); |
324 | if (!obj) { //may have been deleted and this is fine |
325 | continue; |
326 | } |
327 | |
328 | switch (op.type) { |
329 | case Operation::TYPE_METHOD: { |
330 | Callable::CallError ce; |
331 | Variant ret; |
332 | op.callable.callp(nullptr, 0, ret, ce); |
333 | if (ce.error != Callable::CallError::CALL_OK) { |
334 | ERR_PRINT("Error calling UndoRedo method operation '" + String(op.name) + "': " + Variant::get_call_error_text(obj, op.name, nullptr, 0, ce)); |
335 | } |
336 | #ifdef TOOLS_ENABLED |
337 | Resource *res = Object::cast_to<Resource>(obj); |
338 | if (res) { |
339 | res->set_edited(true); |
340 | } |
341 | #endif |
342 | |
343 | if (method_callback) { |
344 | Vector<Variant> binds; |
345 | if (op.callable.is_custom()) { |
346 | CallableCustomBind *ccb = dynamic_cast<CallableCustomBind *>(op.callable.get_custom()); |
347 | if (ccb) { |
348 | binds = ccb->get_binds(); |
349 | } |
350 | } |
351 | |
352 | if (binds.is_empty()) { |
353 | method_callback(method_callback_ud, obj, op.name, nullptr, 0); |
354 | } else { |
355 | args.clear(); |
356 | |
357 | for (int i = 0; i < binds.size(); i++) { |
358 | args.push_back(&binds[i]); |
359 | } |
360 | |
361 | method_callback(method_callback_ud, obj, op.name, args.ptr(), binds.size()); |
362 | } |
363 | } |
364 | } break; |
365 | case Operation::TYPE_PROPERTY: { |
366 | obj->set(op.name, op.value); |
367 | #ifdef TOOLS_ENABLED |
368 | Resource *res = Object::cast_to<Resource>(obj); |
369 | if (res) { |
370 | res->set_edited(true); |
371 | } |
372 | #endif |
373 | if (property_callback) { |
374 | property_callback(prop_callback_ud, obj, op.name, op.value); |
375 | } |
376 | } break; |
377 | case Operation::TYPE_REFERENCE: { |
378 | //do nothing |
379 | } break; |
380 | } |
381 | } |
382 | } |
383 | |
384 | bool UndoRedo::redo() { |
385 | return _redo(true); |
386 | } |
387 | |
388 | bool UndoRedo::undo() { |
389 | ERR_FAIL_COND_V(action_level > 0, false); |
390 | if (current_action < 0) { |
391 | return false; //nothing to redo |
392 | } |
393 | _process_operation_list(actions.write[current_action].undo_ops.front()); |
394 | current_action--; |
395 | version--; |
396 | emit_signal(SNAME("version_changed" )); |
397 | |
398 | return true; |
399 | } |
400 | |
401 | int UndoRedo::get_history_count() { |
402 | ERR_FAIL_COND_V(action_level > 0, -1); |
403 | |
404 | return actions.size(); |
405 | } |
406 | |
407 | int UndoRedo::get_current_action() { |
408 | ERR_FAIL_COND_V(action_level > 0, -1); |
409 | |
410 | return current_action; |
411 | } |
412 | |
413 | String UndoRedo::get_action_name(int p_id) { |
414 | ERR_FAIL_INDEX_V(p_id, actions.size(), "" ); |
415 | |
416 | return actions[p_id].name; |
417 | } |
418 | |
419 | void UndoRedo::clear_history(bool p_increase_version) { |
420 | ERR_FAIL_COND(action_level > 0); |
421 | _discard_redo(); |
422 | |
423 | while (actions.size()) { |
424 | _pop_history_tail(); |
425 | } |
426 | |
427 | if (p_increase_version) { |
428 | version++; |
429 | emit_signal(SNAME("version_changed" )); |
430 | } |
431 | } |
432 | |
433 | String UndoRedo::get_current_action_name() const { |
434 | ERR_FAIL_COND_V(action_level > 0, "" ); |
435 | if (current_action < 0) { |
436 | return "" ; |
437 | } |
438 | return actions[current_action].name; |
439 | } |
440 | |
441 | int UndoRedo::get_action_level() const { |
442 | return action_level; |
443 | } |
444 | |
445 | bool UndoRedo::has_undo() const { |
446 | return current_action >= 0; |
447 | } |
448 | |
449 | bool UndoRedo::has_redo() const { |
450 | return (current_action + 1) < actions.size(); |
451 | } |
452 | |
453 | uint64_t UndoRedo::get_version() const { |
454 | return version; |
455 | } |
456 | |
457 | void UndoRedo::set_commit_notify_callback(CommitNotifyCallback p_callback, void *p_ud) { |
458 | callback = p_callback; |
459 | callback_ud = p_ud; |
460 | } |
461 | |
462 | void UndoRedo::set_method_notify_callback(MethodNotifyCallback p_method_callback, void *p_ud) { |
463 | method_callback = p_method_callback; |
464 | method_callback_ud = p_ud; |
465 | } |
466 | |
467 | void UndoRedo::set_property_notify_callback(PropertyNotifyCallback p_property_callback, void *p_ud) { |
468 | property_callback = p_property_callback; |
469 | prop_callback_ud = p_ud; |
470 | } |
471 | |
472 | UndoRedo::~UndoRedo() { |
473 | clear_history(); |
474 | } |
475 | |
476 | void UndoRedo::_bind_methods() { |
477 | ClassDB::bind_method(D_METHOD("create_action" , "name" , "merge_mode" , "backward_undo_ops" ), &UndoRedo::create_action, DEFVAL(MERGE_DISABLE), DEFVAL(false)); |
478 | ClassDB::bind_method(D_METHOD("commit_action" , "execute" ), &UndoRedo::commit_action, DEFVAL(true)); |
479 | ClassDB::bind_method(D_METHOD("is_committing_action" ), &UndoRedo::is_committing_action); |
480 | |
481 | ClassDB::bind_method(D_METHOD("add_do_method" , "callable" ), &UndoRedo::add_do_method); |
482 | ClassDB::bind_method(D_METHOD("add_undo_method" , "callable" ), &UndoRedo::add_undo_method); |
483 | ClassDB::bind_method(D_METHOD("add_do_property" , "object" , "property" , "value" ), &UndoRedo::add_do_property); |
484 | ClassDB::bind_method(D_METHOD("add_undo_property" , "object" , "property" , "value" ), &UndoRedo::add_undo_property); |
485 | ClassDB::bind_method(D_METHOD("add_do_reference" , "object" ), &UndoRedo::add_do_reference); |
486 | ClassDB::bind_method(D_METHOD("add_undo_reference" , "object" ), &UndoRedo::add_undo_reference); |
487 | |
488 | ClassDB::bind_method(D_METHOD("start_force_keep_in_merge_ends" ), &UndoRedo::start_force_keep_in_merge_ends); |
489 | ClassDB::bind_method(D_METHOD("end_force_keep_in_merge_ends" ), &UndoRedo::end_force_keep_in_merge_ends); |
490 | |
491 | ClassDB::bind_method(D_METHOD("get_history_count" ), &UndoRedo::get_history_count); |
492 | ClassDB::bind_method(D_METHOD("get_current_action" ), &UndoRedo::get_current_action); |
493 | ClassDB::bind_method(D_METHOD("get_action_name" , "id" ), &UndoRedo::get_action_name); |
494 | ClassDB::bind_method(D_METHOD("clear_history" , "increase_version" ), &UndoRedo::clear_history, DEFVAL(true)); |
495 | |
496 | ClassDB::bind_method(D_METHOD("get_current_action_name" ), &UndoRedo::get_current_action_name); |
497 | |
498 | ClassDB::bind_method(D_METHOD("has_undo" ), &UndoRedo::has_undo); |
499 | ClassDB::bind_method(D_METHOD("has_redo" ), &UndoRedo::has_redo); |
500 | ClassDB::bind_method(D_METHOD("get_version" ), &UndoRedo::get_version); |
501 | ClassDB::bind_method(D_METHOD("redo" ), &UndoRedo::redo); |
502 | ClassDB::bind_method(D_METHOD("undo" ), &UndoRedo::undo); |
503 | |
504 | ADD_SIGNAL(MethodInfo("version_changed" )); |
505 | |
506 | BIND_ENUM_CONSTANT(MERGE_DISABLE); |
507 | BIND_ENUM_CONSTANT(MERGE_ENDS); |
508 | BIND_ENUM_CONSTANT(MERGE_ALL); |
509 | } |
510 | |