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
  • Loading branch information
jordi-star committed Apr 27, 2023
1 parent 26fb911 commit ed533bb
Show file tree
Hide file tree
Showing 11 changed files with 226 additions and 5 deletions.
78 changes: 78 additions & 0 deletions editor/plugins/tool_button_editor_plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/**************************************************************************/
/* 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_node.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 = Object::cast_to<Script>(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_theme_icon(action_icon, SNAME("EditorIcons")));
}

void ToolButtonInspectorPlugin::call_action(Object *p_object, StringName p_method_name) {
if (!p_object->has_method(p_method_name)) {
print_error(vformat("Tool button method is invalid. Could not find method '%s' on %s", p_method_name, p_object->get_class_name()));
return;
}
p_object->call(p_method_name, EditorUndoRedoManager::get_singleton()->get_history_undo_redo(EditorUndoRedoManager::get_singleton()->get_history_id_for_object(p_object)));
}

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) {
if (p_usage.has_flag(PROPERTY_USAGE_INTERNAL) && p_usage.has_flag(PROPERTY_USAGE_EDITOR)) {
Button *action_button = EditorInspector::create_inspector_action_button(EditorPropertyNameProcessor::get_singleton()->process_name(p_path, EditorPropertyNameProcessor::STYLE_CAPITALIZED));

PackedStringArray split = p_hint_text.split(",");
if (split.size() > 2) {
String icon = split[2];
action_icon = StringName(icon);
action_button->connect(SNAME("theme_changed"), callable_mp(this, &ToolButtonInspectorPlugin::update_action_icon).bind(action_button));
}
add_custom_control(action_button);

action_button->connect(SNAME("pressed"), callable_mp(this, &ToolButtonInspectorPlugin::call_action).bind(p_object, split[1]));
return true;
}
}
return false;
}

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/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, 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 @@ -107,6 +107,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 @@ -203,6 +204,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
21 changes: 21 additions & 0 deletions modules/gdscript/doc_classes/@GDScript.xml
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,27 @@
[b]Note:[/b] As annotations describe their subject, the [code]@tool[/code] annotation must be placed before the class definition and inheritance.
</description>
</annotation>
<annotation name="@tool_button" qualifiers="vararg">
<return type="void" />
<param index="0" name="name" type="String" default="&quot;Tool Button&quot;" />
<param index="1" name="icon" type="String" default="&quot;&quot;" />
<description>
Mark a function to be displayed in the Inspector as a button. The [UndoRedo] associated with the object will be passed to the function when the button is clicked, so the function must have a parameter to accept it.
[codeblock]
@tool
extends Sprite2D

@tool_button("Randomize Color", "ColorRect")
func rand_col(history):
var random_color = Color(randf_range(0, 1.0), randf_range(0, 1.0), randf_range(0, 1.0));
history.create_action("Randomize Color");
history.add_do_property(self, "modulate", random_color);
history.add_undo_property(self, "modulate", modulate);
history.commit_action();
[/codeblock]
[b]Note:[/b] Your tool button method should make use of the [UndoRedo] passed to it, otherwise you won't be able to undo any changes you make.
</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 @@ -557,7 +557,8 @@ bool GDScript::_update_exports(bool *r_err, bool p_recursive_call, PlaceHolderSc
}
_signals[member.signal->identifier->name] = parameters_names;
} 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 @@ -1063,6 +1063,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 @@ -2404,7 +2404,8 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
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;
StringName name = annotation->export_info.name;

Expand All @@ -2415,6 +2416,7 @@ Error GDScriptCompiler::_populate_class_members(GDScript *p_script, const GDScri
PropertyInfo prop_info;
prop_info.name = name;
prop_info.usage = annotation->export_info.usage;
prop_info.type = annotation->export_info.type;
prop_info.hint_string = annotation->export_info.hint_string;

p_script->member_info[name] = 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 @@ -991,6 +991,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 @@ -2184,6 +2185,7 @@ static bool _guess_identifier_type_from_base(GDScriptParser::CompletionContext &
r_type.type.class_type = member.m_class;
r_type.type.is_meta_type = true;
return true;
case GDScriptParser::ClassNode::Member::TOOL_BUTTON:
case GDScriptParser::ClassNode::Member::GROUP:
return false; // No-op, but silences warnings.
case GDScriptParser::ClassNode::Member::UNDEFINED:
Expand Down
43 changes: 43 additions & 0 deletions modules/gdscript/gdscript_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ GDScriptParser::GDScriptParser() {
register_annotation(MethodInfo("@export_category", PropertyInfo(Variant::STRING, "name")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_CATEGORY>);
register_annotation(MethodInfo("@export_group", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_GROUP>, varray(""));
register_annotation(MethodInfo("@export_subgroup", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "prefix")), AnnotationInfo::STANDALONE, &GDScriptParser::export_group_annotations<PROPERTY_USAGE_SUBGROUP>, varray(""));
register_annotation(MethodInfo("@tool_button", PropertyInfo(Variant::STRING, "name"), PropertyInfo(Variant::STRING, "icon")), AnnotationInfo::FUNCTION, &GDScriptParser::tool_button_annotation, varray("", ""), true);
// Warning annotations.
register_annotation(MethodInfo("@warning_ignore", PropertyInfo(Variant::STRING, "warning")), AnnotationInfo::CLASS | AnnotationInfo::VARIABLE | AnnotationInfo::SIGNAL | AnnotationInfo::CONSTANT | AnnotationInfo::FUNCTION | AnnotationInfo::STATEMENT, &GDScriptParser::warning_annotations, varray(), true);
// Networking.
Expand Down Expand Up @@ -755,6 +756,9 @@ void GDScriptParser::parse_class_member(T *(GDScriptParser::*p_parse_function)()
}

for (AnnotationNode *&annotation : annotations) {
if (annotation->name == SNAME("@tool_button")) {
current_class->add_tool_button_member(annotation);
}
member->annotations.push_back(annotation);
}

Expand Down Expand Up @@ -4035,6 +4039,44 @@ bool GDScriptParser::warning_annotations(const AnnotationNode *p_annotation, Nod
#endif // DEBUG_ENABLED
}

bool GDScriptParser::tool_button_annotation(const AnnotationNode *p_annotation, Node *p_node) {
#ifndef TOOLS_ENABLED // !TOOLS_ENABLED
// Only available in editor.
return true;
#else // TOOLS_ENABLED
AnnotationNode *annotation = const_cast<AnnotationNode *>(p_annotation);
FunctionNode *func = static_cast<FunctionNode *>(p_node);
if (annotation->resolved_arguments.size() < 1) {
push_error("Tool buttons must specify a name.", p_annotation);
return false;
}

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

annotation->export_info.name = annotation->resolved_arguments[0];
annotation->export_info.type = Variant::Type::CALLABLE;
annotation->export_info.usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_INTERNAL;
if (func->parameters.size() != 1) {
push_error("Tool button methods must have an UndoRedo as their only argument.", func);
return false;
}
if (func->parameters[0]->datatype_specifier != nullptr && func->parameters[0]->datatype_specifier->type_chain[0]->name != "UndoRedo") {
push_error("Tool button methods must have an UndoRedo as their only argument.", func);
return false;
}
String hint_string = vformat("%s,%s", annotation->resolved_arguments[0], func->identifier->name);
if (annotation->resolved_arguments.size() > 1) {
// Icon.
hint_string += "," + annotation->resolved_arguments[1].operator String();
}
annotation->export_info.hint_string = hint_string;
return true;
#endif // TOOLS_ENABLED
}

bool GDScriptParser::rpc_annotation(const AnnotationNode *p_annotation, Node *p_node) {
ERR_FAIL_COND_V_MSG(p_node->type != Node::FUNCTION, false, vformat(R"("%s" annotation can only be applied to functions.)", p_annotation->name));

Expand Down Expand Up @@ -4543,6 +4585,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
18 changes: 15 additions & 3 deletions modules/gdscript/gdscript_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ class GDScriptParser {
ENUM,
ENUM_VALUE, // For unnamed enums.
GROUP, // For member grouping.
TOOL_BUTTON,
};

Type type = UNDEFINED;
Expand Down Expand Up @@ -565,6 +566,7 @@ class GDScriptParser {
case ENUM_VALUE:
return enum_value.identifier->name;
case GROUP:
case TOOL_BUTTON:
return annotation->export_info.name;
}
return "";
Expand All @@ -590,6 +592,8 @@ class GDScriptParser {
return "enum value";
case GROUP:
return "group";
case TOOL_BUTTON:
return "tool button";
}
return "";
}
Expand All @@ -611,6 +615,7 @@ class GDScriptParser {
case SIGNAL:
return signal->start_line;
case GROUP:
case TOOL_BUTTON:
return annotation->start_line;
case UNDEFINED:
ERR_FAIL_V_MSG(-1, "Reaching undefined member type.");
Expand All @@ -635,6 +640,7 @@ class GDScriptParser {
case SIGNAL:
return signal->get_datatype();
case GROUP:
case TOOL_BUTTON:
return DataType();
case UNDEFINED:
return DataType();
Expand All @@ -659,6 +665,7 @@ class GDScriptParser {
case SIGNAL:
return signal;
case GROUP:
case TOOL_BUTTON:
return annotation;
case UNDEFINED:
return nullptr;
Expand Down Expand Up @@ -696,8 +703,8 @@ class GDScriptParser {
type = ENUM_VALUE;
enum_value = p_enum_value;
}
Member(AnnotationNode *p_annotation) {
type = GROUP;
Member(AnnotationNode *p_annotation, Member::Type p_type) {
type = p_type;
annotation = p_annotation;
}
};
Expand Down Expand Up @@ -748,7 +755,11 @@ class GDScriptParser {
}
void add_member_group(AnnotationNode *p_annotation_node) {
members_indices[p_annotation_node->export_info.name] = members.size();
members.push_back(Member(p_annotation_node));
members.push_back(Member(p_annotation_node, Member::Type::GROUP));
}
void add_tool_button_member(AnnotationNode *p_annotation_node) {
members_indices[p_annotation_node->export_info.name] = members.size();
members.push_back(Member(p_annotation_node, Member::Type::TOOL_BUTTON));
}

ClassNode() {
Expand Down Expand Up @@ -1430,6 +1441,7 @@ class GDScriptParser {
template <PropertyUsageFlags t_usage>
bool export_group_annotations(const AnnotationNode *p_annotation, Node *p_target);
bool warning_annotations(const AnnotationNode *p_annotation, Node *p_target);
bool tool_button_annotation(const AnnotationNode *p_annotation, Node *p_node);
bool rpc_annotation(const AnnotationNode *p_annotation, Node *p_target);
// Statements.
Node *parse_statement();
Expand Down
Loading

0 comments on commit ed533bb

Please sign in to comment.