Browse Source

Merge pull request #3867 from pycbouh/better-inspector-plugin

Improve Inspector plugin tutorial with a better example
Nathan Lovato 4 years ago
parent
commit
1fc736e5ae

BIN
tutorials/plugins/editor/img/inspector_plugin_example.png


+ 123 - 65
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
+------------------------------
+
+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()``.
 
-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.
+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
+--------------------------------------
+
+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``.
 
-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.
+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")
-
-
-    func _spin_changed(value):
+        # 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 _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.