Editor Tools & Plugins

@tool makes a script run in the editor, so it can update visuals live as you edit properties.

35.1 @tool scripts

@tool makes a script run in the editor, so it can update visuals live as you edit properties.

Call update_configuration_warnings() when the condition changes. Be careful: @tool code that calls queue_free() or modifies the scene runs on your edited scene and can destroy work — commit often.

@tool
extends Node2D

@export var radius := 64.0:
	set(v):
		radius = v
		queue_redraw()
@export var color := Color.CYAN:
	set(v):
		color = v
		queue_redraw()

func _draw() -> void:
	draw_circle(Vector2.ZERO, radius, color)

func _process(delta: float) -> void:
	if Engine.is_editor_hint():
		return                          # editor-only branch: skip gameplay code
	rotation += delta

func _get_configuration_warnings() -> PackedStringArray:
	var warnings := PackedStringArray()
	if radius <= 0.0:
		warnings.append("Radius must be positive.")
	return warnings                     # shows a warning icon in the Scene dock

35.2 Dynamic properties

_get_property_list(), _get(property), and _set(property, value) let you create fully virtual properties.

@tool
extends Node

@export var use_custom_speed := false:
	set(v):
		use_custom_speed = v
		notify_property_list_changed()  # refresh the Inspector
@export var custom_speed := 100.0

func _validate_property(property: Dictionary) -> void:
	if property.name == "custom_speed" and not use_custom_speed:
		property.usage = PROPERTY_USAGE_NO_EDITOR    # hide unless enabled

35.3 EditorScript (one-off automation)

@tool
extends EditorScript

func _run() -> void:                     # File → Run in the script editor (Ctrl+Shift+X)
	var root := get_scene()
	for node in root.find_children("*", "Sprite2D"):
		node.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
	print("Updated sprites in ", root.name)

35.4 Editor plugins

Create via *Project Settings → Plugins → Create New Plugin*. It generates addons/my_plugin/plugin.cfg:

Other hooks: _has_main_screen() + _make_visible() for a main-screen tab, _handles(object) + _forward_canvas_gui_input() for viewport tools, add_control_to_bottom_panel(), add_import_plugin(), add_export_plugin().

[plugin]
name="My Plugin"
description="Adds a custom dock and node type."
author="You"
version="1.0"
script="plugin.gd"
# addons/my_plugin/plugin.gd
@tool
extends EditorPlugin

var dock: Control
var inspector_plugin: EditorInspectorPlugin

func _enter_tree() -> void:
	add_custom_type("Spawner", "Node2D", preload("spawner.gd"), preload("icon.svg"))
	dock = preload("dock.tscn").instantiate()
	add_control_to_dock(DOCK_SLOT_LEFT_UL, dock)
	inspector_plugin = preload("health_inspector.gd").new()
	add_inspector_plugin(inspector_plugin)
	add_tool_menu_item("Clean Unused Assets", _clean_assets)

func _exit_tree() -> void:
	remove_custom_type("Spawner")
	remove_control_from_docks(dock)
	dock.queue_free()
	remove_inspector_plugin(inspector_plugin)
	remove_tool_menu_item("Clean Unused Assets")

func _clean_assets() -> void:
	print(EditorInterface.get_edited_scene_root())

35.5 Custom inspectors

For full replacement editors, subclass EditorProperty, implement _update_property(), call emit_changed(get_edited_property(), new_value), and add it with add_property_editor(name, editor).

# health_inspector.gd
@tool
extends EditorInspectorPlugin

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

func _parse_property(object: Object, type: Variant.Type, name: String, hint_type: PropertyHint,
		hint_string: String, usage_flags: int, wide: bool) -> bool:
	if name == "max_health":
		var button := Button.new()
		button.text = "Set to 100"
		button.pressed.connect(func(): object.max_health = 100; object.notify_property_list_changed())
		add_custom_control(button)
	return false                        # false = keep the default editor too

35.6 Undo/redo in plugins

Share plugins through the Asset Library or Git submodules.

var ur := get_undo_redo()               # EditorUndoRedoManager
ur.create_action("Move nodes")
ur.add_do_property(node, "position", new_pos)
ur.add_undo_property(node, "position", node.position)
ur.commit_action()