godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.16k stars 97 forks source link

Add a way to export clickable actions to the inspector #2149

Closed Calinou closed 1 month ago

Calinou commented 3 years ago

See https://github.com/godotengine/godot/issues/9380 for previous discussion.

Describe the project you are working on

The Godot editor :slightly_smiling_face:

Describe the problem or limitation you are having in your project

There's no officially recognized/documented way to export a clickable action to the inspector. Such actions can be useful to expose "one-off" things you may need to do when using a node. This can be useful to expose additional functionality without having to create an editor plugin.

There's a hack you can use that works with boolean exported properties, but it's not particularly user-friendly.

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

Add a way to export proper clickable buttons in the Inspector, without exporting dummy properties.

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

Add an annotation that exports a method as a clickable action in the inspector. Like for property names, the action's name is named automatically based on the method name. For instance:

@export_func
func bake_all():
    pass

This would result in a clickable "Bake All" button in the inspector.

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

Yes (see above), but it's not particularly clean or user-friendly. A checkbox isn't an one-state button :slightly_smiling_face:

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

This is core editor functionality.

willnationsdev commented 3 years ago

How would this get communicated to the Inspector? Would you need to add a new METHOD_FLAG_* value to indicate to the Inspector that it should do that? Or are we going to add a proper get_annotations() or get_member_metadata() API for the Script interface so that the Editor can make use of that info?

Frontrider commented 3 years ago

I just wanted to open an issue about it, and I'd go for:

@export
func action():
    pass

or in Godot 3 GDScript:

export func action():
    pass

It should look like any other export, and it should throw an error if the exported function has arguments, because we do not want to/can't deal with that on a button.

If the editor can get the name and type of variables, get the name of the function then treating it as a "function type" should be doable (imo). Or if it needs to be a variable then this option can also work:

@export
var action = func():
    action_backend()

func action_backend():
    pass

In this case, if we can detect that this lambda has any number of arguments, then it might just inject as is into the existing export system.

fire-forge commented 2 years ago

I have a few ideas for the implementation of this proposal and I made a concept for it.

Inspector buttons should have 3 customizable options: color, icon, and text.

Syntax:

@export_func(color: Color or EditorInspectorPlugin.BUTTON_COLOR_*, icon: String, custom_text: String")

Concept:

@export_func button
@export_func(EditorInspectorPlugin.BUTTON_COLOR_ACCENT) accent_color_button
@export_func(Color("3cba53"), "ListSelect", "Click Me!") function_name
@export_func(Color("fc1532"), "Remove") remove_item
@export_func(Color("ab4fc2"), "AnimationPlayer", "Add AnimationPlayer") add_animation_player
@export_func(EditorInspectorPlugin.BUTTON_COLOR_DEFAULT, "res://icon.png") custom_icon

image image

I think this feature set would make inspector buttons highly customizable without making them too complex, since all of these settings are optional. Let me know if you have any suggestions or opinions on this :)

Frontrider commented 2 years ago

@fire-forge I think that level is the realm of an addon. The export itself should be as small and simple as possible.

Replacing an exported field with an arbitrary scene is already a feature of addons, applying it to an exported function should not be that difficult. Then you don't even have those limits that you'd impose to make it work as an annotation.

edit to the downvotes: I also wrote this as "this is what you could do until we have nothing else"

fire-forge commented 2 years ago

@Frontrider thanks for the feedback! I wasn't really sure what the scope this feature was, so I was just giving some suggestions for what I would do if I was working on this.

popcar2 commented 1 year ago

To anyone still interested in this coming in the future, the hacky way where you would create a button in the inspector for Godot 4 now uses this syntax:

@export var working_button: bool = false : set = run_code

func run_code(fake_bool = null):
    #Do stuff

Essentially this just creates a setter for working_button that throws away the value and instead runs your code.

Frontrider commented 1 year ago

So do the same as before :)

PoolloverNathan commented 1 year ago

My idea for the syntax, if this gets implemented (with my use case):


@export_button("Bake Textures", "Bakes the mask, fill, and outline textures into a new texture.")
# @icon("res://bake_textures.svg") # optional
func bake():
  # irrelevant
  return OK # can return an error, which will be shown to the user
nathanfranke commented 1 year ago

For the workaround, I usually prefer this syntax

@export var perform_action := false:
    set(_value):
        pass # ...
PoolloverNathan commented 1 year ago

TIL how to highlight gdscript in Github. That workaround is fine; however, it doesn't result in a button that looks like a button.

Frontrider commented 1 year ago

TIL how to highlight gdscript in Github. That workaround is fine; however, it doesn't result in a button that looks like a button.

