Editing a sub-resource within an EditorProperty

Godot Version

4.2.1

Question

I’m currently working through making an Inspector Plugin that allows for custom editing of my own Resource types. I’ve already gotten one working. Let’s call it Money, and it has a single exposed property. Now, I have a Resource that contains an array of things, amongst which is a Money. What I’m trying to do is make Money “transparent” in the inspector, such that it shows up alongside its other properties, rather than with the little sub-prompt. I’ve included a picture which kinda shows what I mean. “Price 2” has two elements, because I left the default one in for clarity. What I want is the top one, which I’ve achieved, except I can’t seem to find a way to save the value – it never sticks (i.e. the value is never actually set in the resource).

image

As I mentioned, I already wrote a Money inspector editor, in which the key lines are:

func _on_value_changed(value: float) -> void:
	emit_changed(get_edited_property(), value)

In my new editor, I tried something like (hard-coded just to rule out weirdness):

func _on_value_changed(value: float) -> void:
    emit_changed("price2", value, "_value", false)

…but that didn’t work. Directly setting the value did work, but I get the impression I’m not supposed to that:

func _on_value_changed(value: float) -> void:
    get_edited_object()[get_edited_property()]._value = value

However, because the object I’m changing isn’t the one being edited, I’m having trouble making it work. I feel like I should be able to somehow just drop in my Money EditorProperty directly and have it just work.

I don’t have a direct answer for your issue but for what you want to achieve a custom inspector plugin is not necessary.

You can expose internal values to the inspector by implementing the functions Object._get(), Object._set(), and Object._get_property_list() and make the script a @tool script.

For example:

@tool
class_name MyResource extends Resource

var money:Money = Money.new()


func _get(property: StringName) -> Variant:
	# redirect the "price" property to money.price
	match property:
		"price":
			return money.price

	return null


func _set(property: StringName, value: Variant) -> bool:
	# redirect the "price" property to money.price
	match property:
		"price":
			money.price = value
			return true

	return false


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

	# set the money property as storage so it gets serialized to disk
	properties.push_back({
		"name": "money",
		"type": TYPE_OBJECT,
		"usage": PROPERTY_USAGE_STORAGE
	})

	# create our price property that will only be used in the editor and
	# it won't be serialized to disk
	properties.push_back({
		"name": "price",
		"type": TYPE_FLOAT,
		"hint": PROPERTY_HINT_RANGE,
		"hint_string": "0,1000,0.01,or_greater,suffix:$",
		"usage": PROPERTY_USAGE_EDITOR
	})

	return properties

Money is just:

class_name Money extends Resource

@export var price:float = 4.00
1 Like

First off, thanks for bringing in the lower-tech solution. I do consider inspector plugins a last resort, and I hadn’t considered this for my use-case.

With that in mind, I’m trying to piece together how to do what I’d like to do now. My Money class is a little more complex, and I’ve already written a custom inspector plugin for that (which you helped me with in another thread). That comes with a custom SpinBox, and works well enough if I create a resource on its own.

So your example above will get me a standard range display for that passthrough, but is it possible to combine these two things? In other words, can I use _get_property_list() to display my custom MoneySpinBox (that extends SpinBox)?

I’ve not tried it but it should be possible, yes.

I did a quick and dirty test and it worked fine:

@tool
extends EditorPlugin


var my_inspector_plugin:MyInspectorPlugin

func _enter_tree() -> void:
	my_inspector_plugin = MyInspectorPlugin.new()
	add_inspector_plugin(my_inspector_plugin)



func _exit_tree() -> void:
	# Clean up nodes
	if is_instance_valid(my_inspector_plugin):
		remove_inspector_plugin(my_inspector_plugin)
		my_inspector_plugin = null


class MyInspectorPlugin extends EditorInspectorPlugin:


	func _can_handle(object: Object) -> bool:
		return object is MyResource

		
	func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint, hint_string: String, usage_flags: int, wide: bool) -> bool:
		if name == "price":
			var control = MyEditorProperty.new()
			control.text_changed.connect(func(text:String): object.set(name, float(text)))
			control.setup(object.get(name))
			add_property_editor(name, control)
			return true

		return false
		

class MyEditorProperty extends EditorProperty:


	signal text_changed(text:String)
	
	var line_edit:LineEdit = LineEdit.new()
	
	
	func _enter_tree() -> void:
		line_edit.text_changed.connect(func(text:String): text_changed.emit(text))
		add_child(line_edit)

		
	func setup(value:float) -> void:
		if not is_node_ready():
			await ready
		line_edit.text = str(value)

		
	func _exit_tree() -> void:
		remove_child(line_edit)

1 Like

Interesting. So the magic bits there are the signals, and then object.set() on that custom property. That looks quite similar to the effect I thought I “wasn’t supposed to do” in my initial post, but you seem to know a lot better than I do, so perhaps it’s fine after all.

Once I can take a step back, I’m going to better evaluate your suggestion using _get, _set, and _get_property_list. I’m doing some funky stuff in my inspector plugin that I can probably at the very least offload and abstract behind that facade.

One interesting thing I noted that deviates from the limited code examples the docs give: you used _enter_tree() instead of _init() in the editor property. Is that important, or more personal preference?

Well, you need to update the value somehow. Looks like you need to call emit_changed() after modifying the value too. At least the documentation says so (probably so the inspector knows that something changed and updates the other properties if needed) I missed that in my test.

Not really, I just didn’t try with _init() but it should be fine either way.