(Inspector Plugin) How can I "steal"/embed default Godot property editors within my own custom editor?/use EditorProperty<type>/EditorInspector

Godot Version

v4.3.dev6.official [89850d553]

How can I “steal”/embed default Godot property editors within my own custom editor?/use EditorProperty/EditorInspector

I’ve been going down the rabbit hole of godot’s @tool stuff and editor plugins, including custom editors for the inspector with EditorProperty.

What I would like to do is somehow find a way to use one of the default godot property editors within my own, for the cases in which I want to add a bunch of custom stuff to a property editor, but also would like to do something godot already implements and that I might be able to just use rather than re-implementing from scratch.

The issue is of course I can’t figure out how to do this. There seem to be a bunch of ‘EditorPropertyArray’, EditorPropertyWhatever that the inspector’s using, but after combing through the docs I can’t find any way to access them. EditorInspector seems to be the class of the inspector itself (maybe?), but it doesn’t seem to expose any actual functionality.

In short, I don’t know if they’re actual scenes or not but I’d like to do something equivalent to instancing the scene of some default godot editors inside my very own EditorProperty/EditorInspectorPlugin, so that I can use their functionality without having to remake them from scratch. Is this even possible, and if so, how?

There are a couple inspector controls exposed to scripting but the array or dictionary ones are not exposed (as far as I can tell). You have EditorSpinSlider, EditorResourcePicker and EditorScriptPicker for example. There may be more, I’m not sure.

1 Like

Hmm. That’s unfortunate. I’ve tried to use EditorResourcePicker, thinking that maybe, just maybe, I could for example make an EditorResourcePicker with a resource containing an array to get the default array control - but it really seems to be just the picker, I can’t seem to find a way to make it spit out default inspector editors.

Sooo I get the feeling most editors like arrays and dictionaries are not exposed in any way like you say and there’s no workarounds to get them either. Maybe somehow somebody knows of some obscure way to make it happen, but I’m not holding my breath, oh well. Thank you for the help though! Even if the answer was ‘not possible’

Somebody (Kas) helped me find this Expose more classes and functionality to editor plugins · Issue #300 · godotengine/godot-proposals · GitHub
Which contains the thing I wanted to do, and also pretty much 100% confirms it’s not possible to do it right now

What are you actually trying to do? Can you give a small example of what you want to do? There may be workarounds depending on what you want to achieve.

Oh there definitely are - I was mostly trying to make custom editors for some nested properties, like dictionaries in dictionaries or resources in resources. Godot’s default gets a bit difficult to use, and I wanted to make it easier by say for example, if I know my dict is only going to contain ints, strings, and other dicts, with strings always as the keys - make a specialized version in which I don’t need so many clicks to select types and to deal with the inspector becoming unwieldy after nesting too deep.

Being able to call in the default godot editors would be convenient for this, just add in the appropriate editor of the appropriate types and the actual stuff I need to implement myself shrinks by a whole ton.

I can def think of a few workarounds. var_to_str and str_to_var is one I’m currently using, just dump whatever I want to edit into a string, edit it as a string, then turn it back into the appropriate type. Nesting is a lot nicer, it’s faster to write {1:{“key”:“value”},2:{“key”:“value”}} than to do the same thing in the default dict inspector. Handles resources and alike poorly though, those are a lot harder to just write into a string (at least this way).

I was just looking for something “nicer” more aesthetically pleasing and less ad hoc, godot has some property editors already, and hey godot is (usually) pretty good about exposing the tools it itself uses! So I thought just using built in editors to help build my own out of would be a lot better. Possibly even eventually if I felt like making a tool just for its own sake make a nested resources editor that uses godot’s own editors for 90% of the work, but handles nesting the same way the node tree does, because that’s already a way to edit nested stuff and it feels a lot nicer than how the inspector handles nesting

But yeah there are definitely workarounds. Do feel free to share your own if you know of particularly effective ones too!

Heck if I want to handle nesting in a node-tree-like way, I can just make a script that turns an actual scene into a resource/dict for me according to some arbitrary rules or whatever! All of these are less “nice and polished” than stealing godot’s editors would’ve been though, and though I already spend 9000% more time fiddling with @tool than I should, re-implementing them from scratch is too much even for me. So less nice workarounds it is

Ideally you would write a custom property editor if you want full control over how to modify the data but you can do a lot with Object._get(), Object._set() and Object._get_property_list()

This is a somewhat convoluted and semi-working example. I ran out of time to fix a few issues but it can give you an idea:

@tool
class_name EnemyParty extends Resource

# This can come from another file
const ENEMY_LIST = ["Goblin", "Skeleton", "Bandit", "Orc", "Troll", "Spider"]

