diff --git a/core/string_name.h b/core/string_name.h index 3b4d6ad8af1..f93a121116f 100644 --- a/core/string_name.h +++ b/core/string_name.h @@ -35,6 +35,8 @@ #include "core/safe_refcount.h" #include "core/ustring.h" +#define UNIQUE_NODE_PREFIX "%" + struct StaticCString { const char *ptr; static StaticCString create(const char *p_ptr); @@ -92,6 +94,17 @@ public: bool operator==(const String &p_name) const; bool operator==(const char *p_name) const; bool operator!=(const String &p_name) const; + + _FORCE_INLINE_ bool is_node_unique_name() const { + if (!_data) { + return false; + } + if (_data->cname != nullptr) { + return (char32_t)_data->cname[0] == (char32_t)UNIQUE_NODE_PREFIX[0]; + } else { + return (char32_t)_data->name[0] == (char32_t)UNIQUE_NODE_PREFIX[0]; + } + } _FORCE_INLINE_ bool operator<(const StringName &p_name) const { return _data < p_name._data; } diff --git a/core/ustring.cpp b/core/ustring.cpp index db82dec9dce..62d039bd619 100644 --- a/core/ustring.cpp +++ b/core/ustring.cpp @@ -39,6 +39,7 @@ #include "core/math/math_funcs.h" #include "core/os/memory.h" #include "core/print_string.h" +#include "core/string_name.h" #include "core/translation.h" #include "core/ucaps.h" #include "core/variant.h" @@ -4140,7 +4141,8 @@ String String::property_name_encode() const { } // Changes made to the set of invalid characters must also be reflected in the String documentation. -const String String::invalid_node_name_characters = ". : @ / \""; +const String String::invalid_node_name_characters = ". : @ / \" " UNIQUE_NODE_PREFIX; +; String String::validate_node_name() const { Vector chars = String::invalid_node_name_characters.split(" "); diff --git a/doc/classes/Node.xml b/doc/classes/Node.xml index 9b9ec3cf3a0..aa16679af87 100644 --- a/doc/classes/Node.xml +++ b/doc/classes/Node.xml @@ -758,6 +758,10 @@ The node's priority in the execution order of the enabled processing callbacks (i.e. [constant NOTIFICATION_PROCESS], [constant NOTIFICATION_PHYSICS_PROCESS] and their internal counterparts). Nodes whose process priority value is [i]lower[/i] will have their processing callbacks executed first. + + Sets this node's name as a unique name in its [member owner]. This allows the node to be accessed as [code]%Name[/code] instead of the full path, from any node within that scene. + If another node with the same owner already had that name declared as unique, that other node's name will no longer be set as having a unique name. + diff --git a/editor/icons/icon_scene_unique_name.svg b/editor/icons/icon_scene_unique_name.svg new file mode 100644 index 00000000000..34279a14a69 --- /dev/null +++ b/editor/icons/icon_scene_unique_name.svg @@ -0,0 +1 @@ + diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp index 76872bccb39..000392c4763 100644 --- a/editor/scene_tree_dock.cpp +++ b/editor/scene_tree_dock.cpp @@ -1122,6 +1122,28 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) { } } } break; + case TOOL_TOGGLE_SCENE_UNIQUE_NAME: { + List selection = editor_selection->get_selected_node_list(); + List::Element *e = selection.front(); + if (e) { + UndoRedo *undo_redo = &editor_data->get_undo_redo(); + Node *node = e->get(); + bool enabled = node->is_unique_name_in_owner(); + if (!enabled && get_tree()->get_edited_scene_root()->get_node_or_null(UNIQUE_NODE_PREFIX + String(node->get_name())) != nullptr) { + accept->set_text(TTR("Another node already uses this unique name in the scene.")); + accept->popup_centered(); + return; + } + if (!enabled) { + undo_redo->create_action(TTR("Enable Scene Unique Name")); + } else { + undo_redo->create_action(TTR("Disable Scene Unique Name")); + } + undo_redo->add_do_method(node, "set_unique_name_in_owner", !enabled); + undo_redo->add_undo_method(node, "set_unique_name_in_owner", enabled); + undo_redo->commit_action(); + } + } break; case TOOL_CREATE_2D_SCENE: case TOOL_CREATE_3D_SCENE: case TOOL_CREATE_USER_INTERFACE: @@ -1353,8 +1375,17 @@ void SceneTreeDock::_node_replace_owner(Node *p_base, Node *p_node, Node *p_root UndoRedo *undo_redo = &editor_data->get_undo_redo(); switch (p_mode) { case MODE_BIDI: { + bool is_unique = p_node->is_unique_name_in_owner() && p_base->get_node_or_null(UNIQUE_NODE_PREFIX + String(p_node->get_name())) != nullptr; + if (is_unique) { + // Will create a unique name conflict. Disable before setting owner. + undo_redo->add_do_method(p_node, "set_unique_name_in_owner", false); + } undo_redo->add_do_method(p_node, "set_owner", p_root); undo_redo->add_undo_method(p_node, "set_owner", p_base); + if (is_unique) { + // Will create a unique name conflict. Enable after setting owner. + undo_redo->add_undo_method(p_node, "set_unique_name_in_owner", true); + } } break; case MODE_DO: { @@ -2753,6 +2784,12 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) { menu->add_icon_shortcut(get_icon("ScriptExtend", "EditorIcons"), ED_GET_SHORTCUT("scene_tree/extend_script"), TOOL_EXTEND_SCRIPT); } } + if (selection[0]->get_owner() == EditorNode::get_singleton()->get_edited_scene()) { + // Only for nodes owned by the edited scene root. + menu->add_separator(); + menu->add_icon_check_item(get_icon("SceneUniqueName", "EditorIcons"), TTR("Access as Scene Unique Name"), TOOL_TOGGLE_SCENE_UNIQUE_NAME); + menu->set_item_checked(menu->get_item_index(TOOL_TOGGLE_SCENE_UNIQUE_NAME), selection[0]->is_unique_name_in_owner()); + } if (existing_script.is_valid() && exisiting_script_removable) { add_separator = true; menu->add_icon_shortcut(get_icon("ScriptRemove", "EditorIcons"), ED_GET_SHORTCUT("scene_tree/detach_script"), TOOL_DETACH_SCRIPT); diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h index dc08f2d7b0b..b81f6be30f1 100644 --- a/editor/scene_tree_dock.h +++ b/editor/scene_tree_dock.h @@ -92,6 +92,7 @@ class SceneTreeDock : public VBoxContainer { TOOL_SCENE_CLEAR_INHERITANCE, TOOL_SCENE_CLEAR_INHERITANCE_CONFIRM, TOOL_SCENE_OPEN_INHERITED, + TOOL_TOGGLE_SCENE_UNIQUE_NAME, TOOL_CREATE_2D_SCENE, TOOL_CREATE_3D_SCENE, diff --git a/editor/scene_tree_editor.cpp b/editor/scene_tree_editor.cpp index f9812263b8f..20ea15feba7 100644 --- a/editor/scene_tree_editor.cpp +++ b/editor/scene_tree_editor.cpp @@ -145,6 +145,13 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i NodeDock::singleton->get_parent()->call("set_current_tab", NodeDock::singleton->get_index()); NodeDock::singleton->show_groups(); + } else if (p_id == BUTTON_UNIQUE) { + undo_redo->create_action(TTR("Disable Scene Unique Name")); + undo_redo->add_do_method(n, "set_unique_name_in_owner", false); + undo_redo->add_undo_method(n, "set_unique_name_in_owner", true); + undo_redo->add_do_method(this, "_update_tree"); + undo_redo->add_undo_method(this, "_update_tree"); + undo_redo->commit_action(); } } void SceneTreeEditor::_toggle_visible(Node *p_node) { @@ -263,6 +270,10 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent, bool p_scroll item->add_button(0, get_icon("NodeWarning", "EditorIcons"), BUTTON_WARNING, false, TTR("Node configuration warning:") + "\n" + p_node->get_configuration_warning()); } + if (p_node->is_unique_name_in_owner()) { + item->add_button(0, get_icon("SceneUniqueName", "EditorIcons"), BUTTON_UNIQUE, false, vformat(TTR("This node can be accessed from within anywhere in the scene by preceding it with the '%s' prefix in a node path.\nClick to disable this."), UNIQUE_NODE_PREFIX)); + } + int num_connections = p_node->get_persistent_signal_connection_count(); int num_groups = p_node->get_persistent_group_count(); @@ -823,6 +834,13 @@ void SceneTreeEditor::_renamed() { // Trim leading/trailing whitespace to prevent node names from containing accidental whitespace, which would make it more difficult to get the node via `get_node()`. new_name = new_name.strip_edges(); + if (n->is_unique_name_in_owner() && get_tree()->get_edited_scene_root()->get_node_or_null("%" + new_name) != nullptr) { + error->set_text(TTR("Another node already uses this unique name in the scene.")); + error->popup_centered(); + which->set_text(0, n->get_name()); + return; + } + if (!undo_redo) { n->set_name(new_name); which->set_metadata(0, n->get_path()); diff --git a/editor/scene_tree_editor.h b/editor/scene_tree_editor.h index 03925326a7d..671ae1eb3b5 100644 --- a/editor/scene_tree_editor.h +++ b/editor/scene_tree_editor.h @@ -53,6 +53,7 @@ class SceneTreeEditor : public Control { BUTTON_SIGNALS = 6, BUTTON_GROUPS = 7, BUTTON_PIN = 8, + BUTTON_UNIQUE = 9, }; Tree *tree; diff --git a/scene/main/node.cpp b/scene/main/node.cpp index 2e0a261ed72..c7fc746f676 100644 --- a/scene/main/node.cpp +++ b/scene/main/node.cpp @@ -269,6 +269,9 @@ void Node::_propagate_after_exit_branch(bool p_exiting_tree) { } if (!found) { + if (data.unique_name_in_owner) { + _release_unique_name_in_owner(); + } data.owner->data.owned.erase(data.OW); data.owner = nullptr; } @@ -979,12 +982,20 @@ void Node::set_name(const String &p_name) { String name = p_name.validate_node_name(); ERR_FAIL_COND(name == ""); + + if (data.unique_name_in_owner && data.owner) { + _release_unique_name_in_owner(); + } data.name = name; if (data.parent) { data.parent->_validate_child_name(this); } + if (data.unique_name_in_owner && data.owner) { + _acquire_unique_name_in_owner(); + } + propagate_notification(NOTIFICATION_PATH_CHANGED); if (is_inside_tree()) { @@ -1332,6 +1343,24 @@ Node *Node::get_node_or_null(const NodePath &p_path) const { next = root; } + } else if (name.is_node_unique_name()) { + if (current->data.owned_unique_nodes.size()) { + // Has unique nodes in ownership + Node **unique = current->data.owned_unique_nodes.getptr(name); + if (!unique) { + return nullptr; + } + next = *unique; + } else if (current->data.owner) { + Node **unique = current->data.owner->data.owned_unique_nodes.getptr(name); + if (!unique) { + return nullptr; + } + next = *unique; + } else { + return nullptr; + } + } else { next = nullptr; @@ -1508,8 +1537,54 @@ void Node::_set_owner_nocheck(Node *p_owner) { data.OW = data.owner->data.owned.back(); } +void Node::_release_unique_name_in_owner() { + ERR_FAIL_NULL(data.owner); // Sanity check. + StringName key = StringName(UNIQUE_NODE_PREFIX + data.name.operator String()); + Node **which = data.owner->data.owned_unique_nodes.getptr(key); + if (which == nullptr || *which != this) { + return; // Ignore. + } + data.owner->data.owned_unique_nodes.erase(key); +} + +void Node::_acquire_unique_name_in_owner() { + ERR_FAIL_NULL(data.owner); // Sanity check. + StringName key = StringName(UNIQUE_NODE_PREFIX + data.name.operator String()); + Node **which = data.owner->data.owned_unique_nodes.getptr(key); + if (which != nullptr && *which != this) { + WARN_PRINT(vformat(RTR("Setting node name '%s' to be unique within scene for '%s', but it's already claimed by '%s'. This node is no longer set unique."), get_name(), is_inside_tree() ? get_path() : data.owner->get_path_to(this), is_inside_tree() ? (*which)->get_path() : data.owner->get_path_to(*which))); + data.unique_name_in_owner = false; + return; + } + data.owner->data.owned_unique_nodes[key] = this; +} + +void Node::set_unique_name_in_owner(bool p_enabled) { + if (data.unique_name_in_owner == p_enabled) { + return; + } + + if (data.unique_name_in_owner && data.owner != nullptr) { + _release_unique_name_in_owner(); + } + data.unique_name_in_owner = p_enabled; + + if (data.unique_name_in_owner && data.owner != nullptr) { + _acquire_unique_name_in_owner(); + } + + update_configuration_warning(); +} + +bool Node::is_unique_name_in_owner() const { + return data.unique_name_in_owner; +} + void Node::set_owner(Node *p_owner) { if (data.owner) { + if (data.unique_name_in_owner) { + _release_unique_name_in_owner(); + } data.owner->data.owned.erase(data.OW); data.OW = nullptr; data.owner = nullptr; @@ -1536,6 +1611,10 @@ void Node::set_owner(Node *p_owner) { ERR_FAIL_COND(!owner_valid); _set_owner_nocheck(p_owner); + + if (data.unique_name_in_owner) { + _acquire_unique_name_in_owner(); + } } Node *Node::get_owner() const { return data.owner; @@ -2918,6 +2997,8 @@ void Node::_bind_methods() { #ifdef TOOLS_ENABLED ClassDB::bind_method(D_METHOD("_set_property_pinned", "property", "pinned"), &Node::set_property_pinned); #endif + ClassDB::bind_method(D_METHOD("set_unique_name_in_owner", "enable"), &Node::set_unique_name_in_owner); + ClassDB::bind_method(D_METHOD("is_unique_name_in_owner"), &Node::is_unique_name_in_owner); { MethodInfo mi; @@ -3003,6 +3084,7 @@ void Node::_bind_methods() { #endif ADD_PROPERTY(PropertyInfo(Variant::STRING, "name", PROPERTY_HINT_NONE, "", 0), "set_name", "get_name"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "unique_name_in_owner", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "set_unique_name_in_owner", "is_unique_name_in_owner"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "filename", PROPERTY_HINT_NONE, "", 0), "set_filename", "get_filename"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "owner", PROPERTY_HINT_RESOURCE_TYPE, "Node", 0), "set_owner", "get_owner"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "multiplayer", PROPERTY_HINT_RESOURCE_TYPE, "MultiplayerAPI", 0), "", "get_multiplayer"); diff --git a/scene/main/node.h b/scene/main/node.h index 3faef603189..98c4d728455 100644 --- a/scene/main/node.h +++ b/scene/main/node.h @@ -97,6 +97,9 @@ private: Node *parent; Node *owner; Vector children; // list of children + HashMap owned_unique_nodes; + bool unique_name_in_owner = false; + int pos; int depth; int blocked; // safeguard that throws an error when attempting to modify the tree in a harmful way while being traversed. @@ -203,6 +206,8 @@ private: friend class SceneTree; void _set_tree(SceneTree *p_tree); + void _release_unique_name_in_owner(); + void _acquire_unique_name_in_owner(); protected: void _block() { data.blocked++; } @@ -326,6 +331,9 @@ public: Node *get_owner() const; void get_owned_by(Node *p_by, List *p_owned); + void set_unique_name_in_owner(bool p_enabled); + bool is_unique_name_in_owner() const; + void remove_and_skip(); int get_index() const; diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp index c86a78698d7..1f780b24526 100644 --- a/scene/resources/packed_scene.cpp +++ b/scene/resources/packed_scene.cpp @@ -309,6 +309,9 @@ Node *SceneState::instance(GenEditState p_edit_state) const { NODE_FROM_ID(owner, n.owner); if (owner) { node->_set_owner_nocheck(owner); + if (node->data.unique_name_in_owner) { + node->_acquire_unique_name_in_owner(); + } } }