Skip to content

Commit a11f970

Browse files
committed
Merge pull request godotengine#96290 from Macksaur/export-action-callable
Add `@export_tool_button` annotation for easily creating inspector buttons.
2 parents d6c0509 + 85dfd89 commit a11f970

File tree

14 files changed

+257
-10
lines changed

14 files changed

+257
-10
lines changed

core/core_constants.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,7 @@ void register_global_constants() {
677677
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_NODE_TYPE);
678678
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_HIDE_QUATERNION_EDIT);
679679
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_PASSWORD);
680+
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_TOOL_BUTTON);
680681
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_MAX);
681682

682683
BIND_CORE_BITFIELD_FLAG(PROPERTY_USAGE_NONE);

core/object/object.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ enum PropertyHint {
8787
PROPERTY_HINT_PASSWORD,
8888
PROPERTY_HINT_LAYERS_AVOIDANCE,
8989
PROPERTY_HINT_DICTIONARY_TYPE,
90+
PROPERTY_HINT_TOOL_BUTTON,
9091
PROPERTY_HINT_MAX,
9192
};
9293

doc/classes/@GlobalScope.xml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2933,7 +2933,15 @@
29332933
<constant name="PROPERTY_HINT_PASSWORD" value="36" enum="PropertyHint">
29342934
Hints that a string property is a password, and every character is replaced with the secret character.
29352935
</constant>
2936-
<constant name="PROPERTY_HINT_MAX" value="39" enum="PropertyHint">
2936+
<constant name="PROPERTY_HINT_TOOL_BUTTON" value="39" enum="PropertyHint">
2937+
Hints that a [Callable] property should be displayed as a clickable button. When the button is pressed, the callable is called. The hint string specifies the button text and optionally an icon from the [code]"EditorIcons"[/code] theme type.
2938+
[codeblock lang=text]
2939+
"Click me!" - A button with the text "Click me!" and the default "Callable" icon.
2940+
"Click me!,ColorRect" - A button with the text "Click me!" and the "ColorRect" icon.
2941+
[/codeblock]
2942+
[b]Note:[/b] A [Callable] cannot be properly serialized and stored in a file, so it is recommended to use [constant PROPERTY_USAGE_EDITOR] instead of [constant PROPERTY_USAGE_DEFAULT].
2943+
</constant>
2944+
<constant name="PROPERTY_HINT_MAX" value="40" enum="PropertyHint">
29372945
Represents the size of the [enum PropertyHint] enum.
29382946
</constant>
29392947
<constant name="PROPERTY_USAGE_NONE" value="0" enum="PropertyUsageFlags" is_bitfield="true">
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**************************************************************************/
2+
/* tool_button_editor_plugin.cpp */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
#include "tool_button_editor_plugin.h"
32+
33+
#include "scene/gui/button.h"
34+
35+
void EditorInspectorToolButtonPlugin::_update_action_icon(Button *p_action_button, const String &p_action_icon) {
36+
p_action_button->set_icon(p_action_button->get_editor_theme_icon(p_action_icon));
37+
}
38+
39+
void EditorInspectorToolButtonPlugin::_call_action(const Variant &p_object, const StringName &p_property) {
40+
Object *object = p_object.get_validated_object();
41+
ERR_FAIL_NULL_MSG(object, vformat(R"(Failed to get property "%s" on a previously freed instance.)", p_property));
42+
43+
const Variant value = object->get(p_property);
44+
ERR_FAIL_COND_MSG(value.get_type() != Variant::CALLABLE, vformat(R"(The value of property "%s" is %s, but Callable was expected.)", p_property, Variant::get_type_name(value.get_type())));
45+
46+
const Callable callable = value;
47+
ERR_FAIL_COND_MSG(!callable.is_valid(), vformat(R"(Tool button action "%s" is an invalid callable.)", callable));
48+
49+
Variant ret;
50+
Callable::CallError ce;
51+
callable.callp(nullptr, 0, ret, ce);
52+
ERR_FAIL_COND_MSG(ce.error != Callable::CallError::CALL_OK, vformat(R"(Error calling tool button action "%s": %s)", callable, Variant::get_call_error_text(callable.get_method(), nullptr, 0, ce)));
53+
}
54+
55+
bool EditorInspectorToolButtonPlugin::can_handle(Object *p_object) {
56+
return true;
57+
}
58+
59+
bool EditorInspectorToolButtonPlugin::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) {
60+
if (p_type != Variant::CALLABLE || p_hint != PROPERTY_HINT_TOOL_BUTTON || !p_usage.has_flag(PROPERTY_USAGE_EDITOR)) {
61+
return false;
62+
}
63+
64+
const PackedStringArray splits = p_hint_text.rsplit(",", true, 1);
65+
const String &hint_text = splits[0]; // Safe since `splits` cannot be empty.
66+
const String &hint_icon = splits.size() > 1 ? splits[1] : "Callable";
67+
68+
Button *action_button = EditorInspector::create_inspector_action_button(hint_text);
69+
action_button->set_auto_translate_mode(Node::AUTO_TRANSLATE_MODE_DISABLED);
70+
action_button->set_disabled(p_usage & PROPERTY_USAGE_READ_ONLY);
71+
action_button->connect(SceneStringName(theme_changed), callable_mp(this, &EditorInspectorToolButtonPlugin::_update_action_icon).bind(action_button, hint_icon));
72+
action_button->connect(SceneStringName(pressed), callable_mp(this, &EditorInspectorToolButtonPlugin::_call_action).bind(p_object, p_path));
73+
74+
add_custom_control(action_button);
75+
return true;
76+
}
77+
78+
ToolButtonEditorPlugin::ToolButtonEditorPlugin() {
79+
Ref<EditorInspectorToolButtonPlugin> plugin;
80+
plugin.instantiate();
81+
add_inspector_plugin(plugin);
82+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**************************************************************************/
2+
/* tool_button_editor_plugin.h */
3+
/**************************************************************************/
4+
/* This file is part of: */
5+
/* GODOT ENGINE */
6+
/* https://godotengine.org */
7+
/**************************************************************************/
8+
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
9+
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
10+
/* */
11+
/* Permission is hereby granted, free of charge, to any person obtaining */
12+
/* a copy of this software and associated documentation files (the */
13+
/* "Software"), to deal in the Software without restriction, including */
14+
/* without limitation the rights to use, copy, modify, merge, publish, */
15+
/* distribute, sublicense, and/or sell copies of the Software, and to */
16+
/* permit persons to whom the Software is furnished to do so, subject to */
17+
/* the following conditions: */
18+
/* */
19+
/* The above copyright notice and this permission notice shall be */
20+
/* included in all copies or substantial portions of the Software. */
21+
/* */
22+
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
23+
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
24+
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
25+
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
26+
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
27+
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
28+
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
29+
/**************************************************************************/
30+
31+
#ifndef TOOL_BUTTON_EDITOR_PLUGIN_H
32+
#define TOOL_BUTTON_EDITOR_PLUGIN_H
33+
34+
#include "editor/editor_inspector.h"
35+
#include "editor/plugins/editor_plugin.h"
36+
37+
class EditorInspectorToolButtonPlugin : public EditorInspectorPlugin {
38+
GDCLASS(EditorInspectorToolButtonPlugin, EditorInspectorPlugin);
39+
40+
void _update_action_icon(Button *p_action_button, const String &p_action_icon);
41+
void _call_action(const Variant &p_object, const StringName &p_property);
42+
43+
public:
44+
virtual bool can_handle(Object *p_object) override;
45+
virtual bool parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide = false) override;
46+
};
47+
48+
class ToolButtonEditorPlugin : public EditorPlugin {
49+
GDCLASS(ToolButtonEditorPlugin, EditorPlugin);
50+
51+
public:
52+
virtual String get_name() const override { return "ToolButtonEditorPlugin"; }
53+
54+
ToolButtonEditorPlugin();
55+
};
56+
57+
#endif // TOOL_BUTTON_EDITOR_PLUGIN_H

