Add ability to export Node pointers as NodePaths

This PR implements:
* A new hint: PROPERTY_HINT_NODE_TYPE for variant type OBJECT, which can take specific node types as hint string.
* The editor will show it as a node path, but will set it as a pointer to a node from the current scene if you select a path.
* When scene is saved, the node path is saved, then restored as a pointer.

NOTE: This is a proof of concept and this approach will most likely not work. The reason if that, if the node referenced is deleted, then when trying to edit this the node will become invalid.

Potential workarounds: Since this uses the Variant API, it should obtain the pointer from the Variant object ID. Yet, this would either only really work in GDScript or it would need to be implemented with workarounds in every language.
Alternative ways to make this work: Nodes could export an additional property with a node path (like for which_node, it could be which_node_path).
Another alternative: Path editing could happen as a hidden metadata (ignoring the pointer).
This commit is contained in:
reduz 2022-06-19 00:42:02 +02:00
parent c32285733d
commit b7c41f9ba1
11 changed files with 184 additions and 36 deletions

View file

@ -613,6 +613,7 @@ void register_global_constants() {
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_ARRAY_TYPE);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_LOCALE_ID);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_LOCALIZABLE_STRING);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_NODE_TYPE);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_MAX);
BIND_CORE_ENUM_CONSTANT(PROPERTY_USAGE_NONE);

View file

@ -91,6 +91,7 @@ enum PropertyHint {
PROPERTY_HINT_INT_IS_POINTER,
PROPERTY_HINT_LOCALE_ID,
PROPERTY_HINT_LOCALIZABLE_STRING,
PROPERTY_HINT_NODE_TYPE, ///< a node object type
PROPERTY_HINT_MAX,
// When updating PropertyHint, also sync the hardcoded list in VisualScriptEditorVariableEdit
};

View file

@ -2583,7 +2583,9 @@
<constant name="PROPERTY_HINT_LOCALIZABLE_STRING" value="44" enum="PropertyHint">
Hints that a dictionary property is string translation map. Dictionary keys are locale codes and, values are translated strings.
</constant>
<constant name="PROPERTY_HINT_MAX" value="45" enum="PropertyHint">
<constant name="PROPERTY_HINT_NODE_TYPE" value="45" enum="PropertyHint">
</constant>
<constant name="PROPERTY_HINT_MAX" value="46" enum="PropertyHint">
</constant>
<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags">
</constant>

View file

@ -38,7 +38,7 @@
Gets the edited object.
</description>
</method>
<method name="get_edited_property">
<method name="get_edited_property" qualifiers="const">
<return type="StringName" />
<description>
Gets the edited property. If your editor is for a single property (added via [method EditorInspectorPlugin._parse_property]), then this will return the property.

View file

