Retrieving object and property path from inspector

Godot Version

v4.5.1.stable

Question

I want to be able to edit properties from other nodes on root node. I have a bunch of GPUParticles and I don’t want to click and scroll every time.

I got pretty far with EditorInspectorPlugin and EditorProperty, but I can’t find a way to pass the specific object and property to EditorProperty function set_object_and_property.

Is there perhaps a way that I can add a button to property context menu or some other option?

You don’t need to use a plugin for that. You can use Object._get(), Object._set(), and Object._get_property_list() instead.

Example:

@tool
extends Node


@onready var sprite_2d: Sprite2D = $Sprite2D

var props_to_expose: Array[String] = ["texture", "offset", "centered"]


func _get(property: StringName) -> Variant:
	if not is_instance_valid(sprite_2d):
		return null

	if property.begins_with("other/"):
		var parts = property.split("/")
		if parts[1] in props_to_expose:
			return sprite_2d.get(parts[1])

	return null


func _set(property: StringName, value: Variant) -> bool:
	if not is_instance_valid(sprite_2d):
		return false

	if property.begins_with("other/"):
		var parts = property.split("/")
		if parts[1] in props_to_expose:
			sprite_2d.set(parts[1], value)
			return true

	return false


func _get_property_list() -> Array[Dictionary]:
	var props: Array[Dictionary]

	if is_instance_valid(sprite_2d):
		var other_props = sprite_2d.get_property_list()
		for other_prop in other_props:
			if other_prop.name in props_to_expose:
				props.append({
					"name": "other/%s" % other_prop.name,
					"type": other_prop.type,
					"hint": other_prop.hint,
					"hint_string": other_prop.hint_string,
					"usage": PROPERTY_USAGE_EDITOR,
				})

	return props
1 Like

Let me clarify what I’m asking. Let’s say that I have a scene like this:

– Node3D
—— GPUParticles3D
—— GPUParticles3D2
—— etc.

I want to have some properties and sub-properties of GPUParticles3D (i.e. properties from its process material) shown on Node3D. EditorInspectorPlugin lets me add EditorProperty controls and EditorProperty can edit a property given an object and a property name.

So, for example, by passing GPUParticles3D as object and lifetime as property to set_object_and_property function, I can edit this property from Node3D.

My problem is that I don’t want to manually type these object and property paths in some sort of plugin control, instead I want to add a more convient shortcut like how you can right click and favorite a property.

Update: I’ve also run into some issues with EditorProperty as well. While an editable property does show, the value seems to have nothing to do with what’s in GPUParticles3D and it seems that it’s trying to look for the property in the original node (Node3D). I think a viable option is to have a Node3D with custom get/set functions that redirect to referenced objects. But the problem of not having a shortcut to object/property paths still persists.

Inspector emits property_selected. Intercept it, check if alt (or some other hotkey) is pressed and re-create this property’s double in your main node.

1 Like

This does work for direct node properties (i.e. lifetime in GPUParticles3D), but not for sub-properties like properties of a mesh in draw pass 1.

I’m currently thinking of making changes to engine code and adding a menu item to where Copy Value, Favorite Property, etc. are.

There are no sub-properties. If you mean properties of resources then you can right click on the resource and select “Edit” and the resource object will become the edited object. Now the signal will catch clicks on its properties. Not as elegant as doing it nested but still better than manually entering names.

Not sure why they chose for the signal to ignore clicks on nested resource properties.

1 Like

Thank you, this solved the problem.

It turns out that I didn’t need an inspector plugin, a custom script for the root node did the trick. I provided custom functions for _get, _set and _get_property_list functions which handle various references added through property_selected signal.

One final problem is that I lose these references after closing and reopening a scene in editor. I don’t know if there’s such a thing as ‘object path’ so that I can store them as metadata and find them again when opening a scene.

Here it is for anyone that end up with a similar question:

# quick_access.gd

signal changed

class Reference:
	var object: Object
	var property: String
	var property_info: Dictionary
	var reference_name: String

var references: Array[Reference]

func toggle_reference(object: Object, property: String) -> void:
	if property.begins_with("quick_access_"):
		for ref in references:
			if ref.reference_name == property:
				references.erase(ref)
				return
	var order: int = 1
	for ref in references:
		if ref.object == object and ref.property == property:
			references.erase(ref)
			return
		if ref.property == property:
			order += 1
	var ref := Reference.new()
	ref.object = object
	ref.property = property
	var list := object.get_property_list()
	for p in list:
		if p["name"] == property:
			ref.property_info = p
	if order > 1:
		ref.reference_name = "quick_access_%s_%d" % [property, order]
	else:
		ref.reference_name = "quick_access_%s" % property
	references.push_back(ref)

func get_references_as_property_list() -> Array[Dictionary]:
	if references.is_empty():
		return []
	var list: Array[Dictionary] = [{
		"name": "Quick Access",
		"type": TYPE_NIL,
		"hint_string": "quick_access",
		"usage": PROPERTY_USAGE_GROUP,
	}]
	for ref in references:
		var prop := ref.property_info
		prop["name"] = ref.reference_name
		list.append(prop)
	return list

func setup() -> void:
	if Engine.is_editor_hint():
		EditorInterface.get_inspector().property_selected.connect(on_property_selected)

func on_property_selected(property: String) -> void:
	var obj := EditorInterface.get_inspector().get_edited_object()
	if !obj:
		return
	if Input.is_key_pressed(KEY_ALT) and Input.is_key_pressed(KEY_CTRL):
		toggle_reference(obj, property)
		changed.emit()

func get_reference(reference_name: String) -> Variant:
	for ref in references:
		if ref.reference_name == reference_name:
			return ref.object.get(ref.property)
	return null

func set_reference(reference_name: String, value: Variant) -> bool:
	for ref in references:
		if ref.reference_name == reference_name:
			ref.object.set(ref.property, value)
			return true
	return false
# root.gd

@tool
extends Node

var quick_access := preload("res://scripts/quick_access.gd").new()

func _ready() -> void:
	quick_access.setup()
	quick_access.changed.connect(on_quick_access_changed)

func _get_property_list() -> Array[Dictionary]:
	return quick_access.get_references_as_property_list()

func _get(property: StringName) -> Variant:
	var r = quick_access.get_reference(property)
	if r:
		return r
	return null

func _set(property: StringName, value: Variant) -> bool:
	if quick_access.set_reference(property, value):
		return true
	return false

func on_quick_access_changed():
	notify_property_list_changed()
1 Like

Perhaps utilize two NodePath objects. One leads to the node and the other is mis(used) to store the “path” from the node to the nested resource/property. You can even use a single plain string with some simple custom syntax like node/node/node.resource.resource.property, then parse this on startup and build the doubles. If you declare those paths as @export_storage (or set the equivalent usage flag via _get_property_list()), Godot will automatically save them with the scene so you won’t need to manually put them into metadata.

In any case just make sure you clean up properly whenever a doubled node/resource is deleted.

1 Like

@export_storageis a great idea!

That said, property_selected gives only an object and a property, so I’m not sure if there’s a better way to find the node that the object is stored in, other than going iterating through all nodes and their properties recursively.

It’d require a little bit of processing but it’s definitely doable. You won’t need to iterate through all if you track the state of the inspector at all times. There are several other signals that can help you with that although it would probably be best to do it as a plugin.

But even recursive bruteforce iteration wouldn’t be much of a problem as you only need to do it when creating a property double.