Improve Inspector plugin tutorial with a better example

This commit is contained in:
Yuri Sizov
2021-08-06 21:19:59 +03:00
committed by Hugo Locurcio
parent df18c298f2
commit cd510f8221
2 changed files with 121 additions and 63 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -3,55 +3,87 @@
Inspector plugins
=================
The inspector dock supports custom plugins to create your own widgets for
editing properties. This tutorial explains how to use the
:ref:`class_EditorInspectorPlugin` and :ref:`class_EditorProperty` classes to
write such plugins with the example of creating a custom value editor.
The inspector dock allows you to create custom widgets to edit properties
through plugins. This can be beneficial when working with custom datatypes and
resources, although you can use the feature to change the inspector widgets for
built-in types. You can design custom controls for specific properties, entire
objects, and even separate controls associated with particular datatypes.
Setup
-----
This guide explains how to use the :ref:`class_EditorInspectorPlugin` and
:ref:`class_EditorProperty` classes to create a custom interface for integers,
replacing the default behavior with a button that generates random values
between 0 and 99.
Just like :ref:`doc_making_plugins`, we start out by making a new plugin,
getting a ``plugin.cfg`` file created, and start with our
:ref:`class_EditorPlugin`. However, instead of using
``add_custom_node`` or ``add_control_to_dock`` we'll use
``add_inspector_plugin``.
.. figure:: img/inspector_plugin_example.png
:align: center
The default behavior on the left and the end result on the right.
Setting up your plugin
----------------------
Create a new empty plugin to get started.
.. seealso:: See :ref:`doc_making_plugins` guide to set up your new plugin.
Let's assume you've called your plugin folder ``my_inspector_plugin``. If so,
you should end up with a new ``addons/my_inspector_plugin`` folder that contains
two files: ``plugin.cfg`` and ``plugin.gd``.
As before, ``plugin.gd`` is a script extending :ref:`class_EditorPlugin` and you
need to introduce new code for its ``_enter_tree`` and ``_exit_tree`` methods.
To set up your inspector plugin, you must load its script, then create and add
the instance by calling ``add_inspector_plugin()``. If the plugin is disabled,
you should remove the instance you have added by calling
``remove_inspector_plugin()``.
.. note:: Here, you are loading a script and not a packed scene. Therefore you
should use ``new()`` instead of ``instance()``.
.. tabs::
.. code-tab:: gdscript GDScript
# MyEditorPlugin.gd
# plugin.gd
tool
extends EditorPlugin
var plugin
func _enter_tree():
# EditorInspectorPlugin is a resource, so we use `new()` instead of `instance()`.
plugin = preload("res://addons/MyPlugin/MyInspectorPlugin.gd").new()
plugin = preload("res://addons/my_inspector_plugin/MyInspectorPlugin.gd").new()
add_inspector_plugin(plugin)
func _exit_tree():
remove_inspector_plugin(plugin)
EditorInspectorPlugin
---------------------
To actually connect into the Inspector, we create a
:ref:`class_EditorInspectorPlugin` class. This script provides the "hooks" to
the inspector. Thanks to this class, the editor will call the functions within
the EditorInspectorPlugin while it goes through the process of building the UI
for the inspector. The script is used to check if we should enable ourselves for
any :ref:`class_Object` that is currently in the inspector (including any
:ref:`class_Resource` that is embedded!).
Interacting with the inspector
------------------------------
Once enabled, EditorInspectorPlugin has methods that allow for adding
:ref:`class_EditorProperty` nodes or just custom :ref:`class_Control` nodes to
the beginning and end of the inspector for that :ref:`class_Object`, or for
overriding or changing existing property editors.
To interact with the inspector dock, your ``MyInspectorPlugin.gd`` script must
extend the :ref:`class_EditorInspectorPlugin` class. This class provides several
virtual methods that affect how the inspector handles properties.
To have any effect at all, the script must implement the ``can_handle()``
method. This function is called for each edited :ref:`class_Object` and must
return ``true`` if this plugin should handle the object or its properties.
.. note:: This includes any :ref:`class_Resource` attached to the object.
You can implement four other methods to add controls to the inspector at
specific positions. The ``parse_begin()`` and ``parse_end()`` methods are called
only once at the beginning and the end of parsing for each object, respectively.
They can add controls at the top or bottom of the inspector layout by calling
``add_custom_control()``.
As the editor parses the object, it calls the ``parse_category()`` and
``parse_property()`` methods. There, in addition to ``add_custom_control()``,
you can call both ``add_property_editor()`` and
``add_property_editor_for_multiple_properties()``. Use these last two methods to
specifically add :ref:`class_EditorProperty`-based controls.
.. tabs::
.. code-tab:: gdscript GDScript
@@ -59,71 +91,97 @@ overriding or changing existing property editors.
# MyInspectorPlugin.gd
extends EditorInspectorPlugin
var RandomIntEditor = preload("res://addons/my_inspector_plugin/RandomIntEditor.gd")
func can_handle(object):
# Here you can specify which object types (classes) should be handled by
# this plugin. For example if the plugin is specific to your player
# class defined with `class_name MyPlayer`, you can do:
# `return object is MyPlayer`
# In this example we'll support all objects, so:
# We support all objects in this example.
return true
func parse_property(object, type, path, hint, hint_text, usage):
# We will handle properties of type integer.
# We handle properties of type integer.
if type == TYPE_INT:
# Register *an instance* of the custom property editor that we'll define next.
add_property_editor(path, MyIntEditor.new())
# We return `true` to notify the inspector that we'll be handling
# this integer property, so it doesn't need to parse other plugins
# (including built-in ones) for an appropriate editor.
# Create an instance of the custom property editor and register
# it to a specific property path.
add_property_editor(path, RandomIntEditor.new())
# Inform the editor to remove the default property editor for
# this property type.
return true
else:
return false
EditorProperty
--------------
Adding an interface to edit properties
--------------------------------------
Next, we define the actual :ref:`class_EditorProperty` custom value editor that
we want instantiated to edit integers. This is a custom :ref:`class_Control` and
we can add any kinds of additional nodes to make advanced widgets to embed in
the inspector.
The :ref:`class_EditorProperty` class is a special type of :ref:`class_Control`
that can interact with the inspector dock's edited objects. It doesn't display
anything but can house any other control nodes, including complex scenes.
There are three essential parts to the script extending
:ref:`class_EditorProperty`:
1. You must define the ``_init()`` method to set up the control nodes'
structure.
2. You should implement the ``update_property()`` to handle changes to the data
from the outside.
3. A signal must be emitted at some point to inform the inspector that the
control has changed the property using ``emit_changed``.
You can display your custom widget in two ways. Use just the default ``add_child()``
method to display it to the right of the property name, and use ``add_child()``
followed by ``set_bottom_editor()`` to position it below the name.
.. tabs::
.. code-tab:: gdscript GDScript
# MyIntEditor.gd
# RandomIntEditor.gd
extends EditorProperty
class_name MyIntEditor
# The main control for editing the property.
var property_control = Button.new()
# An internal value of the property.
var current_value = 0
# A guard against internal changes when the property is updated.
var updating = false
var spin = EditorSpinSlider.new()
func _init():
# We'll add an EditorSpinSlider control, which is the same that the
# inspector already uses for integer and float edition.
# If you want to put the editor below the property name, use:
# `set_bottom_editor(spin)`
# Otherwise to put it inline with the property name use:
add_child(spin)
# To remember focus when selected back:
add_focusable(spin)
# Setup the EditorSpinSlider
spin.set_min(0)
spin.set_max(1000)
spin.connect("value_changed", self, "_spin_changed")
# Add the control as a direct child of EditorProperty node.
add_child(property_control)
# Make sure the control is able to retain the focus.
add_focusable(property_control)
# Setup the initial state and connect to the signal to track changes.
property_control.text = "Value: " + str(current_value)
property_control.connect("pressed", self, "_on_button_pressed")
func _spin_changed(value):
func _on_button_pressed():
# Ignore the signal if the property is currently being updated.
if (updating):
return
emit_changed(get_edited_property(), value)
# Generate a new random integer between 0 and 99.
current_value = randi() % 100
property_control.text = "Value: " + str(current_value)
emit_changed(get_edited_property(), current_value)
func update_property():
# Read the current value from the property.
var new_value = get_edited_object()[get_edited_property()]
if (new_value == current_value):
return
# Update the control with the new value.
updating = true
spin.set_value(new_value)
current_value = new_value
property_control.text = "Value: " + str(current_value)
updating = false
Using the example code above you should be able to make a custom widget that
replaces the default :ref:`class_SpinBox` control for integers with a
:ref:`class_Button` that generates random values.