@ -406,7 +406,7 @@ Object *EditorProperty::get_edited_object() {
return object;
}
StringName EditorProperty::get_edited_property() {
StringName EditorProperty::get_edited_property() const {
return property;
}
@ -437,16 +437,20 @@ Variant EditorPropertyRevert::get_property_revert_value(Object *p_object, const
return PropertyUtils::get_property_default_value(p_object, p_property, r_is_valid);
}
bool EditorPropertyRevert::can_property_revert(Object *p_object, const StringName &p_property) {
bool EditorPropertyRevert::can_property_revert(Object *p_object, const StringName &p_property, const Variant *p_custom_current_value) {
bool is_valid_revert = false;
Variant revert_value = EditorPropertyRevert::get_property_revert_value(p_object, p_property, &is_valid_revert);
if (!is_valid_revert) {
return false;
}
Variant current_value = p_object->get(p_property);
Variant current_value = p_custom_current_value ? *p_custom_current_value : p_object->get(p_property);
return PropertyUtils::is_property_value_different(current_value, revert_value);
}
StringName EditorProperty::_get_revert_property() const {
return property;
}
void EditorProperty::update_revert_and_pin_status() {
if (property == StringName()) {
return; //no property, so nothing to do
@ -458,7 +462,8 @@ void EditorProperty::update_revert_and_pin_status() {
CRASH_COND(!node);
new_pinned = node->is_property_pinned(property);
}
bool new_can_revert = EditorPropertyRevert::can_property_revert(object, property) && !is_read_only();
Variant current = object->get(_get_revert_property());
bool new_can_revert = EditorPropertyRevert::can_property_revert(object, property, &current) && !is_read_only();
if (new_can_revert != can_revert || new_pinned != pinned) {
can_revert = new_can_revert;
@ -717,11 +722,15 @@ void EditorProperty::set_bottom_editor(Control *p_control) {
bottom_editor = p_control;
}
Variant EditorProperty::_get_cache_value(const StringName &p_prop, bool &r_valid) const {
return object->get(p_prop, &r_valid);
}
bool EditorProperty::is_cache_valid() const {
if (object) {
for (const KeyValue<StringName, Variant> &E : cache) {
bool valid;
Variant value = object->get(E.key, &valid);
Variant value = _get_cache_value(E.key, valid);
if (!valid || value != E.value) {
return false;
}
@ -733,7 +742,7 @@ void EditorProperty::update_cache() {
cache.clear();
if (object && property != StringName()) {
bool valid;
Variant value = object->get(property, &valid);
Variant value = _get_cache_value(property, valid);
if (valid) {
cache[property] = value;
}

View file

@ -50,7 +50,7 @@ public:
static bool is_property_value_different(const Variant &p_a, const Variant &p_b);
static Variant get_property_revert_value(Object *p_object, const StringName &p_property, bool *r_is_valid);
static bool can_property_revert(Object *p_object, const StringName &p_property);
static bool can_property_revert(Object *p_object, const StringName &p_property, const Variant *p_custom_current_value = nullptr);
};
class EditorProperty : public Container {
@ -131,6 +131,9 @@ protected:
virtual void shortcut_input(const Ref<InputEvent> &p_event) override;
const Color *_get_property_colors();
virtual Variant _get_cache_value(const StringName &p_prop, bool &r_valid) const;
virtual StringName _get_revert_property() const;
public:
void emit_changed(const StringName &p_property, const Variant &p_value, const StringName &p_field = StringName(), bool p_changing = false);
@ -143,7 +146,7 @@ public:
bool is_read_only() const;
Object *get_edited_object();
StringName get_edited_property();
StringName get_edited_property() const;
virtual void update_property();
void update_revert_and_pin_status();

View file

@ -43,6 +43,7 @@
#include "scene/main/window.h"
#include "scene/resources/font.h"
#include "scene/resources/mesh.h"
#include "scene/resources/packed_scene.h"
///////////////////// Nil /////////////////////////
@ -3017,6 +3018,23 @@ void EditorPropertyNodePath::_set_read_only(bool p_read_only) {
clear->set_disabled(p_read_only);
};
String EditorPropertyNodePath::_get_meta_pointer_property() const {
ERR_FAIL_COND_V(!pointer_mode, String());
return SceneState::get_meta_pointer_property(get_edited_property());
}
Variant EditorPropertyNodePath::_get_cache_value(const StringName &p_prop, bool &r_valid) const {
if (p_prop == get_edited_property()) {
r_valid = true;
return const_cast<EditorPropertyNodePath *>(this)->get_edited_object()->get(_get_meta_pointer_property(), &r_valid);
}
return Variant();
}
StringName EditorPropertyNodePath::_get_revert_property() const {
return _get_meta_pointer_property();
}
void EditorPropertyNodePath::_node_selected(const NodePath &p_path) {
NodePath path = p_path;
Node *base_node = nullptr;
@ -3048,7 +3066,11 @@ void EditorPropertyNodePath::_node_selected(const NodePath &p_path) {
if (base_node) { // for AnimationTrackKeyEdit
path = base_node->get_path().rel_path_to(p_path);
}
emit_changed(get_edited_property(), path);
if (pointer_mode && base_node) {
emit_changed(_get_meta_pointer_property(), path);
} else {
emit_changed(get_edited_property(), path);
}
update_property();
}
@ -3064,7 +3086,11 @@ void EditorPropertyNodePath::_node_assign() {
}
void EditorPropertyNodePath::_node_clear() {
emit_changed(get_edited_property(), NodePath());
if (pointer_mode) {
emit_changed(_get_meta_pointer_property(), NodePath());
} else {
emit_changed(get_edited_property(), NodePath());
}
update_property();
}
@ -3092,7 +3118,12 @@ bool EditorPropertyNodePath::is_drop_valid(const Dictionary &p_drag_data) const
}
void EditorPropertyNodePath::update_property() {
NodePath p = get_edited_object()->get(get_edited_property());
NodePath p;
if (pointer_mode) {
p = get_edited_object()->get(_get_meta_pointer_property());
} else {
p = get_edited_object()->get(get_edited_property());
}
assign->set_tooltip(p);
if (p == NodePath()) {
@ -3131,7 +3162,8 @@ void EditorPropertyNodePath::update_property() {
assign->set_icon(EditorNode::get_singleton()->get_object_icon(target_node, "Node"));
}
void EditorPropertyNodePath::setup(const NodePath &p_base_hint, Vector<StringName> p_valid_types, bool p_use_path_from_scene_root) {
void EditorPropertyNodePath::setup(const NodePath &p_base_hint, Vector<StringName> p_valid_types, bool p_use_path_from_scene_root, bool p_pointer_mode) {
pointer_mode = p_pointer_mode;
base_hint = p_base_hint;
valid_types = p_valid_types;
use_path_from_scene_root = p_use_path_from_scene_root;
@ -3927,23 +3959,31 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
return editor;
} break;
case Variant::OBJECT: {
EditorPropertyResource *editor = memnew(EditorPropertyResource);
editor->setup(p_object, p_path, p_hint == PROPERTY_HINT_RESOURCE_TYPE ? p_hint_text : "Resource");
if (p_hint == PROPERTY_HINT_NODE_TYPE) {
EditorPropertyNodePath *editor = memnew(EditorPropertyNodePath);
Vector<String> types = p_hint_text.split(",", false);
Vector<StringName> sn = Variant(types); //convert via variant
editor->setup(NodePath(), sn, false, true);
return editor;
} else {
EditorPropertyResource *editor = memnew(EditorPropertyResource);
editor->setup(p_object, p_path, p_hint == PROPERTY_HINT_RESOURCE_TYPE ? p_hint_text : "Resource");
if (p_hint == PROPERTY_HINT_RESOURCE_TYPE) {
String open_in_new = EDITOR_GET("interface/inspector/resources_to_open_in_new_inspector");
for (int i = 0; i < open_in_new.get_slice_count(","); i++) {
String type = open_in_new.get_slicec(',', i).strip_edges();
for (int j = 0; j < p_hint_text.get_slice_count(","); j++) {
String inherits = p_hint_text.get_slicec(',', j);
if (ClassDB::is_parent_class(inherits, type)) {
editor->set_use_sub_inspector(false);
if (p_hint == PROPERTY_HINT_RESOURCE_TYPE) {
String open_in_new = EDITOR_GET("interface/inspector/resources_to_open_in_new_inspector");
for (int i = 0; i < open_in_new.get_slice_count(","); i++) {
String type = open_in_new.get_slicec(',', i).strip_edges();
for (int j = 0; j < p_hint_text.get_slice_count(","); j++) {
String inherits = p_hint_text.get_slicec(',', j);
if (ClassDB::is_parent_class(inherits, type)) {
editor->set_use_sub_inspector(false);
}
}
}
}
}
return editor;
return editor;
}
} break;
case Variant::DICTIONARY: {

View file

@ -704,6 +704,7 @@ class EditorPropertyNodePath : public EditorProperty {
SceneTreeDialog *scene_tree = nullptr;
NodePath base_hint;
bool use_path_from_scene_root = false;
bool pointer_mode = false;
Vector<StringName> valid_types;
void _node_selected(const NodePath &p_path);
@ -714,6 +715,10 @@ class EditorPropertyNodePath : public EditorProperty {
void drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from);
bool is_drop_valid(const Dictionary &p_drag_data) const;
String _get_meta_pointer_property() const;
virtual Variant _get_cache_value(const StringName &p_prop, bool &r_valid) const override;
virtual StringName _get_revert_property() const override;
protected:
virtual void _set_read_only(bool p_read_only) override;
static void _bind_methods();
@ -721,7 +726,7 @@ protected:
public:
virtual void update_property() override;
void setup(const NodePath &p_base_hint, Vector<StringName> p_valid_types, bool p_use_path_from_scene_root = true);
void setup(const NodePath &p_base_hint, Vector<StringName> p_valid_types, bool p_use_path_from_scene_root = true, bool p_pointer_mode = false);
EditorPropertyNodePath();
};

View file

@ -35,6 +35,7 @@
#include "core/core_string_names.h"
#include "core/io/missing_resource.h"
#include "core/io/resource_loader.h"
#include "core/templates/local_vector.h"
#include "scene/2d/node_2d.h"
#include "scene/3d/node_3d.h"
#include "scene/gui/control.h"
@ -43,7 +44,7 @@
#include "scene/property_utils.h"
#define PACKED_SCENE_VERSION 2
#define META_POINTER_PROPERTY_BASE "metadata/_editor_prop_ptr_"
bool SceneState::can_instantiate() const {
return nodes.size() > 0;
}
@ -108,6 +109,8 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
HashMap<Ref<Resource>, Ref<Resource>> resources_local_to_scene;
LocalVector<DeferredNodePathProperties> deferred_node_paths;
for (int i = 0; i < nc; i++) {
const NodeData &n = nd[i];
@ -230,9 +233,28 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
for (int j = 0; j < nprop_count; j++) {
bool valid;
ERR_FAIL_INDEX_V(nprops[j].name, sname_count, nullptr);
ERR_FAIL_INDEX_V(nprops[j].value, prop_count, nullptr);
if (nprops[j].name & FLAG_PATH_PROPERTY_IS_NODE) {
uint32_t name_idx = nprops[j].name & (FLAG_PATH_PROPERTY_IS_NODE - 1);
ERR_FAIL_UNSIGNED_INDEX_V(name_idx, (uint32_t)sname_count, nullptr);
if (Engine::get_singleton()->is_editor_hint()) {
// If editor, just set the metadata and be it
node->set(META_POINTER_PROPERTY_BASE + String(snames[name_idx]), props[nprops[j].value]);
} else {
// Do an actual deferred sed of the property path.
DeferredNodePathProperties dnp;
dnp.path = props[nprops[j].value];
dnp.base = node;
dnp.property = snames[name_idx];
deferred_node_paths.push_back(dnp);
}
continue;
}
ERR_FAIL_INDEX_V(nprops[j].name, sname_count, nullptr);
if (snames[nprops[j].name] == CoreStringNames::get_singleton()->_script) {
//work around to avoid old script variables from disappearing, should be the proper fix to:
//https://github.com/godotengine/godot/issues/2958
@ -369,6 +391,12 @@ Node *SceneState::instantiate(GenEditState p_edit_state) const {
}
}
for (uint32_t i = 0; i < deferred_node_paths.size(); i++) {
const DeferredNodePathProperties &dnp = deferred_node_paths[i];
Node *other = dnp.base->get_node_or_null(dnp.path);
dnp.base->set(dnp.property, other);
}
for (KeyValue<Ref<Resource>, Ref<Resource>> &E : resources_local_to_scene) {
E.value->setup_local_to_scene();
}
@ -532,6 +560,9 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
if (E.name == META_PROPERTY_MISSING_RESOURCES) {
continue; // Ignore this property when packing.
}
if (E.name.begins_with(META_POINTER_PROPERTY_BASE)) {
continue; // do not save.
}
// If instance or inheriting, not saving if property requested so.
if (!states_stack.is_empty()) {
@ -542,8 +573,15 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
StringName name = E.name;
Variant value = p_node->get(name);
bool use_deferred_node_path_bit = false;
if (E.type == Variant::OBJECT && missing_resource_properties.has(E.name)) {
if (E.type == Variant::OBJECT && E.hint == PROPERTY_HINT_NODE_TYPE) {
value = p_node->get(META_POINTER_PROPERTY_BASE + E.name);
if (value.get_type() != Variant::NODE_PATH) {
continue; //was never set, ignore.
}
use_deferred_node_path_bit = true;
} else if (E.type == Variant::OBJECT && missing_resource_properties.has(E.name)) {
// Was this missing resource overridden? If so do not save the old value.
Ref<Resource> ures = value;
if (ures.is_null()) {
@ -562,6 +600,9 @@ Error SceneState::_parse_node(Node *p_owner, Node *p_node, int p_parent_idx, Has
NodeData::Property prop;
prop.name = _nm_get_string(name, name_map);
prop.value = _vm_get_variant(value, variant_map);
if (use_deferred_node_path_bit) {
prop.name |= FLAG_PATH_PROPERTY_IS_NODE;
}
nd.properties.push_back(prop);
}
@ -1018,7 +1059,7 @@ Variant SceneState::get_property_value(int p_node, const StringName &p_property,
const NodeData::Property *p = nodes[p_node].properties.ptr();
for (int i = 0; i < pc; i++) {
if (p_property == namep[p[i].name]) {
if (p_property == namep[p[i].name & FLAG_PROP_NAME_MASK]) {
found = true;
return variants[p[i].value];
}
@ -1409,7 +1450,19 @@ int SceneState::get_node_property_count(int p_idx) const {
StringName SceneState::get_node_property_name(int p_idx, int p_prop) const {
ERR_FAIL_INDEX_V(p_idx, nodes.size(), StringName());
ERR_FAIL_INDEX_V(p_prop, nodes[p_idx].properties.size(), StringName());
return names[nodes[p_idx].properties[p_prop].name];
return names[nodes[p_idx].properties[p_prop].name & FLAG_PROP_NAME_MASK];
}
Vector<String> SceneState::get_node_deferred_nodepath_properties(int p_idx) const {
Vector<String> ret;
ERR_FAIL_INDEX_V(p_idx, nodes.size(), ret);
for (int i = 0; i < nodes[p_idx].properties.size(); i++) {
uint32_t idx = nodes[p_idx].properties[i].name;
if (idx & FLAG_PATH_PROPERTY_IS_NODE) {
ret.push_back(names[idx & FLAG_PROP_NAME_MASK]);
}
}
return ret;
}
Variant SceneState::get_node_property_value(int p_idx, int p_prop) const {
@ -1555,13 +1608,16 @@ int SceneState::add_node(int p_parent, int p_owner, int p_type, int p_name, int
return nodes.size() - 1;
}
void SceneState::add_node_property(int p_node, int p_name, int p_value) {
void SceneState::add_node_property(int p_node, int p_name, int p_value, bool p_deferred_node_path) {
ERR_FAIL_INDEX(p_node, nodes.size());
ERR_FAIL_INDEX(p_name, names.size());
ERR_FAIL_INDEX(p_value, variants.size());
NodeData::Property prop;
prop.name = p_name;
if (p_deferred_node_path) {
prop.name |= FLAG_PATH_PROPERTY_IS_NODE;
}
prop.value = p_value;
nodes.write[p_node].properties.push_back(prop);
}
@ -1599,6 +1655,10 @@ void SceneState::add_editable_instance(const NodePath &p_path) {
editable_instances.push_back(p_path);
}
String SceneState::get_meta_pointer_property(const String &p_property) {
return META_POINTER_PROPERTY_BASE + p_property;
}
Vector<String> SceneState::_get_node_groups(int p_idx) const {
Vector<StringName> groups = get_node_groups(p_idx);
Vector<String> ret;

View file

@ -69,6 +69,12 @@ class SceneState : public RefCounted {
Vector<int> groups;
};
struct DeferredNodePathProperties {
Node *base = nullptr;
StringName property;
NodePath path;
};
Vector<NodeData> nodes;
struct ConnectionData {
@ -104,6 +110,8 @@ public:
FLAG_ID_IS_PATH = (1 << 30),
TYPE_INSTANCED = 0x7FFFFFFF,
FLAG_INSTANCE_IS_PLACEHOLDER = (1 << 30),
FLAG_PATH_PROPERTY_IS_NODE = (1 << 30),
FLAG_PROP_NAME_MASK = FLAG_PATH_PROPERTY_IS_NODE - 1,
FLAG_MASK = (1 << 24) - 1,
};
@ -157,6 +165,7 @@ public:
int get_node_property_count(int p_idx) const;
StringName get_node_property_name(int p_idx, int p_prop) const;
Variant get_node_property_value(int p_idx, int p_prop) const;
Vector<String> get_node_deferred_nodepath_properties(int p_idx) const;
int get_connection_count() const;
NodePath get_connection_source(int p_idx) const;
@ -177,7 +186,7 @@ public:
int add_value(const Variant &p_value);
int add_node_path(const NodePath &p_path);
int add_node(int p_parent, int p_owner, int p_type, int p_name, int p_instance, int p_index);
void add_node_property(int p_node, int p_name, int p_value);
void add_node_property(int p_node, int p_name, int p_value, bool p_deferred_node_path = false);
void add_node_group(int p_node, int p_group);
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<int> &p_binds);
@ -186,6 +195,9 @@ public:
virtual void set_last_modified_time(uint64_t p_time) { last_modified_time = p_time; }
uint64_t get_last_modified_time() const { return last_modified_time; }
// Used when saving pointers (saves a path property instead).
static String get_meta_pointer_property(const String &p_property);
SceneState();
};

View file

@ -212,6 +212,15 @@ Ref<PackedScene> ResourceLoaderText::_parse_node_tag(VariantParser::ResourcePars
type = SceneState::TYPE_INSTANCED; //no type? assume this was instantiated
}
HashSet<StringName> path_properties;
if (next_tag.fields.has("node_paths")) {
Vector<String> paths = next_tag.fields["node_paths"];
for (int i = 0; i < paths.size(); i++) {
path_properties.insert(paths[i]);
}
}
if (next_tag.fields.has("instance")) {
instance = packed_scene->get_state()->add_value(next_tag.fields["instance"]);
@ -276,9 +285,10 @@ Ref<PackedScene> ResourceLoaderText::_parse_node_tag(VariantParser::ResourcePars
}
if (!assign.is_empty()) {
int nameidx = packed_scene->get_state()->add_name(assign);
StringName assign_name = assign;
int nameidx = packed_scene->get_state()->add_name(assign_name);
int valueidx = packed_scene->get_state()->add_value(value);
packed_scene->get_state()->add_node_property(node_id, nameidx, valueidx);
packed_scene->get_state()->add_node_property(node_id, nameidx, valueidx, path_properties.has(assign_name));
//it's assignment
} else if (!next_tag.name.is_empty()) {
break;
@ -1941,6 +1951,7 @@ Error ResourceFormatSaverTextInstance::save(const String &p_path, const Ref<Reso
Ref<PackedScene> instance = state->get_node_instance(i);
String instance_placeholder = state->get_node_instance_placeholder(i);
Vector<StringName> groups = state->get_node_groups(i);
Vector<String> deferred_node_paths = state->get_node_deferred_nodepath_properties(i);
String header = "[node";
header += " name=\"" + String(name).c_escape() + "\"";
@ -1957,6 +1968,10 @@ Error ResourceFormatSaverTextInstance::save(const String &p_path, const Ref<Reso
header += " index=\"" + itos(index) + "\"";
}
if (deferred_node_paths.size()) {
header += " node_paths=" + Variant(deferred_node_paths).get_construct_string();
}
if (groups.size()) {
// Write all groups on the same line as they're part of a section header.
// This improves readability while not impacting VCS friendliness too much,