1 | /**************************************************************************/ |
2 | /* replication_editor.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 "replication_editor.h" |
32 | |
33 | #include "../multiplayer_synchronizer.h" |
34 | |
35 | #include "editor/editor_node.h" |
36 | #include "editor/editor_scale.h" |
37 | #include "editor/editor_settings.h" |
38 | #include "editor/editor_string_names.h" |
39 | #include "editor/editor_undo_redo_manager.h" |
40 | #include "editor/gui/scene_tree_editor.h" |
41 | #include "editor/inspector_dock.h" |
42 | #include "editor/property_selector.h" |
43 | #include "scene/gui/dialogs.h" |
44 | #include "scene/gui/separator.h" |
45 | #include "scene/gui/tree.h" |
46 | |
47 | void ReplicationEditor::_pick_node_filter_text_changed(const String &p_newtext) { |
48 | TreeItem *root_item = pick_node->get_scene_tree()->get_scene_tree()->get_root(); |
49 | |
50 | Vector<Node *> select_candidates; |
51 | Node *to_select = nullptr; |
52 | |
53 | String filter = pick_node->get_filter_line_edit()->get_text(); |
54 | |
55 | _pick_node_select_recursive(root_item, filter, select_candidates); |
56 | |
57 | if (!select_candidates.is_empty()) { |
58 | for (int i = 0; i < select_candidates.size(); ++i) { |
59 | Node *candidate = select_candidates[i]; |
60 | |
61 | if (((String)candidate->get_name()).to_lower().begins_with(filter.to_lower())) { |
62 | to_select = candidate; |
63 | break; |
64 | } |
65 | } |
66 | |
67 | if (!to_select) { |
68 | to_select = select_candidates[0]; |
69 | } |
70 | } |
71 | |
72 | pick_node->get_scene_tree()->set_selected(to_select); |
73 | } |
74 | |
75 | void ReplicationEditor::_pick_node_select_recursive(TreeItem *p_item, const String &p_filter, Vector<Node *> &p_select_candidates) { |
76 | if (!p_item) { |
77 | return; |
78 | } |
79 | |
80 | NodePath np = p_item->get_metadata(0); |
81 | Node *node = get_node(np); |
82 | |
83 | if (!p_filter.is_empty() && ((String)node->get_name()).findn(p_filter) != -1) { |
84 | p_select_candidates.push_back(node); |
85 | } |
86 | |
87 | TreeItem *c = p_item->get_first_child(); |
88 | |
89 | while (c) { |
90 | _pick_node_select_recursive(c, p_filter, p_select_candidates); |
91 | c = c->get_next(); |
92 | } |
93 | } |
94 | |
95 | void ReplicationEditor::_pick_node_filter_input(const Ref<InputEvent> &p_ie) { |
96 | Ref<InputEventKey> k = p_ie; |
97 | |
98 | if (k.is_valid()) { |
99 | switch (k->get_keycode()) { |
100 | case Key::UP: |
101 | case Key::DOWN: |
102 | case Key::PAGEUP: |
103 | case Key::PAGEDOWN: { |
104 | pick_node->get_scene_tree()->get_scene_tree()->gui_input(k); |
105 | pick_node->get_filter_line_edit()->accept_event(); |
106 | } break; |
107 | default: |
108 | break; |
109 | } |
110 | } |
111 | } |
112 | |
113 | void ReplicationEditor::_pick_node_selected(NodePath p_path) { |
114 | Node *root = current->get_node(current->get_root_path()); |
115 | ERR_FAIL_COND(!root); |
116 | Node *node = get_node(p_path); |
117 | ERR_FAIL_COND(!node); |
118 | NodePath path_to = root->get_path_to(node); |
119 | adding_node_path = path_to; |
120 | prop_selector->select_property_from_instance(node); |
121 | } |
122 | |
123 | void ReplicationEditor::_pick_new_property() { |
124 | if (current == nullptr) { |
125 | EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it." )); |
126 | return; |
127 | } |
128 | Node *root = current->get_node(current->get_root_path()); |
129 | if (!root) { |
130 | EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root." )); |
131 | return; |
132 | } |
133 | pick_node->popup_scenetree_dialog(); |
134 | pick_node->get_filter_line_edit()->clear(); |
135 | pick_node->get_filter_line_edit()->grab_focus(); |
136 | } |
137 | |
138 | void ReplicationEditor::_add_sync_property(String p_path) { |
139 | config = current->get_replication_config(); |
140 | |
141 | if (config.is_valid() && config->has_property(p_path)) { |
142 | EditorNode::get_singleton()->show_warning(TTR("Property is already being synchronized." )); |
143 | return; |
144 | } |
145 | |
146 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
147 | undo_redo->create_action(TTR("Add property to synchronizer" )); |
148 | |
149 | if (config.is_null()) { |
150 | config.instantiate(); |
151 | current->set_replication_config(config); |
152 | undo_redo->add_do_method(current, "set_replication_config" , config); |
153 | undo_redo->add_undo_method(current, "set_replication_config" , Ref<SceneReplicationConfig>()); |
154 | _update_config(); |
155 | } |
156 | |
157 | undo_redo->add_do_method(config.ptr(), "add_property" , p_path); |
158 | undo_redo->add_undo_method(config.ptr(), "remove_property" , p_path); |
159 | undo_redo->add_do_method(this, "_update_config" ); |
160 | undo_redo->add_undo_method(this, "_update_config" ); |
161 | undo_redo->commit_action(); |
162 | } |
163 | |
164 | void ReplicationEditor::_pick_node_property_selected(String p_name) { |
165 | String adding_prop_path = String(adding_node_path) + ":" + p_name; |
166 | |
167 | _add_sync_property(adding_prop_path); |
168 | } |
169 | |
170 | /// ReplicationEditor |
171 | ReplicationEditor::ReplicationEditor() { |
172 | set_v_size_flags(SIZE_EXPAND_FILL); |
173 | set_custom_minimum_size(Size2(0, 200) * EDSCALE); |
174 | |
175 | delete_dialog = memnew(ConfirmationDialog); |
176 | delete_dialog->connect("canceled" , callable_mp(this, &ReplicationEditor::_dialog_closed).bind(false)); |
177 | delete_dialog->connect("confirmed" , callable_mp(this, &ReplicationEditor::_dialog_closed).bind(true)); |
178 | add_child(delete_dialog); |
179 | |
180 | VBoxContainer *vb = memnew(VBoxContainer); |
181 | vb->set_v_size_flags(SIZE_EXPAND_FILL); |
182 | add_child(vb); |
183 | |
184 | pick_node = memnew(SceneTreeDialog); |
185 | add_child(pick_node); |
186 | pick_node->register_text_enter(pick_node->get_filter_line_edit()); |
187 | pick_node->set_title(TTR("Pick a node to synchronize:" )); |
188 | pick_node->connect("selected" , callable_mp(this, &ReplicationEditor::_pick_node_selected)); |
189 | pick_node->get_filter_line_edit()->connect("text_changed" , callable_mp(this, &ReplicationEditor::_pick_node_filter_text_changed)); |
190 | pick_node->get_filter_line_edit()->connect("gui_input" , callable_mp(this, &ReplicationEditor::_pick_node_filter_input)); |
191 | |
192 | prop_selector = memnew(PropertySelector); |
193 | add_child(prop_selector); |
194 | // Filter out properties that cannot be synchronized. |
195 | // * RIDs do not match across network. |
196 | // * Objects are too large for replication. |
197 | Vector<Variant::Type> types = { |
198 | Variant::BOOL, |
199 | Variant::INT, |
200 | Variant::FLOAT, |
201 | Variant::STRING, |
202 | |
203 | Variant::VECTOR2, |
204 | Variant::VECTOR2I, |
205 | Variant::RECT2, |
206 | Variant::RECT2I, |
207 | Variant::VECTOR3, |
208 | Variant::VECTOR3I, |
209 | Variant::TRANSFORM2D, |
210 | Variant::VECTOR4, |
211 | Variant::VECTOR4I, |
212 | Variant::PLANE, |
213 | Variant::QUATERNION, |
214 | Variant::AABB, |
215 | Variant::BASIS, |
216 | Variant::TRANSFORM3D, |
217 | Variant::PROJECTION, |
218 | |
219 | Variant::COLOR, |
220 | Variant::STRING_NAME, |
221 | Variant::NODE_PATH, |
222 | // Variant::RID, |
223 | // Variant::OBJECT, |
224 | Variant::SIGNAL, |
225 | Variant::DICTIONARY, |
226 | Variant::ARRAY, |
227 | |
228 | Variant::PACKED_BYTE_ARRAY, |
229 | Variant::PACKED_INT32_ARRAY, |
230 | Variant::PACKED_INT64_ARRAY, |
231 | Variant::PACKED_FLOAT32_ARRAY, |
232 | Variant::PACKED_FLOAT64_ARRAY, |
233 | Variant::PACKED_STRING_ARRAY, |
234 | Variant::PACKED_VECTOR2_ARRAY, |
235 | Variant::PACKED_VECTOR3_ARRAY, |
236 | Variant::PACKED_COLOR_ARRAY |
237 | }; |
238 | prop_selector->set_type_filter(types); |
239 | prop_selector->connect("selected" , callable_mp(this, &ReplicationEditor::_pick_node_property_selected)); |
240 | |
241 | HBoxContainer *hb = memnew(HBoxContainer); |
242 | vb->add_child(hb); |
243 | |
244 | add_pick_button = memnew(Button); |
245 | add_pick_button->connect("pressed" , callable_mp(this, &ReplicationEditor::_pick_new_property)); |
246 | add_pick_button->set_text(TTR("Add property to sync..." )); |
247 | hb->add_child(add_pick_button); |
248 | VSeparator *vs = memnew(VSeparator); |
249 | vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); |
250 | hb->add_child(vs); |
251 | hb->add_child(memnew(Label(TTR("Path:" )))); |
252 | np_line_edit = memnew(LineEdit); |
253 | np_line_edit->set_placeholder(":property" ); |
254 | np_line_edit->set_h_size_flags(SIZE_EXPAND_FILL); |
255 | np_line_edit->connect("text_submitted" , callable_mp(this, &ReplicationEditor::_np_text_submitted)); |
256 | hb->add_child(np_line_edit); |
257 | add_from_path_button = memnew(Button); |
258 | add_from_path_button->connect("pressed" , callable_mp(this, &ReplicationEditor::_add_pressed)); |
259 | add_from_path_button->set_text(TTR("Add from path" )); |
260 | hb->add_child(add_from_path_button); |
261 | vs = memnew(VSeparator); |
262 | vs->set_custom_minimum_size(Size2(30 * EDSCALE, 0)); |
263 | hb->add_child(vs); |
264 | pin = memnew(Button); |
265 | pin->set_flat(true); |
266 | pin->set_toggle_mode(true); |
267 | hb->add_child(pin); |
268 | |
269 | tree = memnew(Tree); |
270 | tree->set_hide_root(true); |
271 | tree->set_columns(4); |
272 | tree->set_column_titles_visible(true); |
273 | tree->set_column_title(0, TTR("Properties" )); |
274 | tree->set_column_expand(0, true); |
275 | tree->set_column_title(1, TTR("Spawn" )); |
276 | tree->set_column_expand(1, false); |
277 | tree->set_column_custom_minimum_width(1, 100); |
278 | tree->set_column_title(2, TTR("Replicate" )); |
279 | tree->set_column_custom_minimum_width(2, 100); |
280 | tree->set_column_expand(2, false); |
281 | tree->set_column_expand(3, false); |
282 | tree->create_item(); |
283 | tree->connect("button_clicked" , callable_mp(this, &ReplicationEditor::_tree_button_pressed)); |
284 | tree->connect("item_edited" , callable_mp(this, &ReplicationEditor::_tree_item_edited)); |
285 | tree->set_v_size_flags(SIZE_EXPAND_FILL); |
286 | vb->add_child(tree); |
287 | |
288 | drop_label = memnew(Label); |
289 | drop_label->set_text(TTR("Add properties using the options above, or\ndrag them them from the inspector and drop them here." )); |
290 | drop_label->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER); |
291 | drop_label->set_vertical_alignment(VERTICAL_ALIGNMENT_CENTER); |
292 | tree->add_child(drop_label); |
293 | drop_label->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); |
294 | |
295 | SET_DRAG_FORWARDING_CDU(tree, ReplicationEditor); |
296 | } |
297 | |
298 | void ReplicationEditor::_bind_methods() { |
299 | ClassDB::bind_method(D_METHOD("_update_config" ), &ReplicationEditor::_update_config); |
300 | ClassDB::bind_method(D_METHOD("_update_value" , "property" , "column" , "value" ), &ReplicationEditor::_update_value); |
301 | } |
302 | |
303 | bool ReplicationEditor::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) const { |
304 | Dictionary d = p_data; |
305 | if (!d.has("type" )) { |
306 | return false; |
307 | } |
308 | String t = d["type" ]; |
309 | if (t != "obj_property" ) { |
310 | return false; |
311 | } |
312 | Object *obj = d["object" ]; |
313 | if (!obj) { |
314 | return false; |
315 | } |
316 | Node *node = Object::cast_to<Node>(obj); |
317 | if (!node) { |
318 | return false; |
319 | } |
320 | |
321 | return true; |
322 | } |
323 | |
324 | void ReplicationEditor::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from) { |
325 | if (current == nullptr) { |
326 | EditorNode::get_singleton()->show_warning(TTR("Select a replicator node in order to pick a property to add to it." )); |
327 | return; |
328 | } |
329 | Node *root = current->get_node(current->get_root_path()); |
330 | if (!root) { |
331 | EditorNode::get_singleton()->show_warning(TTR("Not possible to add a new property to synchronize without a root." )); |
332 | return; |
333 | } |
334 | |
335 | Dictionary d = p_data; |
336 | if (!d.has("type" )) { |
337 | return; |
338 | } |
339 | String t = d["type" ]; |
340 | if (t != "obj_property" ) { |
341 | return; |
342 | } |
343 | Object *obj = d["object" ]; |
344 | if (!obj) { |
345 | return; |
346 | } |
347 | Node *node = Object::cast_to<Node>(obj); |
348 | if (!node) { |
349 | return; |
350 | } |
351 | |
352 | String path = root->get_path_to(node); |
353 | path += ":" + String(d["property" ]); |
354 | |
355 | _add_sync_property(path); |
356 | } |
357 | |
358 | void ReplicationEditor::_notification(int p_what) { |
359 | switch (p_what) { |
360 | case NOTIFICATION_ENTER_TREE: |
361 | case EditorSettings::NOTIFICATION_EDITOR_SETTINGS_CHANGED: { |
362 | add_theme_style_override("panel" , EditorNode::get_singleton()->get_editor_theme()->get_stylebox(SNAME("panel" ), SNAME("Panel" ))); |
363 | add_pick_button->set_icon(get_theme_icon(SNAME("Add" ), EditorStringName(EditorIcons))); |
364 | pin->set_icon(get_theme_icon(SNAME("Pin" ), EditorStringName(EditorIcons))); |
365 | } break; |
366 | } |
367 | } |
368 | |
369 | void ReplicationEditor::_add_pressed() { |
370 | if (!current) { |
371 | EditorNode::get_singleton()->show_warning(TTR("Please select a MultiplayerSynchronizer first." )); |
372 | return; |
373 | } |
374 | if (current->get_root_path().is_empty()) { |
375 | EditorNode::get_singleton()->show_warning(TTR("The MultiplayerSynchronizer needs a root path." )); |
376 | return; |
377 | } |
378 | String np_text = np_line_edit->get_text(); |
379 | |
380 | if (np_text.is_empty()) { |
381 | EditorNode::get_singleton()->show_warning(TTR("Property/path must not be empty." )); |
382 | return; |
383 | } |
384 | |
385 | int idx = np_text.find(":" ); |
386 | if (idx == -1) { |
387 | np_text = ".:" + np_text; |
388 | } else if (idx == 0) { |
389 | np_text = "." + np_text; |
390 | } |
391 | NodePath path = NodePath(np_text); |
392 | if (path.is_empty()) { |
393 | EditorNode::get_singleton()->show_warning(vformat(TTR("Invalid property path: '%s'" ), np_text)); |
394 | return; |
395 | } |
396 | |
397 | _add_sync_property(path); |
398 | } |
399 | |
400 | void ReplicationEditor::_np_text_submitted(const String &p_newtext) { |
401 | _add_pressed(); |
402 | } |
403 | |
404 | void ReplicationEditor::_tree_item_edited() { |
405 | TreeItem *ti = tree->get_edited(); |
406 | if (!ti || config.is_null()) { |
407 | return; |
408 | } |
409 | int column = tree->get_edited_column(); |
410 | ERR_FAIL_COND(column < 1 || column > 2); |
411 | const NodePath prop = ti->get_metadata(0); |
412 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
413 | |
414 | if (column == 1) { |
415 | undo_redo->create_action(TTR("Set spawn property" )); |
416 | bool value = ti->is_checked(column); |
417 | undo_redo->add_do_method(config.ptr(), "property_set_spawn" , prop, value); |
418 | undo_redo->add_undo_method(config.ptr(), "property_set_spawn" , prop, !value); |
419 | undo_redo->add_do_method(this, "_update_value" , prop, column, value ? 1 : 0); |
420 | undo_redo->add_undo_method(this, "_update_value" , prop, column, value ? 1 : 0); |
421 | undo_redo->commit_action(); |
422 | } else if (column == 2) { |
423 | undo_redo->create_action(TTR("Set sync property" )); |
424 | int value = ti->get_range(column); |
425 | int old_value = config->property_get_replication_mode(prop); |
426 | // We have a hard limit of 64 watchable properties per synchronizer. |
427 | if (value == SceneReplicationConfig::REPLICATION_MODE_ON_CHANGE && config->get_watch_properties().size() >= 64) { |
428 | EditorNode::get_singleton()->show_warning(TTR("Each MultiplayerSynchronizer can have no more than 64 watched properties." )); |
429 | ti->set_range(column, old_value); |
430 | return; |
431 | } |
432 | undo_redo->add_do_method(config.ptr(), "property_set_replication_mode" , prop, value); |
433 | undo_redo->add_undo_method(config.ptr(), "property_set_replication_mode" , prop, old_value); |
434 | undo_redo->add_do_method(this, "_update_value" , prop, column, value); |
435 | undo_redo->add_undo_method(this, "_update_value" , prop, column, old_value); |
436 | undo_redo->commit_action(); |
437 | } else { |
438 | ERR_FAIL(); |
439 | } |
440 | } |
441 | |
442 | void ReplicationEditor::_tree_button_pressed(Object *p_item, int p_column, int p_id, MouseButton p_button) { |
443 | if (p_button != MouseButton::LEFT) { |
444 | return; |
445 | } |
446 | |
447 | TreeItem *ti = Object::cast_to<TreeItem>(p_item); |
448 | if (!ti) { |
449 | return; |
450 | } |
451 | deleting = ti->get_metadata(0); |
452 | delete_dialog->set_text(TTR("Delete Property?" ) + "\n\"" + ti->get_text(0) + "\"" ); |
453 | delete_dialog->popup_centered(); |
454 | } |
455 | |
456 | void ReplicationEditor::_dialog_closed(bool p_confirmed) { |
457 | if (deleting.is_empty() || config.is_null()) { |
458 | return; |
459 | } |
460 | if (p_confirmed) { |
461 | const NodePath prop = deleting; |
462 | int idx = config->property_get_index(prop); |
463 | bool spawn = config->property_get_spawn(prop); |
464 | SceneReplicationConfig::ReplicationMode mode = config->property_get_replication_mode(prop); |
465 | EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); |
466 | undo_redo->create_action(TTR("Remove Property" )); |
467 | undo_redo->add_do_method(config.ptr(), "remove_property" , prop); |
468 | undo_redo->add_undo_method(config.ptr(), "add_property" , prop, idx); |
469 | undo_redo->add_undo_method(config.ptr(), "property_set_spawn" , prop, spawn); |
470 | undo_redo->add_undo_method(config.ptr(), "property_set_replication_mode" , prop, mode); |
471 | undo_redo->add_do_method(this, "_update_config" ); |
472 | undo_redo->add_undo_method(this, "_update_config" ); |
473 | undo_redo->commit_action(); |
474 | } |
475 | deleting = NodePath(); |
476 | } |
477 | |
478 | void ReplicationEditor::_update_value(const NodePath &p_prop, int p_column, int p_value) { |
479 | if (!tree->get_root()) { |
480 | return; |
481 | } |
482 | TreeItem *ti = tree->get_root()->get_first_child(); |
483 | while (ti) { |
484 | if (ti->get_metadata(0).operator NodePath() == p_prop) { |
485 | if (p_column == 1) { |
486 | ti->set_checked(p_column, p_value != 0); |
487 | } else if (p_column == 2) { |
488 | ti->set_range(p_column, p_value); |
489 | } |
490 | return; |
491 | } |
492 | ti = ti->get_next(); |
493 | } |
494 | } |
495 | |
496 | void ReplicationEditor::_update_config() { |
497 | deleting = NodePath(); |
498 | tree->clear(); |
499 | tree->create_item(); |
500 | drop_label->set_visible(true); |
501 | if (!config.is_valid()) { |
502 | return; |
503 | } |
504 | TypedArray<NodePath> props = config->get_properties(); |
505 | if (props.size()) { |
506 | drop_label->set_visible(false); |
507 | } |
508 | for (int i = 0; i < props.size(); i++) { |
509 | const NodePath path = props[i]; |
510 | _add_property(path, config->property_get_spawn(path), config->property_get_replication_mode(path)); |
511 | } |
512 | } |
513 | |
514 | void ReplicationEditor::edit(MultiplayerSynchronizer *p_sync) { |
515 | if (current == p_sync) { |
516 | return; |
517 | } |
518 | current = p_sync; |
519 | if (current) { |
520 | config = current->get_replication_config(); |
521 | } else { |
522 | config.unref(); |
523 | } |
524 | _update_config(); |
525 | } |
526 | |
527 | Ref<Texture2D> ReplicationEditor::_get_class_icon(const Node *p_node) { |
528 | if (!p_node || !has_theme_icon(p_node->get_class(), EditorStringName(EditorIcons))) { |
529 | return get_theme_icon(SNAME("ImportFail" ), EditorStringName(EditorIcons)); |
530 | } |
531 | return get_theme_icon(p_node->get_class(), EditorStringName(EditorIcons)); |
532 | } |
533 | |
534 | static bool can_sync(const Variant &p_var) { |
535 | switch (p_var.get_type()) { |
536 | case Variant::RID: |
537 | case Variant::OBJECT: |
538 | return false; |
539 | case Variant::ARRAY: { |
540 | const Array &arr = p_var; |
541 | if (arr.is_typed()) { |
542 | const uint32_t type = arr.get_typed_builtin(); |
543 | return (type != Variant::RID) && (type != Variant::OBJECT); |
544 | } |
545 | return true; |
546 | } |
547 | default: |
548 | return true; |
549 | } |
550 | } |
551 | |
552 | void ReplicationEditor::_add_property(const NodePath &p_property, bool p_spawn, SceneReplicationConfig::ReplicationMode p_mode) { |
553 | String prop = String(p_property); |
554 | TreeItem *item = tree->create_item(); |
555 | item->set_selectable(0, false); |
556 | item->set_selectable(1, false); |
557 | item->set_selectable(2, false); |
558 | item->set_selectable(3, false); |
559 | item->set_text(0, prop); |
560 | item->set_metadata(0, prop); |
561 | Node *root_node = current && !current->get_root_path().is_empty() ? current->get_node(current->get_root_path()) : nullptr; |
562 | Ref<Texture2D> icon = _get_class_icon(root_node); |
563 | if (root_node) { |
564 | String path = prop.substr(0, prop.find(":" )); |
565 | String subpath = prop.substr(path.size()); |
566 | Node *node = root_node->get_node_or_null(path); |
567 | if (!node) { |
568 | node = root_node; |
569 | } |
570 | item->set_text(0, String(node->get_name()) + ":" + subpath); |
571 | icon = _get_class_icon(node); |
572 | bool valid = false; |
573 | Variant value = node->get(subpath, &valid); |
574 | if (valid && !can_sync(value)) { |
575 | item->set_icon(0, get_theme_icon(SNAME("StatusWarning" ), EditorStringName(EditorIcons))); |
576 | item->set_tooltip_text(0, TTR("Property of this type not supported." )); |
577 | } else { |
578 | item->set_icon(0, icon); |
579 | } |
580 | } else { |
581 | item->set_icon(0, icon); |
582 | } |
583 | item->add_button(3, get_theme_icon(SNAME("Remove" ), EditorStringName(EditorIcons))); |
584 | item->set_text_alignment(1, HORIZONTAL_ALIGNMENT_CENTER); |
585 | item->set_cell_mode(1, TreeItem::CELL_MODE_CHECK); |
586 | item->set_checked(1, p_spawn); |
587 | item->set_editable(1, true); |
588 | item->set_text_alignment(2, HORIZONTAL_ALIGNMENT_CENTER); |
589 | item->set_cell_mode(2, TreeItem::CELL_MODE_RANGE); |
590 | item->set_range_config(2, 0, 2, 1); |
591 | item->set_text(2, "Never,Always,On Change" ); |
592 | item->set_range(2, (int)p_mode); |
593 | item->set_editable(2, true); |
594 | } |
595 | |