# A flag to find out if a property is stored to disk and it's a variable in a script
# this find the @export variables in a Object
const VAR_STORAGE = (PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_SCRIPT_VARIABLE)

# The party
var party:Dictionary = {}
# The size
var size:int = 0


func _get(property: StringName) -> Variant:
	if property == "Party Size":
		return size

	# If it start with Party/Enemy then it's a new enemy so return an empty string
	if property.begins_with("Party/Enemy "):
		return ""

	# If it start with Party/ then is an enemy, split the property path and get the stat value
	if property.begins_with("Party/"):
		var splits = property.split("/")
		var key = splits[1]
		var field = splits[2]
		return party[key].get(field)

	return null


func _set(property: StringName, value: Variant) -> bool:
	if property == "Party Size":
		size = value
		if size == 0:
			party.clear()
		notify_property_list_changed()
		return false

	# if we are setting a value for the property that begins with Party/Enemy
	# then it's a new enemy so create a CharStats with random stats and
	# add it to the dictionary
	if property.begins_with("Party/Enemy "):
		if not party.find_key(value):
			var stats = CharStats.new()
			stats.strength = randi_range(5, 40)
			stats.dexterity = randi_range(5, 40)
			stats.luck = randi_range(5, 40)
			stats.magic = randi_range(5, 40)
			stats.spirit = randi_range(5, 40)
			stats.vitality = randi_range(5, 40)
			party[value] = stats

		notify_property_list_changed()
		return true

	# If it begins with party then it's an enemy, set the value to the stat
	if property.begins_with("Party/"):
		var splits = property.split("/")
		var key = splits[1]
		var field = splits[2]
		party[key].set(field, value)
		notify_property_list_changed()
		return true

	return false


func _get_property_list() -> Array[Dictionary]:
	var result:Array[Dictionary] = []
	# The party variable, don't show it in the inspector and save it to disk
	result.push_back({
		"name": "party",
		"type": TYPE_DICTIONARY,
		"usage": PROPERTY_USAGE_NO_EDITOR,
	})
	# The size variable, don't show it in the inspector and save it to disk
	result.push_back({
		"name": "size",
		"type": TYPE_INT,
		"usage": PROPERTY_USAGE_NO_EDITOR,
	})
	# I'm using this property as a proxy to the size variable
	result.push_back({
		"name": "Party Size",
		"type": TYPE_INT,
		"hint_string": "0,15,or_greater,suffix:enemies",
		"usage": PROPERTY_USAGE_EDITOR
	})

	# iterate over the size
	for i in size:
		# if i is less than the party size
		if i < party.size():
			# get the key from the party
			var key = party.keys()[i]
			# get the properties that we need to show
			var properties = party[key].get_property_list().filter(func(p): return p.usage & VAR_STORAGE == VAR_STORAGE)
			# for each property
			for prop in properties:
				# Add a property Party/<key>/<stat_name>
				result.push_back({
					"name": "Party/%s/%s" % [key, prop.name],
					"type": prop.type,
					"usage": PROPERTY_USAGE_EDITOR
				})
		else:
			# else i is bigger than the party size so we need to show a new enemy selection
			# Fiter the enemy list removing the ones that where added before
			var list = ENEMY_LIST.filter(func(n): return not party.has(n))
			# Push a new property Party/Enemy <index>
			result.push_back({
				"name": "Party/Enemy %s" % (i+1),
				"type": TYPE_STRING,
				"hint": PROPERTY_HINT_ENUM_SUGGESTION,
				"hint_string": ",".join(list),
				"usage": PROPERTY_USAGE_EDITOR
			})

	return result

And the CharStats script:

class_name CharStats extends Resource

@export var strength := 0
@export var dexterity := 0
@export var vitality := 0
@export var magic := 0
@export var spirit := 0
@export var luck := 0

Result:

1 Like

Dear lord you made a whole example, wow! That’s honestly going way above and beyond. You’re also onto something here too - not exactly the same as just getting to actually steal godot’s default inspectors, but this seems like 99% of the way there!

I had initially dismissed ._get_property_list and related due to feeling like it was a huge hack, just lying to godot and making it think there are properties that don’t exist? Surely that’s not the proper way

But yeah no it seems to be extremely powerful. Look at that, subcategories, the options available change as you change the size, all of it uses godot’s default editors already too…! This seems to be a perfectly good way to squeeze default editors out of godot. Don’t get me wrong it would be nice to have access to them, but this essentially lets me do the ‘make my dictionaries/resources easier to edit’ AND not have to re-implement editors. The flexibility of this is still insane! And nothing stops me from combining it with custom property editors also.

I will experiment with this, it’s already giving me ideas. Thank you!