Skip to content

Commit

Permalink
Add @tool_button annotation for easily creating inspector buttons.
Browse files Browse the repository at this point in the history
Co-authored-by: K. S. Ernest (iFire) Lee <ernest.lee@chibifire.com>
Co-authored-by: Mack <86566939+Macksaur@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 4, 2024
1 parent fd7239c commit b5cd0f0
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 5 deletions.
98 changes: 98 additions & 0 deletions editor/plugins/tool_button_editor_plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**************************************************************************/
/* tool_button_editor_plugin.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#include "tool_button_editor_plugin.h"

#include "editor/editor_property_name_processor.h"
#include "editor/editor_undo_redo_manager.h"
#include "scene/gui/button.h"

bool ToolButtonInspectorPlugin::can_handle(Object *p_object) {
Ref<Script> scr = p_object->get_script();
return scr.is_valid() && scr->is_tool();
}

void ToolButtonInspectorPlugin::update_action_icon(Button *p_action_button) {
p_action_button->set_icon(p_action_button->get_editor_theme_icon(action_icon));
}

void ToolButtonInspectorPlugin::call_action(Object *p_object, const StringName &p_method_name) {
bool method_is_valid = false;
int method_arg_count = p_object->get_method_argument_count(p_method_name, &method_is_valid);

ERR_FAIL_COND_MSG(!method_is_valid, vformat("Tool button method is invalid. Could not find method '%s' on %s.", p_method_name, p_object->get_class_name()));

Variant undo_redo = EditorUndoRedoManager::get_singleton();

const Variant *args = nullptr;
int argc = 0;

// If the function takes arguments the first argument is always the EditorUndoRedoManager.
if (method_arg_count != 0) {
args = { &undo_redo };
argc = 1;
}

Callable::CallError ce;
p_object->callp(p_method_name, &args, argc, ce);
ERR_FAIL_COND_MSG(ce.error != Callable::CallError::CALL_OK, vformat("Error calling tool button method on %s: %s.", p_object->get_class_name(), Variant::get_call_error_text(p_method_name, &args, argc, ce)));
}

bool ToolButtonInspectorPlugin::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) {
if (p_type != Variant::CALLABLE || !p_usage.has_flag(PROPERTY_USAGE_EDITOR)) {
return false;
}

const PackedStringArray splits = p_hint_text.split(",");
ERR_FAIL_COND_V_MSG(splits.size() < 1, false, "Tool button annotations require a method to call.");
const String &method = splits[0];
const String &hint_text = splits.size() > 1 ? splits[1] : "";
const String &hint_icon = splits.size() > 2 ? splits[2] : "Callable";

String action_text = hint_text;
if (action_text.is_empty()) {
action_text = EditorPropertyNameProcessor::get_singleton()->process_name(method, EditorPropertyNameProcessor::STYLE_CAPITALIZED);
}

action_icon = hint_icon;

Button *action_button = EditorInspector::create_inspector_action_button(action_text);
action_button->connect(SceneStringName(theme_changed), callable_mp(this, &ToolButtonInspectorPlugin::update_action_icon).bind(action_button));
action_button->connect(SceneStringName(pressed), callable_mp(this, &ToolButtonInspectorPlugin::call_action).bind(p_object, method));

add_custom_control(action_button);
return true;
}

ToolButtonEditorPlugin::ToolButtonEditorPlugin() {
Ref<ToolButtonInspectorPlugin> plugin;
plugin.instantiate();
add_inspector_plugin(plugin);
}
57 changes: 57 additions & 0 deletions editor/plugins/tool_button_editor_plugin.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**************************************************************************/
/* tool_button_editor_plugin.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/

#ifndef TOOL_BUTTON_EDITOR_PLUGIN_H
#define TOOL_BUTTON_EDITOR_PLUGIN_H

#include "editor/editor_inspector.h"
#include "editor/plugins/editor_plugin.h"

class ToolButtonInspectorPlugin : public EditorInspectorPlugin {
GDCLASS(ToolButtonInspectorPlugin, EditorInspectorPlugin);

public:
StringName action_icon;
virtual bool can_handle(Object *p_object) override;
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;
void update_action_icon(Button *p_action_button);
void call_action(Object *p_object, const StringName &p_method_name);
};

class ToolButtonEditorPlugin : public EditorPlugin {
GDCLASS(ToolButtonEditorPlugin, EditorPlugin);

public:
virtual String get_name() const override { return "ToolButtonEditorPlugin"; }

ToolButtonEditorPlugin();
};

#endif // TOOL_BUTTON_EDITOR_PLUGIN_H
2 changes: 2 additions & 0 deletions editor/register_editor_types.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
#include "editor/plugins/texture_region_editor_plugin.h"
#include "editor/plugins/theme_editor_plugin.h"
#include "editor/plugins/tiles/tiles_editor_plugin.h"
#include "editor/plugins/tool_button_editor_plugin.h"
#include "editor/plugins/version_control_editor_plugin.h"
#include "editor/plugins/visual_shader_editor_plugin.h"
#include "editor/plugins/voxel_gi_editor_plugin.h"
Expand Down Expand Up @@ -242,6 +243,7 @@ void register_editor_types() {
EditorPlugins::add_by_type<TextureLayeredEditorPlugin>();
EditorPlugins::add_by_type<TextureRegionEditorPlugin>();
EditorPlugins::add_by_type<ThemeEditorPlugin>();
EditorPlugins::add_by_type<ToolButtonEditorPlugin>();
EditorPlugins::add_by_type<VoxelGIEditorPlugin>();

// 2D
Expand Down
28 changes: 28 additions & 0 deletions modules/gdscript/doc_classes/@GDScript.xml
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,34 @@
[b]Note:[/b] As annotations describe their subject, the [annotation @tool] annotation must be placed before the class definition and inheritance.
</description>
</annotation>
<annotation name="@tool_button">
<return type="void" />
<param index="0" name="text" type="String" default="&quot;&quot;" />
<param index="1" name="icon" type="StringName" default="&quot;&quot;" />
<description>
Mark a function to be displayed as a clickable button in the Inspector when running the editor.
If [param text] is specified it is used as the label text of the button. If [param text] is omitted, the label text is derived from the name of the annotated function and automatically capitalized.
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.
The first argument of the annotated function is optional and is the [EditorUndoRedoManager]. Consider using the optionally passed [EditorUndoRedoManager] to allow the action to be reverted safely. The code snippet below demonstrates this:
[codeblock]
@tool
extends Sprite2D

@tool_button
func hello():
print("Hello world!")

@tool_button("Randomize the color!", "ColorRect")
func randomize_color(undo_redo):
undo_redo.create_action("Randomized Sprite2D Color")
undo_redo.add_do_property(self, "self_modulate", Color(randf(), randf(), randf()))
undo_redo.add_undo_property(self, "self_modulate", self_modulate)
undo_redo.commit_action()
[/codeblock]
[b]Note:[/b] When implementing undo/redo make sure to provide separate [code]do[/code] and [code]undo[/code] methods that perform and revert the action respectively.
[b]Note:[/b] In an exported project neither [EditorInterface] nor [EditorUndoRedoManager] exist, which may cause some scripts to break. To prevent this, change the [code]undo_redo[/code] object's type to [Variant] or omit the static type from the declaration.
</description>
</annotation>
<annotation name="@warning_ignore" qualifiers="vararg">
<return type="void" />
<param index="0" name="warning" type="String" />
Expand Down
3 changes: 2 additions & 1 deletion modules/gdscript/gdscript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,8 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call, PlaceHolderSc
case GDScriptParser::ClassNode::Member::SIGNAL: {
_signals[member.signal->identifier->name] = member.signal->method_info;
} break;
case GDScriptParser::ClassNode::Member::GROUP: {
case GDScriptParser::ClassNode::Member::GROUP:
case GDScriptParser::ClassNode::Member::TOOL_BUTTON: {
members_cache.push_back(member.annotation->export_info);
} break;
default:
Expand Down
1 change: 1 addition & 0 deletions modules/gdscript/gdscript_analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1187,6 +1187,7 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
}
break;
case GDScriptParser::ClassNode::Member::GROUP:
case GDScriptParser::ClassNode::Member::TOOL_BUTTON:
// No-op, but needed to silence warnings.
break;
case GDScriptParser::ClassNode::Member::UNDEFINED:
Expand Down
4 changes: 3 additions & 1 deletion modules/gdscript/gdscript_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2849,7 +2849,8 @@ Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptP
p_script->constants.insert(name, enum_n->dictionary);
} break;

case GDScriptParser::ClassNode::Member::GROUP: {
case GDScriptParser::ClassNode::Member::GROUP:
case GDScriptParser::ClassNode::Member::TOOL_BUTTON: {
const GDScriptParser::AnnotationNode *annotation = member.annotation;
// Avoid name conflict. See GH-78252.
StringName name = vformat("@group_%d_%s", p_script->members.size(), annotation->export_info.name);
Expand All @@ -2861,6 +2862,7 @@ Error GDScriptCompiler::_prepare_compilation(GDScript *p_script, const GDScriptP
PropertyInfo prop_info;
prop_info.name = annotation->export_info.name;
prop_info.usage = annotation->export_info.usage;
prop_info.type = annotation->export_info.type;
prop_info.hint_string = annotation->export_info.hint_string;
minfo.property_info = prop_info;

Expand Down
2 changes: 2 additions & 0 deletions modules/gdscript/gdscript_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,7 @@ static void _find_identifiers_in_class(const GDScriptParser::ClassNode *p_class,
option = ScriptLanguage::CodeCompletionOption(member.signal->identifier->name, ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL, location);
break;
case GDScriptParser::ClassNode::Member::GROUP:
case GDScriptParser::ClassNode::Member::TOOL_BUTTON:
break; // No-op, but silences warnings.
case GDScriptParser::ClassNode::Member::UNDEFINED:
break;
Expand Down Expand Up @@ -2424,6 +2425,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
r_type.type.is_meta_type = true;
return true;
case GDScriptParser::ClassNode::Member::GROUP:
case GDScriptParser::ClassNode::Member::TOOL_BUTTON:
return false; // No-op, but silences warnings.
case GDScriptParser::ClassNode::Member::UNDEFINED:
return false; // Unreachable.
Expand Down
39 changes: 39 additions & 0 deletions modules/gdscript/gdscript_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ GDScriptParser::GDScriptParser() {
register_annotation(MethodInfo("@icon", PropertyInfo(Variant::STRING, "icon_path")), AnnotationInfo::SCRIPT, &GDScriptParser::icon_annotation);
register_annotation(MethodInfo("@static_unload"), AnnotationInfo::SCRIPT, &GDScriptParser::static_unload_annotation);

register_annotation(MethodInfo("@tool_button", PropertyInfo(Variant::STRING, "text"), PropertyInfo(Variant::STRING_NAME, "icon")), AnnotationInfo::FUNCTION, &GDScriptParser::tool_button_annotation, varray("", ""));

register_annotation(MethodInfo("@onready"), AnnotationInfo::VARIABLE, &GDScriptParser::onready_annotation);
// Export annotations.
register_annotation(MethodInfo("@export"), AnnotationInfo::VARIABLE, &GDScriptParser::export_annotations<PROPERTY_HINT_NONE, Variant::NIL>);
Expand Down Expand Up @@ -935,6 +937,9 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)(b
#endif // TOOLS_ENABLED

for (AnnotationNode *&annotation : annotations) {
if (annotation->name == SNAME("@tool_button")) {
current_class->add_tool_button_member(annotation);
}
member->annotations.push_back(annotation);
#ifdef TOOLS_ENABLED
if (annotation->start_line <= doc_comment_line) {
Expand Down Expand Up @@ -4157,6 +4162,39 @@ bool GDScriptParser::onready_annotation(AnnotationNode *p_annotation, Node *p_ta
return true;
}

bool GDScriptParser::tool_button_annotation(AnnotationNode *p_annotation, Node *p_node, ClassNode *p_class) {
#ifdef TOOLS_ENABLED
AnnotationNode *annotation = const_cast<AnnotationNode *>(p_annotation);
FunctionNode *func = static_cast<FunctionNode *>(p_node);

if (!is_tool()) {
push_error("Tool buttons can only be used in tool scripts.", p_annotation);
return false;
}

annotation->export_info.name = "__tool_button_" + func->identifier->name;
annotation->export_info.type = Variant::Type::CALLABLE;
annotation->export_info.usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_INTERNAL;

if (func->parameters.size() >= 1 && func->parameters[0]->datatype_specifier != nullptr && func->parameters[0]->datatype_specifier->type_chain[0]->name != "EditorUndoRedoManager") {
push_error("Tool button methods should have their first argument typed as an EditorUndoRedoManager.", func);
return false;
}

// Build the hint string (format: "<method>","<text>","[icon]")
String hint_string = func->identifier->name;
if (annotation->resolved_arguments.size() > 0) {
hint_string += "," + annotation->resolved_arguments[0].operator String(); // Button text.
}
if (annotation->resolved_arguments.size() > 1) {
hint_string += "," + annotation->resolved_arguments[1].operator String(); // Button icon.
}
annotation->export_info.hint_string = hint_string;
#endif // TOOLS_ENABLED

return true; // Only available in editor.
}

static String _get_annotation_error_string(const StringName &p_annotation_name, const Vector<Variant::Type> &p_expected_types, const GDScriptParser::DataType &p_provided_type) {
Vector<String> types;
for (int i = 0; i < p_expected_types.size(); i++) {
Expand Down Expand Up @@ -5277,6 +5315,7 @@ void GDScriptParser::TreePrinter::print_class(ClassNode *p_class) {
case ClassNode::Member::ENUM_VALUE:
break; // Nothing. Will be printed by enum.
case ClassNode::Member::GROUP:
case ClassNode::Member::TOOL_BUTTON:
break; // Nothing. Groups are only used by inspector.
case ClassNode::Member::UNDEFINED:
push_line("<unknown member>");
Expand Down
Loading

0 comments on commit b5cd0f0

Please sign in to comment.