【Godot】Extending the Godot Editor with EditorPlugin (@tool, Custom Docks, Inspector)

Created: 2026-02-08

Learn how to extend the Godot editor using the @tool annotation and EditorPlugin class to add custom docks, toolbar buttons, and inspector plugins. Covers plugin.cfg setup to best practices.

Overview

Tested with: Godot 4.3+

The Godot editor is highly extensible -- using the EditorPlugin class, you can add custom docks and inspector panels. However, without a proper understanding of how the @tool annotation behaves and the plugin lifecycle works, you may encounter editor crashes or unexpected behavior.

This article walks through the process from basic plugin structure to implementing custom docks and inspector plugins for practical use.

@tool Annotation Basics

When implementing editor extensions in Godot, the first thing to understand is the @tool annotation. For example, imagine you want to visualize an attack range as a circle right in the editor -- that's where @tool comes in.

Adding @tool at the top of a script makes it run inside the editor as well. However, it's important to separate editor behavior from gameplay logic.

@tool
extends Node2D

func _process(delta):
    if Engine.is_editor_hint():
        # Runs only in the editor
        queue_redraw()
    else:
        # Runs during gameplay
        move_character(delta)

Important: Without branching via Engine.is_editor_hint(), game logic will execute inside the editor as well.

Here's a practical example combining @tool setters with _draw(). When you change radius in the inspector, the circle updates in real time in the editor.

Common uses for @tool:

Use CaseDescription
Custom drawingDisplay guides or previews in the editor with _draw()
Property previewUpdate preview in real time when @export values change
EditorPluginRequired for the plugin's main script
@tool
extends Sprite2D

@export var radius: float = 100.0:
    set(value):
        radius = value
        queue_redraw()  # Redraw when the value changes

func _draw():
    if Engine.is_editor_hint():
        draw_circle(Vector2.ZERO, radius, Color(0, 1, 0, 0.3))

plugin.cfg and EditorPlugin Setup

With @tool basics under your belt, let's move on to creating an actual plugin. To create a plugin, you need to follow a specific directory structure. Godot automatically detects this structure and displays it in the plugin settings panel.

Directory Structure

addons/
└── my_plugin/
    ├── plugin.cfg          # Plugin configuration file
    ├── my_plugin.gd        # EditorPlugin script
    └── dock/
        └── my_dock.tscn    # Custom dock scene (optional)

plugin.cfg

[plugin]

name="My Plugin"
description="A plugin that adds a custom dock"
author="Your Name"
version="1.0.0"
script="my_plugin.gd"

EditorPlugin Basics

@tool
extends EditorPlugin

func _enter_tree():
    # Called when the plugin is activated
    print("Plugin activated")

func _exit_tree():
    # Called when the plugin is deactivated
    # Always remove added UI elements here
    print("Plugin deactivated")

How to enable a plugin:

  1. Go to "Project" -> "Project Settings" -> "Plugins" tab
  2. Find your plugin in the list and check the enable checkbox

Adding a Custom Dock

With the plugin skeleton in place, let's add some real functionality. The most common use case is adding a custom panel (Dock) to the editor. For example, you could build a debug tool that lists all objects in a scene or a texture preview UI.

The following code adds a custom dock to the upper-left panel:

@tool
extends EditorPlugin

var dock: Control

func _enter_tree():
    dock = preload("res://addons/my_plugin/dock/my_dock.tscn").instantiate()
    add_control_to_dock(DOCK_SLOT_LEFT_UL, dock)

func _exit_tree():
    if dock:
        remove_control_from_docks(dock)
        dock.queue_free()

Dock Placement Slots

Slot ConstantPosition
DOCK_SLOT_LEFT_ULLeft panel, upper
DOCK_SLOT_LEFT_BLLeft panel, lower
DOCK_SLOT_RIGHT_ULRight panel, upper
DOCK_SLOT_RIGHT_BLRight panel, lower

Creating a Dock Scene

The content displayed in a dock can be created as a regular scene.

Use a Control node as the root and add the UI elements you need.

# dock/my_dock.gd
@tool
extends VBoxContainer

