diff --git a/tutorials/plugins/editor/img/inspector_plugin_example.png b/tutorials/plugins/editor/img/inspector_plugin_example.png new file mode 100644 index 000000000..774302b99 Binary files /dev/null and b/tutorials/plugins/editor/img/inspector_plugin_example.png differ diff --git a/tutorials/plugins/editor/inspector_plugins.rst b/tutorials/plugins/editor/inspector_plugins.rst index 84c7504ed..dace101ee 100644 --- a/tutorials/plugins/editor/inspector_plugins.rst +++ b/tutorials/plugins/editor/inspector_plugins.rst @@ -3,54 +3,86 @@ 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 @@ -58,71 +90,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 the default ``add_child()`` +method to display it to the right of the property name, and +``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.