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 26, 2024
1 parent 2be730a commit 19cf6d9
Show file tree
Hide file tree
Showing 21 changed files with 302 additions and 32 deletions.
99 changes: 99 additions & 0 deletions editor/plugins/tool_button_editor_plugin.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**************************************************************************/
/* 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->set_auto_translate_mode(Node::AUTO_TRANSLATE_MODE_DISABLED);
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 @@ -127,6 +127,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 @@ -247,6 +248,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
30 changes: 30 additions & 0 deletions modules/gdscript/doc_classes/@GDScript.xml
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,36 @@
[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.
</description>
</annotation>
<annotation name="@export_tool_button">
<return type="void" />
<param index="0" name="method" type="StringName" />
<param index="1" name="text" type="String" default="&quot;&quot;" />
<param index="2" name="icon" type="StringName" default="&quot;&quot;" />
<description>
Add a clickable button in the Inspector when running the editor. When the button is pressed, the specified [param method] is called.
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 [param method] name 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 [param method] 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

@export_tool_button(&amp;"hello")
@export_tool_button(&amp;"randomize_color", "Randomize the color!", &amp;"ColorRect")

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

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="@icon">
<return type="void" />
<param index="0" name="icon_path" type="String" />
Expand Down
2 changes: 1 addition & 1 deletion modules/gdscript/gdscript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -567,7 +567,7 @@ 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::META: {
members_cache.push_back(member.annotation->export_info);
} break;
default:
Expand Down
6 changes: 3 additions & 3 deletions modules/gdscript/gdscript_analyzer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1203,7 +1203,7 @@ void GDScriptAnalyzer::resolve_class_member(GDScriptParser::ClassNode *p_class,
resolve_class_inheritance(member.m_class, p_source);
}
break;
case GDScriptParser::ClassNode::Member::GROUP:
case GDScriptParser::ClassNode::Member::META:
// No-op, but needed to silence warnings.
break;
case GDScriptParser::ClassNode::Member::UNDEFINED:
Expand Down Expand Up @@ -1380,8 +1380,8 @@ void GDScriptAnalyzer::resolve_class_body(GDScriptParser::ClassNode *p_class, co
resolve_function_body(member.variable->setter);
}
}
} else if (member.type == GDScriptParser::ClassNode::Member::GROUP) {
// Apply annotation (`@export_{category,group,subgroup}`).
} else if (member.type == GDScriptParser::ClassNode::Member::META) {
// Apply annotation (`@export_{category,group,subgroup}`, `@export_tool_button`).
resolve_annotation(member.annotation);
member.annotation->apply(parser, nullptr, p_class);
}
Expand Down
5 changes: 3 additions & 2 deletions modules/gdscript/gdscript_compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2859,10 +2859,10 @@ 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::META: {
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);
StringName name = vformat("@meta_%d_%s", p_script->members.size(), annotation->export_info.name);

// This is not a normal member, but we need this to keep indices in order.
GDScript::MemberInfo minfo;
Expand All @@ -2871,6 +2871,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
4 changes: 2 additions & 2 deletions modules/gdscript/gdscript_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1126,7 +1126,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::META:
break; // No-op, but silences warnings.
case GDScriptParser::ClassNode::Member::UNDEFINED:
break;
Expand Down Expand Up @@ -2427,7 +2427,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::GROUP:
case GDScriptParser::ClassNode::Member::META:
return false; // No-op, but silences warnings.
case GDScriptParser::ClassNode::Member::UNDEFINED:
return false; // Unreachable.
Expand Down
Loading

0 comments on commit 19cf6d9

Please sign in to comment.