1/**************************************************************************/
2/* multi_node_edit.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 "multi_node_edit.h"
32
33#include "core/math/math_fieldwise.h"
34#include "editor/editor_node.h"
35#include "editor/editor_undo_redo_manager.h"
36
37bool MultiNodeEdit::_set(const StringName &p_name, const Variant &p_value) {
38 return _set_impl(p_name, p_value, "");
39}
40
41bool MultiNodeEdit::_set_impl(const StringName &p_name, const Variant &p_value, const String &p_field) {
42 Node *es = EditorNode::get_singleton()->get_edited_scene();
43 if (!es) {
44 return false;
45 }
46
47 String name = p_name;
48
49 if (name == "scripts") { // Script set is intercepted at object level (check Variant Object::get()), so use a different name.
50 name = "script";
51 } else if (name.begins_with("Metadata/")) {
52 name = name.replace_first("Metadata/", "metadata/");
53 }
54
55 Node *node_path_target = nullptr;
56 if (p_value.get_type() == Variant::NODE_PATH && p_value != NodePath()) {
57 node_path_target = es->get_node(p_value);
58 }
59
60 EditorUndoRedoManager *ur = EditorUndoRedoManager::get_singleton();
61
62 ur->create_action(vformat(TTR("Set %s on %d nodes"), name, get_node_count()), UndoRedo::MERGE_ENDS);
63 for (const NodePath &E : nodes) {
64 Node *n = es->get_node_or_null(E);
65 if (!n) {
66 continue;
67 }
68
69 if (p_value.get_type() == Variant::NODE_PATH) {
70 NodePath path;
71 if (node_path_target) {
72 path = n->get_path_to(node_path_target);
73 }
74 ur->add_do_property(n, name, path);
75 } else {
76 Variant new_value;
77 if (p_field.is_empty()) {
78 // whole value
79 new_value = p_value;
80 } else {
81 // only one field
82 new_value = fieldwise_assign(n->get(name), p_value, p_field);
83 }
84 ur->add_do_property(n, name, new_value);
85 }
86
87 ur->add_undo_property(n, name, n->get(name));
88 }
89
90 ur->commit_action();
91 return true;
92}
93
94bool MultiNodeEdit::_get(const StringName &p_name, Variant &r_ret) const {
95 Node *es = EditorNode::get_singleton()->get_edited_scene();
96 if (!es) {
97 return false;
98 }
99
100 String name = p_name;
101 if (name == "scripts") { // Script set is intercepted at object level (check Variant Object::get()), so use a different name.
102 name = "script";
103 } else if (name.begins_with("Metadata/")) {
104 name = name.replace_first("Metadata/", "metadata/");
105 }
106
107 for (const NodePath &E : nodes) {
108 const Node *n = es->get_node_or_null(E);
109 if (!n) {
110 continue;
111 }
112
113 bool found;
114 r_ret = n->get(name, &found);
115 if (found) {
116 return true;
117 }
118 }
119
120 return false;
121}
122
123void MultiNodeEdit::_get_property_list(List<PropertyInfo> *p_list) const {
124 HashMap<String, PLData> usage;
125
126 Node *es = EditorNode::get_singleton()->get_edited_scene();
127 if (!es) {
128 return;
129 }
130
131 int nc = 0;
132
133 List<PLData *> data_list;
134
135 for (const NodePath &E : nodes) {
136 Node *n = es->get_node_or_null(E);
137 if (!n) {
138 continue;
139 }
140
141 List<PropertyInfo> plist;
142 n->get_property_list(&plist, true);
143
144 for (PropertyInfo F : plist) {
145 if (F.name == "script") {
146 continue; // Added later manually, since this is intercepted before being set (check Variant Object::get()).
147 } else if (F.name.begins_with("metadata/")) {
148 F.name = F.name.replace_first("metadata/", "Metadata/"); // Trick to not get actual metadata edited from MultiNodeEdit.
149 }
150
151 if (!usage.has(F.name)) {
152 PLData pld;
153 pld.uses = 0;
154 pld.info = F;
155 pld.info.name = F.name;
156 usage[F.name] = pld;
157 data_list.push_back(usage.getptr(F.name));
158 }
159
160 // Make sure only properties with the same exact PropertyInfo data will appear.
161 if (usage[F.name].info == F) {
162 usage[F.name].uses++;
163 }
164 }
165
166 nc++;
167 }
168
169 for (const PLData *E : data_list) {
170 if (nc == E->uses) {
171 p_list->push_back(E->info);
172 }
173 }
174
175 p_list->push_back(PropertyInfo(Variant::OBJECT, "scripts", PROPERTY_HINT_RESOURCE_TYPE, "Script"));
176}
177
178String MultiNodeEdit::_get_editor_name() const {
179 return vformat(TTR("%s (%d Selected)"), get_edited_class_name(), get_node_count());
180}
181
182bool MultiNodeEdit::_property_can_revert(const StringName &p_name) const {
183 Node *es = EditorNode::get_singleton()->get_edited_scene();
184 if (!es) {
185 return false;
186 }
187
188 if (ClassDB::has_property(get_edited_class_name(), p_name)) {
189 StringName class_name;
190 for (const NodePath &E : nodes) {
191 Node *node = es->get_node_or_null(E);
192 if (!node) {
193 continue;
194 }
195
196 class_name = node->get_class_name();
197 }
198
199 Variant default_value = ClassDB::class_get_default_property_value(class_name, p_name);
200 for (const NodePath &E : nodes) {
201 Node *node = es->get_node_or_null(E);
202 if (!node) {
203 continue;
204 }
205
206 if (node->get(p_name) != default_value) {
207 // A node that doesn't have the default value has been found, so show the revert button.
208 return true;
209 }
210 }
211
212 return false;
213 }
214
215 // Don't show the revert button if the edited class doesn't have the property.
216 return false;
217}
218
219bool MultiNodeEdit::_property_get_revert(const StringName &p_name, Variant &r_property) const {
220 Node *es = EditorNode::get_singleton()->get_edited_scene();
221 if (!es) {
222 return false;
223 }
224
225 for (const NodePath &E : nodes) {
226 Node *node = es->get_node_or_null(E);
227 if (!node) {
228 continue;
229 }
230
231 r_property = ClassDB::class_get_default_property_value(node->get_class_name(), p_name);
232 return true;
233 }
234
235 return false;
236}
237
238void MultiNodeEdit::add_node(const NodePath &p_node) {
239 nodes.push_back(p_node);
240}
241
242int MultiNodeEdit::get_node_count() const {
243 return nodes.size();
244}
245
246NodePath MultiNodeEdit::get_node(int p_index) const {
247 ERR_FAIL_INDEX_V(p_index, nodes.size(), NodePath());
248 return nodes[p_index];
249}
250
251StringName MultiNodeEdit::get_edited_class_name() const {
252 Node *es = EditorNode::get_singleton()->get_edited_scene();
253 if (!es) {
254 return SNAME("Node");
255 }
256
257 // Get the class name of the first node.
258 StringName class_name;
259 for (const NodePath &E : nodes) {
260 Node *node = es->get_node_or_null(E);
261 if (!node) {
262 continue;
263 }
264
265 class_name = node->get_class_name();
266 break;
267 }
268
269 if (class_name == StringName()) {
270 return SNAME("Node");
271 }
272
273 bool check_again = true;
274 while (check_again) {
275 check_again = false;
276
277 if (class_name == SNAME("Node") || class_name == StringName()) {
278 // All nodes inherit from Node, so no need to continue checking.
279 return SNAME("Node");
280 }
281
282 // Check that all nodes inherit from class_name.
283 for (const NodePath &E : nodes) {
284 Node *node = es->get_node_or_null(E);
285 if (!node) {
286 continue;
287 }
288
289 const StringName node_class_name = node->get_class_name();
290 if (class_name == node_class_name || ClassDB::is_parent_class(node_class_name, class_name)) {
291 // class_name is the same or a parent of the node's class.
292 continue;
293 }
294
295 // class_name is not a parent of the node's class, so check again with the parent class.
296 class_name = ClassDB::get_parent_class(class_name);
297 check_again = true;
298 break;
299 }
300 }
301
302 return class_name;
303}
304
305void MultiNodeEdit::set_property_field(const StringName &p_property, const Variant &p_value, const String &p_field) {
306 _set_impl(p_property, p_value, p_field);
307}
308
309void MultiNodeEdit::_bind_methods() {
310 ClassDB::bind_method("_hide_script_from_inspector", &MultiNodeEdit::_hide_script_from_inspector);
311 ClassDB::bind_method("_hide_metadata_from_inspector", &MultiNodeEdit::_hide_metadata_from_inspector);
312 ClassDB::bind_method("_get_editor_name", &MultiNodeEdit::_get_editor_name);
313}
314
315MultiNodeEdit::MultiNodeEdit() {
316}
317