godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.11k stars 69 forks source link

Allow addons to contain more than one EditorPlugin #3782

Open Zylann opened 2 years ago

Zylann commented 2 years ago

Describe the project you are working on

Large addons such as a heightmap plugin and a voxel engine. Both of which come with multiple object types and many editors which are not tied to a single node or resource type.

Describe the problem or limitation you are having in your project

Script plugins are defined with a plugin.cfg file with plugin name, version, author etc... but only ONE script containing an EditorPlugin.

The problem is, in Godot, EditorPlugin seems more geared towards one node, or one resource type, sometimes with a few tweaks to handles a few extra cases or types. In my terrain plugin, there is more than one node, and more than one resource type. For now, I'm lucky enough, my heighmap plugin essentially keeps the same editors open for each node I have. But that still means my plugin.gd file has become a big monolithic mess. Adding more in the future is going to be tough.

When I think of my voxel C++ module, which I wish to become an addon too in the future (using GDExtension and maybe some GDScript), I defined 7 separate EditorPlugins inside. It just works. Because each handle a different feature provided by the same addon. I can't begin to imagine the pain of having to merge these all into a single EditorPlugin handling everything. It would become tedious to manage, and some things would become impossible.

We could think that a workaround would be to make a "multiplugin" class, with a list of sub-plugins inside, but it gets confusing quickly:

extends EditorPlugin

var _plugins = []

func add_sub_plugin(plugin: SubEditorPlugin):
    _plugins.append(plugin)

func handles(obj: Object) -> bool:
    for p in _plugins:
        if p.handles(obj):
            return true
    return false

func edit(obj: Object):
    for p in _plugins:
        if p.handles(obj):
            p.edit(obj)

func forward_spatial_gui_input(event, camera) -> bool:
    for p in _plugins:
        if p.has_method("forward_spatial_gui_input"):
            # TODO How do I know if that particular plugin should not have received input?
            # Maybe I would have to check if that plugin handles the currently selected object?
            # That's detailed logic from the editor I have to copy here, and I could be wrong
            if p.forward_spatial_gui_input(event, camera):
                return true
    return false

func forward_canvas_gui_input(event) -> bool:
    for p in _plugins:
        if p.has_method("forward_canvas_gui_input"):
            # TODO How do I know if that particular plugin should not have received input?
            # Maybe I would have to check if that plugin handles the currently selected object?
            # That's detailed logic from the editor I have to copy here, and I could be wrong
            if p.forward_canvas_gui_input(event):
                return true
    return false

func make_visible(visible: bool) -> bool:
    for p in _plugins:
        # TODO How do I handle the case where one plugin needs to hide and another to show?
        # It's editor logic I have no precise knowledge about, especially from a script.
        # Here we have no choice but to make ALL plugins show, or NONE.
        p.make_visible(visible)

func has_main_screen():
    # TODO Can't implement this. EditorPlugin can have only one main screen.
    # Without two proper EditorPlugin it would require scene tree hacks.
    return false

# And likely more

There could be other workarounds such as somehow shipping with multiple plugin.cfg, along with the confusion it would cause in ProjectSettings. But when I see adding multiple EditorPlugins in C++ just works with no added complexity, I wish this was simply also a given for addons.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

An addon (by that I also include GDExtensions, which can't even add editor plugins yet) should be allowed to contain zero, one, or more EditorPlugins. Just like modules. This allows to split large editor features in a more manageable way (as done already in C++), and unlocks some issues like the inability to have more than one main screen.

The term "plugin" can be confusing here: When I say "plugin", I mean a single entity downloaded from the asset library. We can call this an "addon" too. Technically, I see modules as the same thing. It's a removable addon. The only difference is the way it integrates (by compiling with the engine, as opposed to being a bunch of files or dll). On the other hand, "EditorPlugin" is different. EditorPlugin is a class. It is the concrete implementation of the editor for a particular feature. An addon should be allowed to have multiple features. The engine and modules gladly add one or more when they need to. Each gets called edit when necessary, become visible when necessary, process input when necessary, has a main screen when necessary. Conversely, an "addon" can come with no "EditorPlugin" at all in them (a side-effect of that leads to this nonsense). If zero plugins in an addon is allowed, there is no need to define an empty EditorPlugin.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

plugin.cfg, instead of containing a single plugin, could contain an array of them:

[plugin]

name="RPGMaker"
description="RPGMaker in Godot"
author="Dude"
version="1.0"
editor_plugins=[
   "room_editor_plugin.gd",
   "character_editor_plugin.gd",
   "achievements_editor_plugin.gd"
]

The editor may consider that file as the plugin eventually, so that when it is turned on or off, it can lookup the list of nodes to remove. Note: the presence of plugin.cfg would at least mean it can be turned on and off. If it has no plugins, there is still the case about class_name under addons/ not showing up if the corresponding addon is off.

Another way would be to define a new, broader class meant to have the same role as the register_types.cpp files seen in modules. It would register EditorPlugins and other unique things that are not necessarily EditorPlugins, so the latters could focus their code on actual editor features.

If this enhancement will not be used often, can it be worked around with a few lines of script?

Well you can choose to mix up everything inside a single EditorPlugin, but if it's a big one it will follow you during the whole development of your addon, eventually becoming a mess. And the uniqueness of some EditorPlugin features still cannot be solved. See also above a script attempt at encapsulating it.

Is there a reason why this should be core and not an add-on in the asset library?

This relates to how the engine manages addons.

KoBeWi commented 8 months ago

It is already possible to register multiple EditorPlugins per addon, by nesting addons. If you have addon folder and put another addon in it, the editor will not detect it (it won't appear in project settings), but it can still be activated via script. Then in your top plugin you can do:

func _enable_plugin() -> void:
    EditorInterface.set_plugin_enabled("TopPlugin/NestedPlugin", true)

func _disable_plugin() -> void:
    EditorInterface.set_plugin_enabled("TopPlugin/NestedPlugin", false)