From 5418e0fa0f3d8158c77e12d0cbe721f9ddbe39dc Mon Sep 17 00:00:00 2001
From: yahkr <62478788+yahkr@users.noreply.github.com>
Date: Sun, 27 Oct 2024 12:42:28 -0400
Subject: [PATCH] Add the ability to expose nodes for direct access in
instantiated scenes
---
doc/classes/EditorSettings.xml | 3 +
editor/editor_data.cpp | 12 ++-
editor/editor_settings.cpp | 1 +
editor/gui/scene_tree_editor.cpp | 81 +++++++++++++-
editor/gui/scene_tree_editor.h | 7 ++
editor/icons/SceneExposedNode.svg | 1 +
editor/icons/SceneExposedNodeInstanced.svg | 1 +
editor/import/3d/resource_importer_scene.cpp | 10 +-
editor/scene_tree_dock.cpp | 75 ++++++++++++-
editor/scene_tree_dock.h | 2 +
scene/main/node.cpp | 15 +++
scene/main/node.h | 9 ++
scene/resources/packed_scene.cpp | 107 ++++++++++++++++++-
scene/resources/packed_scene.h | 3 +
scene/resources/resource_format_text.cpp | 30 ++++++
15 files changed, 346 insertions(+), 11 deletions(-)
create mode 100644 editor/icons/SceneExposedNode.svg
create mode 100644 editor/icons/SceneExposedNodeInstanced.svg
diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index e1c85d3595f5..c94e0a995541 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -232,6 +232,9 @@
If [code]true[/code], when a node is deleted with animation tracks referencing it, a confirmation dialog appears before the tracks are deleted. The dialog will appear even when using the "Delete (No Confirm)" shortcut.
+
+ If [code]true[/code], displays a confirmation dialog after left-clicking the "exposed" icon next to a node name in the Scene tree dock. When clicked, this icon revokes the node's scene exposure, which can impact the behavior of scripts that rely on this exposed node due to identifiers not being found anymore.
+
If [code]true[/code], displays a confirmation dialog after left-clicking the "percent" icon next to a node name in the Scene tree dock. When clicked, this icon revokes the node's scene-unique name, which can impact the behavior of scripts that rely on this scene-unique name due to identifiers not being found anymore.
diff --git a/editor/editor_data.cpp b/editor/editor_data.cpp
index 73af9f39a6f5..c8ccab837fa4 100644
--- a/editor/editor_data.cpp
+++ b/editor/editor_data.cpp
@@ -724,9 +724,17 @@ bool EditorData::check_and_update_scene(int p_idx) {
for (const Node *E : edited_scene.write[p_idx].selection) {
NodePath p = edited_scene[p_idx].root->get_path_to(E);
Node *new_node = new_scene->get_node(p);
- if (new_node) {
- new_selection.push_back(new_node);
+ // Node can't be found, skip it.
+ if (!new_node) {
+ continue;
}
+
+ // Node is no longer exposed, skip it.
+ if (E->has_meta(META_EXPOSED_IN_OWNER) && !new_node->has_meta(META_EXPOSED_IN_OWNER)) {
+ continue;
+ }
+
+ new_selection.push_back(new_node);
}
new_scene->set_scene_file_path(edited_scene[p_idx].root->get_scene_file_path());
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index 78dd3919147d..5602e8e7583a 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -509,6 +509,7 @@ void EditorSettings::_load_defaults(Ref p_extra_config) {
_initial_set("interface/editors/show_scene_tree_root_selection", true);
_initial_set("interface/editors/derive_script_globals_by_name", true);
_initial_set("docks/scene_tree/ask_before_revoking_unique_name", true);
+ _initial_set("docks/scene_tree/ask_before_revoking_node_exposure", true);
// Inspector
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_RANGE, "interface/inspector/max_array_dictionary_items_per_page", 20, "10,100,1")
diff --git a/editor/gui/scene_tree_editor.cpp b/editor/gui/scene_tree_editor.cpp
index fbe679cd15c8..d4e6ab73e122 100644
--- a/editor/gui/scene_tree_editor.cpp
+++ b/editor/gui/scene_tree_editor.cpp
@@ -208,6 +208,18 @@ void SceneTreeEditor::_cell_button_pressed(Object *p_item, int p_column, int p_i
} else {
_revoke_unique_name();
}
+ } else if (p_id == BUTTON_EXPOSED) {
+ bool ask_before_revoking_node_exposure = EDITOR_GET("docks/scene_tree/ask_before_revoking_node_exposure");
+ revoke_node = n;
+ if (ask_before_revoking_node_exposure) {
+ String msg = vformat(TTR("Unexpose node \"%s\"?"), n->get_name());
+ ask_before_revoke_node_exposure_checkbox->set_pressed(false);
+ revoke_node_exposure_dialog_label->set_text(msg);
+ revoke_node_exposure->reset_size();
+ revoke_node_exposure->popup_centered();
+ } else {
+ _toggle_node_exposure();
+ }
}
}
@@ -231,6 +243,39 @@ void SceneTreeEditor::_revoke_unique_name() {
undo_redo->commit_action();
}
+void SceneTreeEditor::_update_ask_before_revoking_node_exposure() {
+ if (ask_before_revoke_node_exposure_checkbox->is_pressed()) {
+ EditorSettings::get_singleton()->set("docks/scene_tree/ask_before_revoking_node_exposure", false);
+ ask_before_revoke_node_exposure_checkbox->set_pressed(false);
+ }
+ _toggle_node_exposure();
+}
+
+void SceneTreeEditor::_toggle_node_exposure() {
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ bool enabled = revoke_node->has_meta(META_MARKED_FOR_EXPOSURE);
+ undo_redo->create_action(enabled ? TTR("Unexpose Node In Scene") : TTR("Expose Node In Scene"));
+ if (revoke_node->get_owner() == get_tree()->get_edited_scene_root()) {
+ if (enabled) {
+ undo_redo->add_do_method(revoke_node, "remove_meta", META_EXPOSED_IN_OWNER);
+ undo_redo->add_undo_method(revoke_node, "set_meta", META_EXPOSED_IN_OWNER, true);
+ } else {
+ undo_redo->add_do_method(revoke_node, "set_meta", META_EXPOSED_IN_OWNER, true);
+ undo_redo->add_undo_method(revoke_node, "remove_meta", META_EXPOSED_IN_OWNER);
+ }
+ }
+ if (enabled) {
+ undo_redo->add_do_method(revoke_node, "remove_meta", META_MARKED_FOR_EXPOSURE);
+ undo_redo->add_undo_method(revoke_node, "set_meta", META_MARKED_FOR_EXPOSURE, true);
+ } else {
+ undo_redo->add_do_method(revoke_node, "set_meta", META_MARKED_FOR_EXPOSURE, true);
+ undo_redo->add_undo_method(revoke_node, "remove_meta", META_MARKED_FOR_EXPOSURE);
+ }
+ 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) {
if (p_node->has_method("is_visible") && p_node->has_method("set_visible")) {
bool v = bool(p_node->call("is_visible"));
@@ -274,8 +319,16 @@ void SceneTreeEditor::_update_node_subtree(Node *p_node, TreeItem *p_parent, boo
bool part_of_subscene = false;
if (!display_foreign && p_node->get_owner() != get_scene_node() && p_node != get_scene_node()) {
- if ((show_enabled_subscene || can_open_instance) && p_node->get_owner() && (get_scene_node()->is_editable_instance(p_node->get_owner()))) {
+ if ((show_enabled_subscene || can_open_instance) && p_node->get_owner() && (get_scene_node()->is_editable_instance(p_node->get_owner()) || (p_node->has_meta(META_EXPOSED_IN_INSTANCE) && p_node->has_meta(META_EXPOSED_IN_OWNER)))) {
part_of_subscene = true;
+ //allow
+ } else if (p_node->has_exposed_nodes()) {
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ if (!get_scene_node()->is_editable_instance(p_node->get_child(i)->get_owner()) || p_node->get_child(i)->has_meta(META_EXPOSED_IN_INSTANCE)) {
+ _update_node_subtree(p_node->get_child(i), p_parent);
+ }
+ }
+ return;
// Allow.
} else {
// Stale node, remove recursively.
@@ -319,6 +372,10 @@ void SceneTreeEditor::_update_node_subtree(Node *p_node, TreeItem *p_parent, boo
}
} else {
index = p_node->get_index(false);
+ // Shift the exposed nodes based on how many siblings already exist to maintain exposed node order.
+ if (!p_node->get_owner()->is_editable_instance(p_node) && p_node->has_meta(META_EXPOSED_IN_OWNER)) {
+ index = p_parent->get_child_count() + index;
+ }
item = tree->create_item(p_parent, index);
}
@@ -498,6 +555,16 @@ void SceneTreeEditor::_update_node(Node *p_node, TreeItem *p_item, bool p_part_o
p_item->add_button(0, get_editor_theme_icon(warning_icon), BUTTON_WARNING, false, TTR("Node configuration warning:") + all_warnings);
}
+ if (p_node->has_meta(META_EXPOSED_IN_OWNER) && p_node->get_owner() == EditorNode::get_singleton()->get_edited_scene()) {
+ p_item->add_button(0, get_editor_theme_icon(SNAME("SceneExposedNode")), BUTTON_EXPOSED, false, TTR("This node will be exposed in the editor when this scene is instantiated.") + "\n" + TTR("Click to disable this."));
+ } else {
+ if (p_node->has_meta(META_MARKED_FOR_EXPOSURE)) {
+ p_item->add_button(0, get_editor_theme_icon(SNAME("SceneExposedNode")), BUTTON_EXPOSED, false, TTR("This node has been exposed in the underlying scene.") + "\n" + TTR("This node will be exposed in the editor when this scene is instantiated.") + "\n" + TTR("Click to disable this."));
+ } else if (p_node->has_meta(META_EXPOSED_IN_INSTANCE)) {
+ p_item->add_button(0, get_editor_theme_icon(SNAME("SceneExposedNodeInstanced")), BUTTON_EXPOSED, p_node->get_owner() != EditorNode::get_singleton()->get_edited_scene(), TTR("This node has been exposed in the underlying scene."));
+ }
+ }
+
if (p_node->is_unique_name_in_owner()) {
const bool disabled = p_node->get_owner() != EditorNode::get_singleton()->get_edited_scene();
String button_text = vformat(TTR("This node can be accessed from anywhere within the scene it belongs to by using the '%s' prefix in the node path."), UNIQUE_NODE_PREFIX);
@@ -2003,6 +2070,18 @@ SceneTreeEditor::SceneTreeEditor(bool p_label, bool p_can_rename, bool p_can_ope
ask_before_revoke_checkbox->set_tooltip_text(TTR("This dialog can also be enabled/disabled in the Editor Settings: Docks > Scene Tree > Ask Before Revoking Unique Name."));
vb->add_child(ask_before_revoke_checkbox);
+ revoke_node_exposure = memnew(ConfirmationDialog);
+ revoke_node_exposure->set_ok_button_text(TTR("Revoke"));
+ add_child(revoke_node_exposure);
+ revoke_node_exposure->connect(SceneStringName(confirmed), callable_mp(this, &SceneTreeEditor::_update_ask_before_revoking_node_exposure));
+ VBoxContainer *nevb = memnew(VBoxContainer);
+ revoke_node_exposure->add_child(nevb);
+ revoke_node_exposure_dialog_label = memnew(Label);
+ nevb->add_child(revoke_node_exposure_dialog_label);
+ ask_before_revoke_node_exposure_checkbox = memnew(CheckBox(TTR("Don't Ask Again")));
+ ask_before_revoke_node_exposure_checkbox->set_tooltip_text(TTR("This dialog can also be enabled/disabled in the Editor Settings: Docks > Scene Tree > Ask Before Revoking Node Exposure."));
+ nevb->add_child(ask_before_revoke_node_exposure_checkbox);
+
script_types = memnew(List);
ClassDB::get_inheriters_from_class("Script", script_types);
}
diff --git a/editor/gui/scene_tree_editor.h b/editor/gui/scene_tree_editor.h
index 79000d537d17..a4bc45b8387c 100644
--- a/editor/gui/scene_tree_editor.h
+++ b/editor/gui/scene_tree_editor.h
@@ -56,6 +56,7 @@ class SceneTreeEditor : public Control {
BUTTON_GROUPS = 7,
BUTTON_PIN = 8,
BUTTON_UNIQUE = 9,
+ BUTTON_EXPOSED = 10,
};
struct CachedNode {
@@ -125,6 +126,10 @@ class SceneTreeEditor : public Control {
CheckBox *ask_before_revoke_checkbox = nullptr;
Node *revoke_node = nullptr;
+ ConfirmationDialog *revoke_node_exposure = nullptr;
+ Label *revoke_node_exposure_dialog_label = nullptr;
+ CheckBox *ask_before_revoke_node_exposure_checkbox = nullptr;
+
bool auto_expand_selected = true;
bool connect_to_script_mode = false;
bool connecting_signal = false;
@@ -218,6 +223,8 @@ class SceneTreeEditor : public Control {
void _update_ask_before_revoking_unique_name();
void _revoke_unique_name();
+ void _update_ask_before_revoking_node_exposure();
+ void _toggle_node_exposure();
public:
// Public for use with callable_mp.
diff --git a/editor/icons/SceneExposedNode.svg b/editor/icons/SceneExposedNode.svg
new file mode 100644
index 000000000000..f7ba92d1f2e7
--- /dev/null
+++ b/editor/icons/SceneExposedNode.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/icons/SceneExposedNodeInstanced.svg b/editor/icons/SceneExposedNodeInstanced.svg
new file mode 100644
index 000000000000..e22dadd4aa25
--- /dev/null
+++ b/editor/icons/SceneExposedNodeInstanced.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/editor/import/3d/resource_importer_scene.cpp b/editor/import/3d/resource_importer_scene.cpp
index b85857b4f4e8..099a4a766f1b 100644
--- a/editor/import/3d/resource_importer_scene.cpp
+++ b/editor/import/3d/resource_importer_scene.cpp
@@ -1409,6 +1409,10 @@ Node *ResourceImporterScene::_post_fix_node(Node *p_node, Node *p_root, HashMap<
}
}
+ if (!isroot && node_settings.has("import/exposed")) {
+ p_node->set_meta(META_EXPOSED_IN_OWNER, bool(node_settings["import/exposed"]));
+ }
+
if (Object::cast_to(p_node)) {
ObjectID node_id = p_node->get_instance_id();
for (int i = 0; i < post_importer_plugins.size(); i++) {
@@ -1988,9 +1992,11 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p
switch (p_category) {
case INTERNAL_IMPORT_CATEGORY_NODE: {
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
+ r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/exposed", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
} break;
case INTERNAL_IMPORT_CATEGORY_MESH_3D_NODE: {
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
+ r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/exposed", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "generate/physics", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "generate/navmesh", PROPERTY_HINT_ENUM, "Disabled,Mesh + NavMesh,NavMesh Only"), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "physics/body_type", PROPERTY_HINT_ENUM, "Static,Dynamic,Area"), 0));
@@ -2067,6 +2073,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p
} break;
case INTERNAL_IMPORT_CATEGORY_ANIMATION_NODE: {
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
+ r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/exposed", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "optimizer/enabled", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), true));
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "optimizer/max_velocity_error", PROPERTY_HINT_RANGE, "0,1,0.01"), 0.01));
r_options->push_back(ImportOption(PropertyInfo(Variant::FLOAT, "optimizer/max_angular_error", PROPERTY_HINT_RANGE, "0,1,0.01"), 0.01));
@@ -2079,6 +2086,7 @@ void ResourceImporterScene::get_internal_import_options(InternalImportCategory p
} break;
case INTERNAL_IMPORT_CATEGORY_SKELETON_3D_NODE: {
r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/skip_import", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
+ r_options->push_back(ImportOption(PropertyInfo(Variant::BOOL, "import/exposed", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), false));
r_options->push_back(ImportOption(PropertyInfo(Variant::INT, "rest_pose/load_pose", PROPERTY_HINT_ENUM, "Default Pose,Use AnimationPlayer,Load External Animation", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), 0));
r_options->push_back(ImportOption(PropertyInfo(Variant::OBJECT, "rest_pose/external_animation_library", PROPERTY_HINT_RESOURCE_TYPE, "Animation,AnimationLibrary", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), Variant()));
r_options->push_back(ImportOption(PropertyInfo(Variant::STRING, "rest_pose/selected_animation", PROPERTY_HINT_ENUM, "", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_UPDATE_ALL_IF_MODIFIED), ""));
@@ -2619,7 +2627,7 @@ Node *ResourceImporterScene::_generate_meshes(Node *p_node, const Dictionary &p_
mesh_node->set_gi_mode(GeometryInstance3D::GI_MODE_STATIC);
} break;
}
-
+ mesh_node->set_meta(META_EXPOSED_IN_OWNER, src_mesh_node->has_meta(META_EXPOSED_IN_OWNER));
mesh_node->set_layer_mask(src_mesh_node->get_layer_mask());
mesh_node->set_cast_shadows_setting(src_mesh_node->get_cast_shadows_setting());
mesh_node->set_visible(src_mesh_node->is_visible());
diff --git a/editor/scene_tree_dock.cpp b/editor/scene_tree_dock.cpp
index 3b977ac11b11..080d20b6266f 100644
--- a/editor/scene_tree_dock.cpp
+++ b/editor/scene_tree_dock.cpp
@@ -222,6 +222,8 @@ void SceneTreeDock::shortcut_input(const Ref &p_event) {
_tool_selected(TOOL_SHOW_IN_FILE_SYSTEM);
} else if (ED_IS_SHORTCUT("scene_tree/toggle_unique_name", p_event)) {
_tool_selected(TOOL_TOGGLE_SCENE_UNIQUE_NAME);
+ } else if (ED_IS_SHORTCUT("scene_tree/toggle_expose_node", p_event)) {
+ _tool_selected(TOOL_TOGGLE_SCENE_EXPOSE_NODE);
} else if (ED_IS_SHORTCUT("scene_tree/toggle_editable_children", p_event)) {
_tool_selected(TOOL_SCENE_EDITABLE_CHILDREN);
} else if (ED_IS_SHORTCUT("scene_tree/delete", p_event)) {
@@ -321,6 +323,13 @@ void SceneTreeDock::_perform_instantiate_scenes(const Vector &p_files, N
instantiated_scene->set_scene_file_path(ProjectSettings::get_singleton()->localize_path(p_files[i]));
instances.push_back(instantiated_scene);
+
+ for (const NodePath &e_path : sdata->get_state()->get_exposed_nodes()) {
+ Node *ei = instantiated_scene->get_node_or_null(e_path);
+ if (ei) {
+ ei->set_meta(META_EXPOSED_IN_INSTANCE, true);
+ }
+ }
}
if (error) {
@@ -1474,6 +1483,58 @@ void SceneTreeDock::_tool_selected(int p_tool, bool p_confirm_override) {
undo_redo->commit_action();
}
} break;
+ case TOOL_TOGGLE_SCENE_EXPOSE_NODE: {
+ // Enabling/disabling based on the same node based on which the checkbox in the menu is checked/unchecked.
+ List::Element *first_selected = editor_selection->get_selected_node_list().front();
+ if (first_selected == nullptr) {
+ return;
+ }
+ if (first_selected->get() == EditorNode::get_singleton()->get_edited_scene()) {
+ // Exclude Root Node. It should never be exposed in its own scene!
+ editor_selection->remove_node(first_selected->get());
+ first_selected = editor_selection->get_selected_node_list().front();
+ if (first_selected == nullptr) {
+ return;
+ }
+ }
+
+ List full_selection = editor_selection->get_full_selected_node_list();
+ bool enabling = !first_selected->get()->has_meta(META_MARKED_FOR_EXPOSURE);
+
+ EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
+ if (enabling) {
+ undo_redo->create_action(TTR("Expose Node(s) In Scene"));
+ } else {
+ undo_redo->create_action(TTR("Unexpose Node(s) In Scene"));
+ }
+ for (Node *node : full_selection) {
+ StringName name = node->get_name();
+ if (get_tree()->get_edited_scene_root() == node->get_owner()) {
+ if (enabling) {
+ undo_redo->add_do_method(node, "set_meta", META_EXPOSED_IN_OWNER, true);
+ undo_redo->add_undo_method(node, "remove_meta", META_EXPOSED_IN_OWNER);
+ } else {
+ undo_redo->add_do_method(node, "remove_meta", META_EXPOSED_IN_OWNER);
+ undo_redo->add_undo_method(node, "set_meta", META_EXPOSED_IN_OWNER, true);
+ }
+ } else {
+ // Skip nodes that are not exposed in their original scene.
+ if (!node->has_meta(META_EXPOSED_IN_OWNER)) {
+ continue;
+ }
+ }
+ if (enabling) {
+ undo_redo->add_do_method(node, "set_meta", META_MARKED_FOR_EXPOSURE, true);
+ undo_redo->add_undo_method(node, "remove_meta", META_MARKED_FOR_EXPOSURE);
+ } else {
+ undo_redo->add_do_method(node, "remove_meta", META_MARKED_FOR_EXPOSURE);
+ undo_redo->add_undo_method(node, "set_meta", META_MARKED_FOR_EXPOSURE, true);
+ }
+ undo_redo->add_do_method(scene_tree, "update_tree");
+ undo_redo->add_undo_method(scene_tree, "update_tree");
+ }
+ undo_redo->commit_action();
+ } break;
case TOOL_CREATE_2D_SCENE:
case TOOL_CREATE_3D_SCENE:
case TOOL_CREATE_USER_INTERFACE:
@@ -3438,7 +3499,7 @@ static bool _is_node_visible(Node *p_node) {
if (!p_node->get_owner()) {
return false;
}
- if (p_node->get_owner() != EditorNode::get_singleton()->get_edited_scene() && !EditorNode::get_singleton()->get_edited_scene()->is_editable_instance(p_node->get_owner())) {
+ if (p_node->get_owner() != EditorNode::get_singleton()->get_edited_scene() && !EditorNode::get_singleton()->get_edited_scene()->is_editable_instance(p_node->get_owner()) && !p_node->has_meta(META_MARKED_FOR_EXPOSURE)) {
return false;
}
@@ -3878,14 +3939,23 @@ void SceneTreeDock::_tree_rmb(const Vector2 &p_menu_pos) {
break;
}
}
+
+ Node *node = full_selection.front()->get();
if (all_owned) {
// Group "toggle_unique_name" with "copy_node_path", if it is available.
if (menu->get_item_index(TOOL_COPY_NODE_PATH) == -1) {
menu->add_separator();
}
- Node *node = full_selection.front()->get();
menu->add_icon_shortcut(get_editor_theme_icon(SNAME("SceneUniqueName")), ED_GET_SHORTCUT("scene_tree/toggle_unique_name"), TOOL_TOGGLE_SCENE_UNIQUE_NAME);
menu->set_item_text(menu->get_item_index(TOOL_TOGGLE_SCENE_UNIQUE_NAME), node->is_unique_name_in_owner() ? TTR("Revoke Unique Name") : TTR("Access as Unique Name"));
+ } //if all_unique
+
+ if (node->get_owner() == EditorNode::get_singleton()->get_edited_scene()) {
+ menu->add_icon_shortcut(get_editor_theme_icon(SNAME("SceneExposedNode")), ED_GET_SHORTCUT("scene_tree/toggle_expose_node"), TOOL_TOGGLE_SCENE_EXPOSE_NODE);
+ menu->set_item_text(menu->get_item_index(TOOL_TOGGLE_SCENE_EXPOSE_NODE), node->has_meta(META_EXPOSED_IN_OWNER) || node->has_meta(META_MARKED_FOR_EXPOSURE) ? TTR("Unexpose Node") : TTR("Expose Node"));
+ } else if (node->has_meta(META_EXPOSED_IN_OWNER)) {
+ menu->add_icon_shortcut(get_editor_theme_icon(SNAME("SceneExposedNode")), ED_GET_SHORTCUT("scene_tree/toggle_expose_node"), TOOL_TOGGLE_SCENE_EXPOSE_NODE);
+ menu->set_item_text(menu->get_item_index(TOOL_TOGGLE_SCENE_EXPOSE_NODE), node->has_meta(META_MARKED_FOR_EXPOSURE) ? TTR("Unexpose Node") : TTR("Expose Node"));
}
}
@@ -4652,6 +4722,7 @@ SceneTreeDock::SceneTreeDock(Node *p_scene_root, EditorSelection *p_editor_selec
ED_SHORTCUT("scene_tree/copy_node_path", TTRC("Copy Node Path"), KeyModifierMask::CMD_OR_CTRL | KeyModifierMask::SHIFT | Key::C);
ED_SHORTCUT("scene_tree/show_in_file_system", TTRC("Show in FileSystem"));
ED_SHORTCUT("scene_tree/toggle_unique_name", TTRC("Toggle Access as Unique Name"));
+ ED_SHORTCUT("scene_tree/toggle_expose_node", TTR("Toggle Node Exposure"));
ED_SHORTCUT("scene_tree/toggle_editable_children", TTRC("Toggle Editable Children"));
ED_SHORTCUT("scene_tree/delete_no_confirm", TTRC("Delete (No Confirm)"), KeyModifierMask::SHIFT | Key::KEY_DELETE);
ED_SHORTCUT("scene_tree/delete", TTRC("Delete"), Key::KEY_DELETE);
diff --git a/editor/scene_tree_dock.h b/editor/scene_tree_dock.h
index 5a37eafc9dd2..f114276baba9 100644
--- a/editor/scene_tree_dock.h
+++ b/editor/scene_tree_dock.h
@@ -89,6 +89,7 @@ class SceneTreeDock : public VBoxContainer {
TOOL_SCENE_CLEAR_INHERITANCE_CONFIRM,
TOOL_SCENE_OPEN_INHERITED,
TOOL_TOGGLE_SCENE_UNIQUE_NAME,
+ TOOL_TOGGLE_SCENE_EXPOSE_NODE,
TOOL_CREATE_2D_SCENE,
TOOL_CREATE_3D_SCENE,
TOOL_CREATE_USER_INTERFACE,
@@ -156,6 +157,7 @@ class SceneTreeDock : public VBoxContainer {
Label *delete_dialog_label = nullptr;
CheckBox *delete_tracks_checkbox = nullptr;
ConfirmationDialog *editable_instance_remove_dialog = nullptr;
+ ConfirmationDialog *revoke_node_exposure_dialog = nullptr;
ConfirmationDialog *placeholder_editable_instance_remove_dialog = nullptr;
ReparentDialog *reparent_dialog = nullptr;
diff --git a/scene/main/node.cpp b/scene/main/node.cpp
index f5bc3344dd4a..e2675ac4abfa 100644
--- a/scene/main/node.cpp
+++ b/scene/main/node.cpp
@@ -2172,6 +2172,21 @@ bool Node::is_unique_name_in_owner() const {
return data.unique_name_in_owner;
}
+bool Node::has_exposed_nodes() const {
+ for (const KeyValue &KV : data.children) {
+ if (!KV.value->data.owner) {
+ continue;
+ }
+ if (KV.value->has_meta(META_EXPOSED_IN_INSTANCE)) {
+ return true;
+ }
+ if (KV.value->has_exposed_nodes()) {
+ return true;
+ }
+ }
+ return false;
+}
+
void Node::set_owner(Node *p_owner) {
ERR_MAIN_THREAD_GUARD
if (data.owner) {
diff --git a/scene/main/node.h b/scene/main/node.h
index 2d6fa1575064..cae3331a05c2 100644
--- a/scene/main/node.h
+++ b/scene/main/node.h
@@ -517,6 +517,8 @@ class Node : public Object {
void set_unique_name_in_owner(bool p_enabled);
bool is_unique_name_in_owner() const;
+ bool has_exposed_nodes() const;
+
_FORCE_INLINE_ int get_index(bool p_include_internal = true) const {
// p_include_internal = false doesn't make sense if the node is internal.
ERR_FAIL_COND_V_MSG(!p_include_internal && data.internal_mode != INTERNAL_MODE_DISABLED, -1, "Node is internal. Can't get index with 'include_internal' being false.");
@@ -846,6 +848,13 @@ Error Node::rpc_id(int p_peer_id, const StringName &p_method, VarArgs... p_args)
return rpcp(p_peer_id, p_method, sizeof...(p_args) == 0 ? nullptr : (const Variant **)argptrs, sizeof...(p_args));
}
+#define META_PROPERTY_EXPOSED_IN_OWNER "metadata/_exposed_in_owner"
+#define META_EXPOSED_IN_OWNER "_exposed_in_owner"
+#define META_PROPERTY_MARKED_FOR_EXPOSURE "metadata/_marked_for_exposure"
+#define META_MARKED_FOR_EXPOSURE "_marked_for_exposure"
+#define META_PROPERTY_EXPOSED_IN_INSTANCE "metadata/_exposed_in_instance"
+#define META_EXPOSED_IN_INSTANCE "_exposed_in_instance"
+
#ifdef DEBUG_ENABLED
#define ERR_THREAD_GUARD ERR_FAIL_COND_MSG(!is_accessible_from_caller_thread(), vformat("Caller thread can't call this function in this node (%s). Use call_deferred() or call_thread_group() instead.", get_description()));
#define ERR_THREAD_GUARD_V(m_ret) ERR_FAIL_COND_V_MSG(!is_accessible_from_caller_thread(), (m_ret), vformat("Caller thread can't call this function in this node (%s). Use call_deferred() or call_thread_group() instead.", get_description()));
diff --git a/scene/resources/packed_scene.cpp b/scene/resources/packed_scene.cpp
index 7f170e0085ec..7cf8ae45ef8c 100644
--- a/scene/resources/packed_scene.cpp
+++ b/scene/resources/packed_scene.cpp
@@ -230,6 +230,18 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
if (sdata.is_valid()) {
node = sdata->instantiate(p_edit_state == GEN_EDIT_STATE_DISABLED ? PackedScene::GEN_EDIT_STATE_DISABLED : PackedScene::GEN_EDIT_STATE_INSTANCE);
ERR_FAIL_NULL_V_MSG(node, nullptr, vformat("Failed to load scene dependency: \"%s\". Make sure the required scene is valid.", sdata->get_path()));
+#ifdef TOOLS_ENABLED
+ if (p_edit_state == GEN_EDIT_STATE_MAIN || p_edit_state == GEN_EDIT_STATE_MAIN_INHERITED) {
+ for (const NodePath &e_path : sdata->get_state()->exposed_nodes) {
+ Node *ei = node->get_node_or_null(e_path);
+ if (ei) {
+ if (ei->has_meta(META_EXPOSED_IN_OWNER)) {
+ ei->set_meta(META_EXPOSED_IN_INSTANCE, true);
+ }
+ }
+ }
+ }
+#endif
} else if (ResourceLoader::is_creating_missing_resources_if_class_unavailable_enabled()) {
missing_node = memnew(MissingNode);
#ifdef TOOLS_ENABLED
@@ -461,6 +473,14 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
node->set_name(snames[n.name]);
parent->add_child(node, true);
pending_add = false;
+ } else if (n.owner == 0) {
+ // Check whether a node's parent is no longer exposed. This only matters within the Editor.
+ NODE_FROM_ID(nowner, n.owner);
+ if (nowner != parent && parent->get_owner() != ret_nodes[0] && !parent->has_meta(META_EXPOSED_IN_INSTANCE) && !parent->get_owner()->is_editable_instance(parent)) {
+ WARN_PRINT(vformat("Exposed parent path '%s' for node '%s' has vanished when instantiating: '%s'.", String(node_paths[n.parent & FLAG_MASK]), String(snames[n.name]), get_path()));
+ old_parent_path = String(node_paths[n.parent & FLAG_MASK]).trim_prefix("./").replace("/", "@");
+ parent = ret_nodes[0];
+ }
}
}
#endif
@@ -623,6 +643,17 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
}
}
+#ifdef TOOLS_ENABLED
+ for (const NodePath &e_path : exposed_nodes) {
+ Node *ei = ret_nodes[0]->get_node_or_null(e_path);
+ if (ei) {
+ ei->set_meta(META_EXPOSED_IN_OWNER, true);
+ if (p_edit_state == GEN_EDIT_STATE_MAIN || p_edit_state == GEN_EDIT_STATE_MAIN_INHERITED) {
+ ei->set_meta(META_MARKED_FOR_EXPOSURE, true);
+ }
+ }
+ }
+#endif
return ret_nodes[0];
}
@@ -716,8 +747,22 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
// given the complexity of this process, an attempt will be made to properly
// document it. if you fail to understand something, please ask!
- //discard nodes that do not belong to be processed
- if (p_node != p_owner && p_node->get_owner() != p_owner && !p_owner->is_editable_instance(p_node->get_owner())) {
+ //discard nodes that do not belong to be processed.
+ // - `owned_by_p_owner`: True if the node is directly or indirectly owned by `p_owner`.
+ // - `exposed_node`: True if the node is flagged as exposed via metadata `META_EXPOSED_IN_OWNER`.
+ bool owned_by_p_owner = p_node == p_owner || p_node->get_owner() == p_owner || p_owner->is_editable_instance(p_node->get_owner());
+ bool exposed_node = p_node->has_meta(META_EXPOSED_IN_OWNER);
+
+ if (!owned_by_p_owner && !exposed_node) {
+ if (p_node->has_exposed_nodes()) {
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ Node *c = p_node->get_child(i);
+ Error err = _parse_node(p_owner, c, NO_PARENT_SAVED, name_map, variant_map, node_map, nodepath_map);
+ if (err) {
+ return err;
+ }
+ }
+ }
return OK;
}
@@ -729,11 +774,16 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
editable_instances.push_back(p_owner->get_path_to(p_node));
// Node is the root of an editable instance.
is_editable_instance = true;
- } else if (p_node->get_owner() && p_owner->is_ancestor_of(p_node->get_owner()) && p_owner->is_editable_instance(p_node->get_owner())) {
+ } else if (p_node->get_owner() && p_owner->is_ancestor_of(p_node->get_owner()) && p_owner->is_editable_instance(p_node->get_owner()) && p_owner->has_meta(META_EXPOSED_IN_OWNER)) {
// Node is part of an editable instance.
is_editable_instance = true;
}
+ // Save the nodes that are chosen as exposed, so they can be restored on load
+ if (p_node->has_meta(META_MARKED_FOR_EXPOSURE) || (p_node->has_meta(META_EXPOSED_IN_OWNER) && p_node->get_owner() == p_owner)) {
+ exposed_nodes.push_back(p_owner->get_path_to(p_node));
+ }
+
NodeData nd;
nd.name = _nm_get_string(p_node->get_name(), name_map);
@@ -916,6 +966,11 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
}
}
+ // Skip exposure metadata.
+ if (name == META_PROPERTY_EXPOSED_IN_OWNER || name == META_PROPERTY_MARKED_FOR_EXPOSURE || name == META_PROPERTY_EXPOSED_IN_INSTANCE) {
+ continue;
+ }
+
NodeData::Property prop;
prop.name = _nm_get_string(name, name_map);
prop.value = _vm_get_variant(value, variant_map);
@@ -993,6 +1048,7 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
bool save_node = nd.properties.size() || nd.groups.size(); // some local properties or groups exist
save_node = save_node || p_node == p_owner; // owner is always saved
save_node = save_node || (p_node->get_owner() == p_owner && instantiated_by_owner); //part of scene and not instanced
+ save_node = save_node || (p_owner->has_meta(META_MARKED_FOR_EXPOSURE) && nd.properties.size() > 0);
int idx = nodes.size();
int parent_node = NO_PARENT_SAVED;
@@ -1033,8 +1089,22 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
}
Error SceneState::_parse_connections(Node *p_owner, Node *p_node, HashMap &name_map, HashMap &variant_map, HashMap &node_map, HashMap &nodepath_map) {
- // Ignore nodes that are within a scene instance.
- if (p_node != p_owner && p_node->get_owner() && p_node->get_owner() != p_owner && !p_owner->is_editable_instance(p_node->get_owner())) {
+ // Ignore nodes that are within a scene instance or not exposed.
+ // - `owned_by_p_owner`: True if the node is directly or indirectly owned by `p_owner`.
+ // - `exposed_node`: True if the node is flagged as exposed via metadata `META_EXPOSED_IN_OWNER`.
+ bool owned_by_p_owner = p_node == p_owner || p_node->get_owner() == p_owner || p_owner->is_editable_instance(p_node->get_owner());
+ bool exposed_node = p_node->has_meta(META_EXPOSED_IN_OWNER);
+
+ if (!owned_by_p_owner && !exposed_node) {
+ if (p_node->has_exposed_nodes()) {
+ for (int i = 0; i < p_node->get_child_count(); i++) {
+ Node *c = p_node->get_child(i);
+ Error err = _parse_connections(p_owner, c, name_map, variant_map, node_map, nodepath_map);
+ if (err) {
+ return err;
+ }
+ }
+ }
return OK;
}
@@ -1311,6 +1381,7 @@ void SceneState::clear() {
node_path_cache.clear();
node_paths.clear();
editable_instances.clear();
+ exposed_nodes.clear();
base_scene_idx = -1;
}
@@ -1340,6 +1411,9 @@ Error SceneState::copy_from(const Ref &p_scene_state) {
for (const NodePath &E : p_scene_state->editable_instances) {
editable_instances.append(E);
}
+ for (const NodePath &E : p_scene_state->exposed_nodes) {
+ exposed_nodes.append(E);
+ }
base_scene_idx = p_scene_state->base_scene_idx;
return OK;
@@ -1594,6 +1668,10 @@ void SceneState::set_bundled_scene(const Dictionary &p_dictionary) {
if (p_dictionary.has("editable_instances")) {
ei = p_dictionary["editable_instances"];
}
+ Array en;
+ if (p_dictionary.has("exposed_nodes")) {
+ en = p_dictionary["exposed_nodes"];
+ }
if (p_dictionary.has("base_scene")) {
base_scene_idx = p_dictionary["base_scene"];
@@ -1603,6 +1681,10 @@ void SceneState::set_bundled_scene(const Dictionary &p_dictionary) {
for (int i = 0; i < editable_instances.size(); i++) {
editable_instances.write[i] = ei[i];
}
+ exposed_nodes.resize(en.size());
+ for (int i = 0; i < exposed_nodes.size(); i++) {
+ exposed_nodes.write[i] = en[i];
+ }
//path=p_dictionary["path"];
}
@@ -1682,6 +1764,13 @@ Dictionary SceneState::get_bundled_scene() const {
reditable_instances[i] = editable_instances[i];
}
d["editable_instances"] = reditable_instances;
+
+ Array rexposed_nodes;
+ rexposed_nodes.resize(exposed_nodes.size());
+ for (int i = 0; i < exposed_nodes.size(); i++) {
+ rexposed_nodes[i] = exposed_nodes[i];
+ }
+ d["exposed_nodes"] = rexposed_nodes;
if (base_scene_idx >= 0) {
d["base_scene"] = base_scene_idx;
}
@@ -1950,6 +2039,10 @@ Vector SceneState::get_editable_instances() const {
return editable_instances;
}
+Vector SceneState::get_exposed_nodes() const {
+ return exposed_nodes;
+}
+
Ref SceneState::get_sub_resource(const String &p_path) {
for (const Variant &v : variants) {
const Ref &res = v;
@@ -2038,6 +2131,10 @@ void SceneState::add_editable_instance(const NodePath &p_path) {
editable_instances.push_back(p_path);
}
+void SceneState::add_exposed_node(const NodePath &p_path) {
+ exposed_nodes.push_back(p_path);
+}
+
bool SceneState::remove_group_references(const StringName &p_name) {
bool edited = false;
for (NodeData &node : nodes) {
diff --git a/scene/resources/packed_scene.h b/scene/resources/packed_scene.h
index 9f8088910f18..614d71919667 100644
--- a/scene/resources/packed_scene.h
+++ b/scene/resources/packed_scene.h
@@ -40,6 +40,7 @@ class SceneState : public RefCounted {
Vector names;
Vector variants;
Vector node_paths;
+ Vector exposed_nodes;
Vector editable_instances;
mutable HashMap node_path_cache;
mutable HashMap base_scene_node_remap;
@@ -196,6 +197,7 @@ class SceneState : public RefCounted {
bool has_connection(const NodePath &p_node_from, const StringName &p_signal, const NodePath &p_node_to, const StringName &p_method, bool p_no_inheritance = false);
Vector get_editable_instances() const;
+ Vector get_exposed_nodes() const;
Ref get_sub_resource(const String &p_path);
//build API
@@ -209,6 +211,7 @@ class SceneState : public RefCounted {
void set_base_scene(int p_idx);
void add_connection(int p_from, int p_to, int p_signal, int p_method, int p_flags, int p_unbinds, const Vector &p_binds);
void add_editable_instance(const NodePath &p_path);
+ void add_exposed_node(const NodePath &p_path);
bool remove_group_references(const StringName &p_name);
bool rename_group_references(const StringName &p_old_name, const StringName &p_new_name);
diff --git a/scene/resources/resource_format_text.cpp b/scene/resources/resource_format_text.cpp
index f48d15fc8476..a5191ddcbc27 100644
--- a/scene/resources/resource_format_text.cpp
+++ b/scene/resources/resource_format_text.cpp
@@ -383,6 +383,29 @@ Ref ResourceLoaderText::_parse_node_tag(VariantParser::ResourcePars
error = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &parser);
+ if (error) {
+ if (error != ERR_FILE_EOF) {
+ _printerr();
+ return Ref();
+ } else {
+ error = OK;
+ return packed_scene;
+ }
+ }
+ } else if (next_tag.name == "exposed") {
+ if (!next_tag.fields.has("path")) {
+ error = ERR_FILE_CORRUPT;
+ error_text = "missing 'path' field from exposed tag";
+ _printerr();
+ return Ref();
+ }
+
+ NodePath path = next_tag.fields["path"];
+
+ packed_scene->get_state()->add_exposed_node(path.simplified());
+
+ error = VariantParser::parse_tag(&stream, lines, error_text, next_tag, &parser);
+
if (error) {
if (error != ERR_FILE_EOF) {
_printerr();
@@ -2077,6 +2100,13 @@ Error ResourceFormatSaverTextInstance::save(const String &p_path, const Refstore_line("[editable path=\"" + editable_instances[i].operator String().c_escape() + "\"]");
}
+ Vector exposed_nodes = state->get_exposed_nodes();
+ for (int i = 0; i < exposed_nodes.size(); i++) {
+ if (i == 0) {
+ f->store_line("");
+ }
+ f->store_line("[exposed path=\"" + exposed_nodes[i].operator String().c_escape() + "\"]");
+ }
}
if (f->get_error() != OK && f->get_error() != ERR_FILE_EOF) {