editor/register_editor_types.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
#include "editor/plugins/texture_region_editor_plugin.h"
128128
#include "editor/plugins/theme_editor_plugin.h"
129129
#include "editor/plugins/tiles/tiles_editor_plugin.h"
130+
#include "editor/plugins/tool_button_editor_plugin.h"
130131
#include "editor/plugins/version_control_editor_plugin.h"
131132
#include "editor/plugins/visual_shader_editor_plugin.h"
132133
#include "editor/plugins/voxel_gi_editor_plugin.h"
@@ -247,6 +248,7 @@ void register_editor_types() {
247248
EditorPlugins::add_by_type<TextureLayeredEditorPlugin>();
248249
EditorPlugins::add_by_type<TextureRegionEditorPlugin>();
249250
EditorPlugins::add_by_type<ThemeEditorPlugin>();
251+
EditorPlugins::add_by_type<ToolButtonEditorPlugin>();
250252
EditorPlugins::add_by_type<VoxelGIEditorPlugin>();
251253

252254
// 2D

modules/gdscript/doc_classes/@GDScript.xml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,41 @@
669669
[b]Note:[/b] Subgroups cannot be nested, they only provide one extra level of depth. Just like the next group ends the previous group, so do the subsequent subgroups.
670670
</description>
671671
</annotation>
672+
<annotation name="@export_tool_button">
673+
<return type="void" />
674+
<param index="0" name="text" type="String" />
675+
<param index="1" name="icon" type="String" default="&quot;&quot;" />
676+
<description>
677+
Export a [Callable] property as a clickable button with the label [param text]. When the button is pressed, the callable is called.
678+
If [param icon] is specified, it is used to fetch an icon for the button via [method Control.get_theme_icon], from the [code]"EditorIcons"[/code] theme type. If [param icon] is omitted, the default [code]"Callable"[/code] icon is used instead.
679+
Consider using the [EditorUndoRedoManager] to allow the action to be reverted safely.
680+
See also [constant PROPERTY_HINT_TOOL_BUTTON].
681+
[codeblock]
682+
@tool
683+
extends Sprite2D
684+
685+
@export_tool_button("Hello") var hello_action = hello
686+
@export_tool_button("Randomize the color!", "ColorRect")
687+
var randomize_color_action = randomize_color
688+
689+
func hello():
690+
print("Hello world!")
691+
692+
func randomize_color():
693+
var undo_redo = EditorInterface.get_editor_undo_redo()
694+
undo_redo.create_action("Randomized Sprite2D Color")
695+
undo_redo.add_do_property(self, &amp;"self_modulate", Color(randf(), randf(), randf()))
696+
undo_redo.add_undo_property(self, &amp;"self_modulate", self_modulate)
697+
undo_redo.commit_action()
698+
[/codeblock]
699+
[b]Note:[/b] The property is exported without the [constant PROPERTY_USAGE_STORAGE] flag because a [Callable] cannot be properly serialized and stored in a file.
700+
[b]Note:[/b] In an exported project neither [EditorInterface] nor [EditorUndoRedoManager] exist, which may cause some scripts to break. To prevent this, you can use [method Engine.get_singleton] and omit the static type from the variable declaration:
701+
[codeblock]
702+
var undo_redo = Engine.get_singleton(&amp;"EditorInterface").get_editor_undo_redo()
703+
[/codeblock]
704+
[b]Note:[/b] Avoid storing lambda callables in member variables of [RefCounted]-based classes (e.g. resources), as this can lead to memory leaks. Use only method callables and optionally [method Callable.bind] or [method Callable.unbind].
705+
</description>
706+
</annotation>
672707
<annotation name="@icon">
673708
<return type="void" />
674709
<param index="0" name="icon_path" type="String" />

modules/gdscript/gdscript_parser.cpp

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ GDScriptParser::GDScriptParser() {
122122
register_annotation(MethodInfo("@export_flags_avoidance"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_LAYERS_AVOIDANCE, Variant::INT>);
123123
register_annotation(MethodInfo("@export_storage"), AnnotationInfo::VARIABLE, &GDScriptParser::export_storage_annotation);
124124
register_annotation(MethodInfo("@export_custom", PropertyInfo(Variant::INT, "hint", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_CLASS_IS_ENUM, "PropertyHint"), PropertyInfo(Variant::STRING, "hint_string"), PropertyInfo(Variant::INT, "usage", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_CLASS_IS_BITFIELD, "PropertyUsageFlags")), AnnotationInfo::VARIABLE, &GDScriptParser::export_custom_annotation, varray(PROPERTY_USAGE_DEFAULT));
125+
register_annotation(MethodInfo("@export_tool_button", PropertyInfo(Variant::STRING, "text"), PropertyInfo(Variant::STRING, "icon")), AnnotationInfo::VARIABLE, &GDScriptParser::export_tool_button_annotation, varray(""));
125126
// Export grouping annotations.
126127
register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_CATEGORY>);
127128
register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_GROUP>, varray(""));
@@ -4618,10 +4619,10 @@ bool GDScriptParser::export_annotations(AnnotationNode *p_annotation, Node *p_ta
46184619
// For `@export_storage` and `@export_custom`, there is no need to check the variable type, argument values,
46194620
// or handle array exports in a special way, so they are implemented as separate methods.
46204621

4621-
bool GDScriptParser::export_storage_annotation(AnnotationNode *p_annotation, Node *p_node, ClassNode *p_class) {
4622-
ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name));
4622+
bool GDScriptParser::export_storage_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
4623+
ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name));
46234624