@onready var label = $StatusLabel
@onready var button = $RunButton

func _ready():
    button.pressed.connect(_on_run_pressed)

func _on_run_pressed():
    label.text = "Running..."
    # EditorInterface is directly accessible in EditorPlugin scripts.
    # For dock scripts, pass a reference from the EditorPlugin
    # or use signals to communicate back to the plugin.
    label.text = "Done"

Adding Toolbar Buttons

Docks are always-visible panels, but if you just need to trigger a specific action with a single click, a toolbar button is more lightweight. For example, a button that runs validation across all nodes in a scene is a great fit.

@tool
extends EditorPlugin

var button: Button

func _enter_tree():
    button = Button.new()
    button.text = "My Tool"
    button.pressed.connect(_on_button_pressed)
    add_control_to_container(CONTAINER_TOOLBAR, button)

func _exit_tree():
    if button:
        remove_control_from_container(CONTAINER_TOOLBAR, button)
        button.queue_free()

func _on_button_pressed():
    print("Toolbar button clicked")

Common Container Constants

ConstantLocation
CONTAINER_TOOLBARMain toolbar
CONTAINER_SPATIAL_EDITOR_MENU3D editor menu
CONTAINER_CANVAS_EDITOR_MENU2D editor menu
CONTAINER_INSPECTOR_BOTTOMBottom of the inspector

Custom Inspector Plugins

Following docks and toolbar buttons, the inspector is another powerful extension point. The inspector normally displays node properties automatically, but you can also add dedicated UI when specific nodes are selected. For instance, you could add a widget that displays speed values in a user-friendly way when a CharacterBody2D is selected.

First, register the inspector plugin from the EditorPlugin, then define the actual plugin class:

# my_plugin.gd
@tool
extends EditorPlugin

var inspector_plugin: MyInspectorPlugin

func _enter_tree():
    inspector_plugin = MyInspectorPlugin.new()
    add_inspector_plugin(inspector_plugin)

func _exit_tree():
    if inspector_plugin:
        remove_inspector_plugin(inspector_plugin)
# my_inspector_plugin.gd
@tool
class_name MyInspectorPlugin
extends EditorInspectorPlugin

func _can_handle(object: Object) -> bool:
    # Determine which objects this plugin handles
    return object is CharacterBody2D

func _parse_begin(object: Object):
    # Add UI elements at the top of the inspector
    var label = Label.new()
    label.text = "== Character Info =="
    add_custom_control(label)

func _parse_property(object, type, name, hint_type, hint_string, usage_flags, wide):
    if name == "speed":
        var label = Label.new()
        label.text = "Speed: %s" % str(object.get(name))
        add_custom_control(label)
        return true
    return false

Best Practices

Now that you've seen the key features, let's go over common pitfalls in plugin development. Plugin development has different considerations than regular GDScript. In particular, poor resource management can cause editor crashes or memory leaks.

TopicRecommendation
Null checksAlways check for null before removing elements in _exit_tree()
Editor/runtime separationUse Engine.is_editor_hint() to branch editor-only logic
Resource cleanupCall queue_free() on all added UI elements in _exit_tree()
Use preloadPreload scenes and resources with preload()
Error handlingUse push_warning() to notify users of issues

Common mistakes:

# BAD: Forgetting to free UI elements in _exit_tree()
func _exit_tree():
    pass  # Causes memory leaks or editor crashes

# GOOD: Always clean up
func _exit_tree():
    if dock:
        remove_control_from_docks(dock)
        dock.queue_free()
    if inspector_plugin:
        remove_inspector_plugin(inspector_plugin)

Summary

  • Adding @tool at the top of a script makes it run inside the editor
  • Use Engine.is_editor_hint() to branch editor-only logic
  • Implement _enter_tree() / _exit_tree() in the EditorPlugin class to set up your plugin
  • Add custom docks to left/right panels with add_control_to_dock()
  • Extend the inspector for specific nodes with EditorInspectorPlugin
  • Always clean up all added elements in _exit_tree() to prevent memory leaks

Further Reading