Yeah. But that is the best we have for now.

This is the key problem for this, that needs to be kept in mind. https://github.com/godotengine/godot/pull/59289#issuecomment-1204320618

Shadowblitz16 commented 10 months ago

How would parameters work?

Calinou commented 10 months ago

How would parameters work?

There's no proposed way to supply parameters in this proposal, so you'd have to spawn a dialog that asks for a parameter yourself if you need that.

domske commented 2 months ago

If you are interessted in a lightweight Godot addon that turns your bool button-like checkboxes into real buttons. Just in place. Without changing the code logic or having dependencies. Then you could try out my addon.

Just download the addon and enable it in your project settings. Prefix your bool exports with btn_ and enjoy your buttons. Reload the scene (Scene > Reload Saved Scene) if you see no buttons.

@export var btn_update: bool:
  set(v): update()

func update():
  print("Button pressed")

How it works: In Godot you can export a bool variable, which will appear as a checkbox in the inspector. Then define a setter that ignores the value and only calls a function. Since a checkbox is confusing when expecting a button, I developed this addon to replace it with a real button. It detects buttons by the variable name prefix, which still allows regular checkboxes.

I still use the bool checkbox button solution, because this is currently the native way to implement button-like behavior in Godot inspector. If this addon is not present, you can still use it as a fallback like before. No changes needed. It simply replaces the checkbox with the button appearance.

This addon just replaces the bool fake button with a real button. No more, no less. There is currently no option for color or icon for compatibility reasons. But I already have some ideas for implementation. Also you have to reload the scene as you might already know when crafting tools in Godot. Just go to Scene > Reload Saved Scene. Maybe I can improve this for upcoming versions. Checkout my addon repository or download it from Godot Asset Library:

Simple Tool Button


Update v1.1.0: Now in Color! You can now use colors.

Update v1.2.0: Advanced Button! You can now use icons and more.

Update v1.3.0: You can now use multiple buttons in row.

See Godot Assets Library or my repository for more info and upcoming updates.

caphindsight commented 1 month ago

Why not simply

@export_tool_button
func my_function():
    pass

Why the complexity?

Also looking in 4.4dev3; the following pattern doesn't seem to be stable:

@export_tool_button
var my_function = func():
    pass

After project reload it works as expected, but change the code / add new tool buttons / etc., and it breaks until the next project reload.

dalexeev commented 1 month ago

Why the complexity?

In order to show a new element in the Inspector, we need to add a property, either explicitly or implicitly. The Inspector simply doesn't care about methods and they don't have hints/flags for that purpose. The original suggestion was to use a fake property, but this has conceptual and technical problems:

  1. You can't control the order of tool buttons, nest them in groups or subgroups. Buttons were always added to the end, even if you moved functions up. We could try to fix this, but it would still be against the GDScript style guide, which recommends putting functions after variables.
  2. It is possible to dynamically add properties via _get_property_list(), but you can't dynamically dispatch calls (like _call()), so the old approach didn't allow creating dynamic tool buttons, since you couldn't create a new method or pass an argument to differentiate between buttons.
  3. It is also possible to change existing property info via _validate_property(). The problem with a fake property is that you would need to know its internal name, which is generated implicitly (like @tool_button_{method_name}).
  4. Finally, annotations are resolved and applied quite late, when the abstract syntax tree has already been built. Adding new class members at this stage caused unexpected technical problems due to our implicit assumptions that this is no longer possible at this stage.

After project reload it works as expected, but change the code / add new tool buttons / etc., and it breaks until the next project reload.

Thanks for the bug report, this was already reported in the contributor chat. I have some guesses about the cause and fix for this bug, so I'll try to take a look.

nanto2016 commented 1 month ago

Sorry, I'm not very familiar with this platform but I am unsure why this issue has been close. Has it been implemented into the engine? Is it only going to make it in 4.4? Has it been closed to due to there being an addon that does exactly this (Simple Tool Button by domske)? I am trying to use @tool_button it in v4.3.stable and the error message I am getting is as follows:

Annotation "@tool_button" is not allowed in this level.

I have tried putting this piece of code before/after class_name, @icon, and extends, and it never worked:

@tool_button("Something")
func something():
    print("something")

Am I doing something wrong or is this simply not out yet?

Edit: apparently the error that I am getting, "Annotation "@[something]" is not allowed in this level" is also thrown when I put random scribblish as an annotation name.

dalexeev commented 1 month ago

@nanto2016 This is implemented for the upcoming 4.4, it doesn't exist in 4.3. As for the confusing error Annotation "%s" is not allowed in this level., we should first check if the annotation exists.