4624-
VariableNode *variable = static_cast<VariableNode *>(p_node);
4625+
VariableNode *variable = static_cast<VariableNode *>(p_target);
46254626
if (variable->is_static) {
46264627
push_error(vformat(R"(Annotation "%s" cannot be applied to a static variable.)", p_annotation->name), p_annotation);
46274628
return false;
@@ -4640,11 +4641,11 @@ bool GDScriptParser::export_storage_annotation(AnnotationNode *p_annotation, Nod
46404641
return true;
46414642
}
46424643

4643-
bool GDScriptParser::export_custom_annotation(AnnotationNode *p_annotation, Node *p_node, ClassNode *p_class) {
4644-
ERR_FAIL_COND_V_MSG(p_node->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name));
4644+
bool GDScriptParser::export_custom_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
4645+
ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name));
46454646
ERR_FAIL_COND_V_MSG(p_annotation->resolved_arguments.size() < 2, false, R"(Annotation "@export_custom" requires 2 arguments.)");
46464647

4647-
VariableNode *variable = static_cast<VariableNode *>(p_node);
4648+
VariableNode *variable = static_cast<VariableNode *>(p_target);
46484649
if (variable->is_static) {
46494650
push_error(vformat(R"(Annotation "%s" cannot be applied to a static variable.)", p_annotation->name), p_annotation);
46504651
return false;
@@ -4668,12 +4669,56 @@ bool GDScriptParser::export_custom_annotation(AnnotationNode *p_annotation, Node
46684669
return true;
46694670
}
46704671

4671-
template <PropertyUsageFlags t_usage>
4672-
bool GDScriptParser::export_group_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
4673-
if (p_annotation->resolved_arguments.is_empty()) {
4672+
bool GDScriptParser::export_tool_button_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
4673+
#ifdef TOOLS_ENABLED
4674+
ERR_FAIL_COND_V_MSG(p_target->type != Node::VARIABLE, false, vformat(R"("%s" annotation can only be applied to variables.)", p_annotation->name));
4675+
ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false);
4676+
4677+
if (!is_tool()) {
4678+
push_error(R"(Tool buttons can only be used in tool scripts (add "@tool" to the top of the script).)", p_annotation);
4679+
return false;
4680+
}
4681+
4682+
VariableNode *variable = static_cast<VariableNode *>(p_target);
4683+
4684+
if (variable->is_static) {
4685+
push_error(vformat(R"(Annotation "%s" cannot be applied to a static variable.)", p_annotation->name), p_annotation);
4686+
return false;
4687+
}
4688+
if (variable->exported) {
4689+
push_error(vformat(R"(Annotation "%s" cannot be used with another "@export" annotation.)", p_annotation->name), p_annotation);
46744690
return false;
46754691
}
46764692

4693+
const DataType variable_type = variable->get_datatype();
4694+
if (!variable_type.is_variant() && variable_type.is_hard_type()) {
4695+
if (variable_type.kind != DataType::BUILTIN || variable_type.builtin_type != Variant::CALLABLE) {
4696+
push_error(vformat(R"("@export_tool_button" annotation requires a variable of type "Callable", but type "%s" was given instead.)", variable_type.to_string()), p_annotation);
4697+
return false;
4698+
}
4699+
}
4700+
4701+
variable->exported = true;
4702+
4703+
// Build the hint string (format: `<text>[,<icon>]`).
4704+
String hint_string = p_annotation->resolved_arguments[0].operator String(); // Button text.
4705+
if (p_annotation->resolved_arguments.size() > 1) {
4706+
hint_string += "," + p_annotation->resolved_arguments[1].operator String(); // Button icon.
4707+
}
4708+
4709+
variable->export_info.type = Variant::CALLABLE;
4710+
variable->export_info.hint = PROPERTY_HINT_TOOL_BUTTON;
4711+
variable->export_info.hint_string = hint_string;
4712+
variable->export_info.usage = PROPERTY_USAGE_EDITOR;
4713+
#endif // TOOLS_ENABLED
4714+
4715+
return true; // Only available in editor.
4716+
}
4717+
4718+
template <PropertyUsageFlags t_usage>
4719+
bool GDScriptParser::export_group_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class) {
4720+
ERR_FAIL_COND_V(p_annotation->resolved_arguments.is_empty(), false);
4721+
46774722
p_annotation->export_info.name = p_annotation->resolved_arguments[0];
46784723

46794724
switch (t_usage) {

modules/gdscript/gdscript_parser.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,7 @@ class GDScriptParser {
15071507
bool export_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15081508
bool export_storage_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15091509
bool export_custom_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
1510+
bool export_tool_button_annotation(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15101511
template <PropertyUsageFlags t_usage>
15111512
bool export_group_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);
15121513
bool warning_annotations(AnnotationNode *p_annotation, Node *p_target, ClassNode *p_class);

0 commit comments

Comments
 (0)