-
-
Notifications
You must be signed in to change notification settings - Fork 21.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add @export_tool_button
annotation for easily creating inspector buttons.
#96290
Conversation
If I may recommend, |
I wanted to provide all avenues of approach on the first draft of this as I wasn't 100% sure how acceptable the parameterization would be. I'm happy to drop it from this PR, just say when. |
I think both exposing undoredo and passing it as argument is a bit redundant. Though it's a bit tricky, because referring to EditorUndoRedoManager or EditorInterface in your script will break it once the project is exported. If you want a game script with helper tool button, your undoredo needs to be untyped. Which I guess would be a valid reason to pass it as argument 🤔 From what I see, the argument is optional, so it needs to be better documented.
Well, since #90130 exists, and will likely get merged sooner, I think you can remove the new method and write the docs as if the other PR was merged already. |
Putting this by 4.4 because the prior PR was by 4.4. |
c8a72c0
to
25376f4
Compare
This comment was marked as resolved.
This comment was marked as resolved.
607244e
to
b06e404
Compare
Thanks for the review @KoBeWi! I believe I tackled all of your comments, let me know if any of the changes missed the mark. Here is the updated documentation (it grows!): I think that tidies up the convoluted word order while still being at the correct verbosity to make sure people are aware of the caveats of exporting a scene with a Interestingly the way I plan to use this is actually to automatically remove the offending nodes (that are in the Maybe there are other ways to handle resolving editor only doodads. For now I think this solution suits me and is worth pushing. |
"non-tools build" is a confusing term. Exported project is always "non-tools".
My idea for this problem was recognizing "editor scripts" and warning the user if they are referenced outside addons folder. Automatically removing nodes is rather unexpected behavior. |
7f4b4bf
to
19cf6d9
Compare
Thanks for the review and suggestions @dalexeev! I've gone ahead and included the changes of Given these changes now force the explicit inclusion of the method name (as a part of the annotation, as a magic string) I would like to ask about how the team feel about something like this (not an exact implementation, just ideas) to help alleviate the magic string issue a little: diff --git a/modules/gdscript/gdscript_analyzer.cpp b/modules/gdscript/gdscript_analyzer.cpp
index 8fc2d62c07..41ea62e8b3 100644
--- a/modules/gdscript/gdscript_analyzer.cpp
+++ b/modules/gdscript/gdscript_analyzer.cpp
@@ -1603,13 +1603,18 @@ void GDScriptAnalyzer::resolve_annotation(GDScriptParser::AnnotationNode *p_anno
reduce_expression(argument);
- if (!argument->is_constant) {
+ Variant value;
+
+ if (argument->type == GDScriptParser::Node::IDENTIFIER) {
+ GDScriptParser::IdentifierNode *identifier = static_cast<GDScriptParser::IdentifierNode*>(p_annotation->arguments[i]);
+ value = identifier->name;
+ } else if (argument->is_constant) {
+ value = argument->reduced_value;
+ } else {
push_error(vformat(R"(Argument %d of annotation "%s" isn't a constant expression.)", i + 1, p_annotation->name), argument);
return;
}
- Variant value = argument->reduced_value;
-
if (value.get_type() != argument_info.type) {
#ifdef DEBUG_ENABLED
if (argument_info.type == Variant::INT && value.get_type() == Variant::FLOAT) { to change, and permit, either of the following to now compile: Without this change the analyzer complains that the name of a function is not a constant expression, which I don't think is quite true. Perhaps this error, in the case of an identifier name, is unwarranted? Either way this PR is shaping up pretty nice! Thanks! |
This has been discussed before. Annotations support constant expressions, so treating an identifier as a quoteless string would be inconsistent and confusing. See:
Actually, it doesn't matter if it's a string literal or an "identifier". In both cases it's a string. As long as there are static checks, you shouldn't worry, it's just an aesthetic issue.
It's correct. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GDScript changes look good to me. But I have a question about the core/editor part. Why is it implemented as EditorInspectorPlugin
and not a new PROPERTY_HINT_TOOL_BUTTON
handled in editor_properties.cpp
? In my opinion, an explicit property hint is better than implicit handling of properties with Callable
type and empty value. It would allow to document hint format properly, for example for dynamic addition of tool buttons via _get_property_list()
.
Also, we could not limit tool buttons to GDScript, but add them to core, GDExtension and C# as well. Yes, it is possible with implicit Callable
handling, but explicit and documented property hint looks better.
#define ADD_TOOL_BUTTON(m_method, m_label, m_icon) ::ClassDB::add_property(get_class_static(), PropertyInfo(Variant::NIL, "", PROPERTY_HINT_TOOL_BUTTON, String(m_method) + String(m_label) + String(m_icon), PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_INTERNAL), StringName(), StringName())
Note that to fully dynamically add tool buttons we would need to be able to pass an argument from hint string to the function, since there is no _call()
to dynamically dispatch calls.
Alternatively, we could implement this as a more verbose option:
@export_tool_button("Click me") var click_button: Callable = click_action
func click_action():
print("Clicked!")
This would allow you to use lambdas:
@export_tool_button("Click me") var click_button: Callable = func ():
print("Clicked!")
modules/gdscript/tests/scripts/parser/errors/export_tool_button_bad_argument_type.gd
Outdated
Show resolved
Hide resolved
19cf6d9
to
7b3c01f
Compare
It would be good to somehow bind arguments to the called function, especially in the dynamic case to permit multiple functions to hit the same end-point. I am not sure how to do this in a satisfying fashion. In your comment, what argument is unable to be passed here normally?
Annotating a callable does appeal to me more than an otherwise magic method name string even if it is a little wordy. I might try changing it to do that instead. @dalexeev Is this what you meant by using property hints, in b53967b? My original use case was quite minimal, I just need to be able to trigger something cleanly in the editor, so when I salvaged this I just got it working to the point that I could use it for myself. I do like this, not using a property hint feels like an oversight. Code from the picture@tool
extends Node2D
@export var first:int = 123
@export_tool_button("test")
@export_tool_button("test_disabled")
@export var last:int = 42
func _validate_property(property: Dictionary) -> void:
if property.name == "@tool_button_test": # hide the test button
property.usage = property.usage & ~PROPERTY_USAGE_EDITOR
if property.name == "@tool_button_test_disabled":
property.usage = property.usage | PROPERTY_USAGE_READ_ONLY
func _get_property_list() -> Array[Dictionary]:
return [{
"name": "cool_dynamic_tool_button",
"type": TYPE_NIL,
"hint": PROPERTY_HINT_TOOL_BUTTON,
"hint_string": "dynamic_button",
}]
func test():
print("toot")
func test_disabled():
print("can't touch this")
func dynamic_button():
print("dynamic button") |
Unfortunately, I realized that this could be problematic.
I think something like this would work, although it doesn't allow you to bind arbitrary arguments, unlike @tool_button(method: StringName, text: String = "", icon: StringName = "", arg: String = "")
# hint string format: <method>[,<text>[,<icon>[,<arg>]]]
@export_tool_button(&"test", "Click me", &"", "123")
func test(_undo_redo, arg):
prints("Hello world!", arg) |
b53967b
to
d92eda0
Compare
d92eda0
to
cab3454
Compare
There are too many caveats to annotate I think I'm done with the changes necessary for property hint backing and dynamic addition of tool buttons! With the addition of the This now more than meets my own needs, let me know if I've missed/broken anything. @tool
extends Sprite2D
@export var first:int = 123
@export_tool_button(&"test_hidden")
@export_tool_button(&"test_disabled", "Disabled", &"Stop")
@export_tool_button(&"test_undoredo", "", &"UndoRedo")
@export_tool_button(&"set_modulate", "Make Green", &"ColorRect", "Color(0, 1, 0, 1)")
@export_tool_button(&"set_modulate", "Clear Modulation", &"Clear", "Color(1, 1, 1, 1)")
@export var last:int = 42
func _validate_property(property: Dictionary) -> void:
if property.name == "@tool_button_test_hidden": # hide the test button
property.usage = property.usage & ~PROPERTY_USAGE_EDITOR
if property.name == "@tool_button_test_disabled":
property.usage = property.usage | PROPERTY_USAGE_READ_ONLY
func _get_property_list() -> Array[Dictionary]:
var properties:Array[Dictionary]
for i in 3:
properties.append({
"name": "cool_dynamic_tool_button_%d" % i,
"type": TYPE_NIL,
"hint": PROPERTY_HINT_TOOL_BUTTON,
"hint_string": "test_dynamic,%s,Variant,Vector2(123, %d)" % ["Dynamic Button %d" % i, i],
})
return properties
func test_hidden():
print("toot")
func test_disabled():
print("can't touch this")
func test_undoredo(undo_redo:EditorUndoRedoManager):
prints("undoredo", undo_redo)
func test_dynamic(what:Variant):
prints("dynamic button", type_string(typeof(what)), typeof(what), what) |
@Macksaur Thank you for your patience! I'm sorry to make you do so much work, but I think the current version is overcomplicated, especially using I tried to change it to be as simple and functional as possible by removing unnecessary arguments and checks (since Godot is quite dynamic with callables). Please see dalexeev@0827064 and if you agree with this approach, feel free to rebase your PR branch using the commit. Updated testing script@tool
extends Sprite2D
@export var first: int = 123
@export_tool_button("Hidden") var hidden_action = test_hidden
@export_tool_button("Disabled", "Stop") var stop_action = test_disabled
@export_tool_button("UndoRedo", "UndoRedo") var undoredo_action = test_undoredo
@export_tool_button("Make Green", "ColorRect")
var make_green_action = set_self_modulate.bind(Color.GREEN)
@export_tool_button("Clear Modulation", "Clear")
var clear_modulation_action = set_self_modulate.bind(Color.WHITE)
@export var last: int = 42
func _validate_property(property: Dictionary) -> void:
if property.name == "hidden_action": # hide the test button
property.usage = property.usage & ~PROPERTY_USAGE_EDITOR
if property.name == "stop_action":
property.usage = property.usage | PROPERTY_USAGE_READ_ONLY
func _get_property_list() -> Array[Dictionary]:
var properties:Array[Dictionary]
for i in 3:
properties.append({
"name": "cool_dynamic_tool_button_%d" % i,
"type": TYPE_CALLABLE,
"hint": PROPERTY_HINT_TOOL_BUTTON,
"hint_string": "Dynamic Button %d" % i,
"usage": PROPERTY_USAGE_EDITOR,
})
return properties
func _get(property: StringName) -> Variant:
if property.begins_with("cool_dynamic_tool_button_"):
return test_dynamic.bind(property.trim_prefix("cool_dynamic_tool_button_"))
return null
func test_hidden():
print("toot")
func test_disabled():
print("can't touch this")
func test_undoredo():
prints("undoredo", EditorInterface.get_editor_undo_redo())
func test_dynamic(what):
prints("dynamic button", type_string(typeof(what)), typeof(what), what) Another example@tool
extends Node
@export_tool_button("Test") var test_action = test.bind(1)
func test(x):
print(x)
test_action = test.bind(x + 1) |
2c422d5
to
645a230
Compare
@tool_button
annotation for easily creating inspector buttons.@export_tool_button
annotation for easily creating inspector buttons.
645a230
to
7cc20ac
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. @Macksaur Thanks again for your work and patience!
…ttons Co-authored-by: jordi <creptthrust@gmail.com> Co-authored-by: K. S. Ernest (iFire) Lee <ernest.lee@chibifire.com> Co-authored-by: Mack <86566939+Macksaur@users.noreply.github.com>
7cc20ac
to
85dfd89
Compare
Thanks @Macksaur, @jordi-star, @fire, @dalexeev, and everyone involved in the thorough review and testing! |
I'm currently looking into bringing this to C# ✌️ |
Not sure how the workflow for documenting new feature works, but in case it wasn't considered I wanted to mention that these pages need to be updated with information about the new annotation: GDScript exported properties - https://docs.godotengine.org/en/latest/tutorials/scripting/gdscript/gdscript_exports.html |
Supersedes: #78355
Supersedes: #59289
Bugsquad edit: The syntax below is not the one that was merged in the end, see #96290 (comment) for updated examples.