Compare commits

..

5 Commits

Author SHA1 Message Date
Serhii Snitsaruk 0f9c566a63
Merge pull request #159 from limbonaut/improve-tooltips
Improve documentation tooltips
2024-07-07 13:44:26 +02:00
Serhii Snitsaruk 86d31f5f00
Update demo documentation comments 2024-07-07 13:22:02 +02:00
Serhii Snitsaruk 5f40d38f8d
Add a hack to force documentation parsing on user scripts
As things currently stand in Godot Engine, documentation comments are
not loaded from user scripts into help system when the project is
started. This leads to empty tooltips for user tasks in LimboAI, unless
such a script is resaved.

This hack forces script documentation to be added to help system,
when the editor needs it to show a tooltip.
2024-07-07 12:13:07 +02:00
Serhii Snitsaruk 800bc8f16d
Tooltip improvements
- Fix tooltip not shown for scripted tasks in TaskPalette
- Allow following links in the tooltip text
- If help data cannot be found, an empty tooltip is shown instead
2024-07-06 16:46:55 +02:00
Serhii Snitsaruk 40acd04eb9
Fix tooltip popup too small 2024-07-06 12:33:08 +02:00
12 changed files with 113 additions and 76 deletions

View File

@ -10,9 +10,9 @@
#* #*
@tool @tool
extends BTAction extends BTAction
## ArrivePos: Arrive to a position, with a bias to horizontal movement. ## Moves the agent to the specified position, favoring horizontal movement. [br]
## Returns SUCCESS when close to the target position (see tolerance); ## Returns [code]SUCCESS[/code] when close to the target position (see [member tolerance]);
## otherwise returns RUNNING. ## otherwise returns [code]RUNNING[/code].
## Blackboard variable that stores the target position (Vector2) ## Blackboard variable that stores the target position (Vector2)
@export var target_position_var := &"pos" @export var target_position_var := &"pos"

View File

@ -10,8 +10,8 @@
#* #*
@tool @tool
extends BTAction extends BTAction
## BackAway ## Moves the agent in the opposite direction of its current facing. [br]
## Returns RUNNING always. ## Returns [code]RUNNING[/code] always.
## Blackboard variable that stores desired speed. ## Blackboard variable that stores desired speed.
@export var speed_var: StringName = &"speed" @export var speed_var: StringName = &"speed"

View File

@ -10,8 +10,8 @@
#* #*
@tool @tool
extends BTAction extends BTAction
## FaceTarget and return SUCCESS. ## Flips the agent to face the target, returning [code]SUCCESS[/code]. [br]
## Returns FAILURE if target is not a valid Node2D instance. ## Returns [code]FAILURE[/code] if [member target_var] is not a valid [Node2D] instance.
## Blackboard variable that stores our target (expecting Node2D). ## Blackboard variable that stores our target (expecting Node2D).
@export var target_var: StringName = &"target" @export var target_var: StringName = &"target"

View File

@ -10,8 +10,8 @@
#* #*
@tool @tool
extends BTAction extends BTAction
## Get first node in group and save it to the blackboard. ## Stores the first node in the [member group] on the blackboard, returning [code]SUCCESS[/code]. [br]
## Returns FAILURE if group contains 0 nodes. ## Returns [code]FAILURE[/code] if the group contains 0 nodes.
## Name of the SceneTree group. ## Name of the SceneTree group.
@export var group: StringName @export var group: StringName

View File

@ -10,11 +10,10 @@
#* #*
@tool @tool
extends BTCondition extends BTCondition
## InRange condition checks if the agent is within a range of target, ## InRange condition checks if the agent is within a range of target,
## defined by distance_min and distance_max. ## defined by [member distance_min] and [member distance_max]. [br]
## Returns SUCCESS if the agent is within the defined range; ## Returns [code]SUCCESS[/code] if the agent is within the given range;
## otherwise, returns FAILURE. ## otherwise, returns [code]FAILURE[/code].
## Minimum distance to target. ## Minimum distance to target.
@export var distance_min: float @export var distance_min: float

View File

@ -10,9 +10,9 @@
#* #*
@tool @tool
extends BTCondition extends BTCondition
## IsAlignedWithTarget ## Checks if the agent is horizontally aligned with the target. [br]
## Returns SUCCESS if the agent is horizontally aligned with the target. ## Returns [code]SUCCESS[/code] if the agent is horizontally aligned with the target.
## Returns FAILURE if not aligned or if target is not a valid node instance. ## Returns [code]FAILURE[/code] if not aligned or if target is not a valid node instance.
@export var target_var: StringName = &"target" @export var target_var: StringName = &"target"

View File

@ -10,9 +10,10 @@
#* #*
@tool @tool
extends BTAction extends BTAction
## MoveForward: Applies velocity each tick until duration is exceeded. ## Applies velocity in the direction the agent is facing on each tick
## Returns SUCCESS if elapsed time exceeded duration. ## until the [member duration] is exceeded. [br]
## Returns RUNNING if elapsed time didn't exceed duration. ## Returns [code]SUCCESS[/code] if the elapsed time exceeds [member duration]. [br]
## Returns [code]RUNNING[/code] if the elapsed time does not exceed [member duration]. [br]
## Blackboard variable that stores desired speed. ## Blackboard variable that stores desired speed.
@export var speed_var: StringName = &"speed" @export var speed_var: StringName = &"speed"

View File

@ -10,11 +10,10 @@
#* #*
@tool @tool
extends BTAction extends BTAction
## Pursue: Move towards target until agent is flanking it. ## Move towards the target until the agent is flanking it. [br]
## ## Returns [code]RUNNING[/code] while moving towards the target but not yet at the desired position. [br]
## Returns RUNNING, while moving towards target but not yet at the desired position. ## Returns [code]SUCCESS[/code] when at the desired position relative to the target (flanking it). [br]
## Returns SUCCESS, when at the desired position from target (flanking it). ## Returns [code]FAILURE[/code] if the target is not a valid [Node2D] instance. [br]
## Returns FAILURE, if target is not a valid Node2D instance.
## How close should the agent be to the desired position to return SUCCESS. ## How close should the agent be to the desired position to return SUCCESS.
const TOLERANCE := 30.0 const TOLERANCE := 30.0

View File

@ -10,8 +10,9 @@
#* #*
@tool @tool
extends BTAction extends BTAction
## SelectFlankingPos on the side of a target, and return SUCCESS. ## Selects a position on the target's side and stores it on the
## Returns FAILURE, if the target is not valid. ## blackboard, returning [code]SUCCESS[/code]. [br]
## Returns [code]FAILURE[/code] if the target is not valid.
enum AgentSide { enum AgentSide {
CLOSEST, CLOSEST,
@ -71,4 +72,3 @@ func _tick(_delta: float) -> Status:
flank_pos = target.global_position - offset flank_pos = target.global_position - offset
blackboard.set_var(position_var, flank_pos) blackboard.set_var(position_var, flank_pos)
return SUCCESS return SUCCESS

View File

@ -1,7 +1,7 @@
@tool @tool
extends BTAction extends BTAction
## SelectRandomNearbyPos: Select a position nearby within specified range. ## Selects a random position nearby within the specified range and stores it on the blackboard. [br]
## Returns SUCCESS. ## Returns [code]SUCCESS[/code].
## Minimum distance to the desired position. ## Minimum distance to the desired position.
@export var range_min: float = 300.0 @export var range_min: float = 300.0

View File

@ -24,8 +24,8 @@
#include "editor/editor_help.h" #include "editor/editor_help.h"
#include "editor/editor_node.h" #include "editor/editor_node.h"
#include "editor/editor_paths.h" #include "editor/editor_paths.h"
#include "editor/themes/editor_scale.h"
#include "editor/plugins/script_editor_plugin.h" #include "editor/plugins/script_editor_plugin.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/check_box.h" #include "scene/gui/check_box.h"
#endif // LIMBO_MODULE #endif // LIMBO_MODULE
@ -57,30 +57,86 @@ using namespace godot;
void TaskButton::_bind_methods() { void TaskButton::_bind_methods() {
} }
Control *TaskButton::_do_make_tooltip(const String &p_text) const { Control *TaskButton::_do_make_tooltip() const {
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
EditorHelpBit *help_bit = memnew(EditorHelpBit); String help_symbol;
help_bit->set_content_height_limits(1, 360 * EDSCALE); bool is_resource = task_meta.begins_with("res://");
String help_text; if (is_resource) {
if (!p_text.is_empty()) { help_symbol = "class|\"" + task_meta.lstrip("res://") + "\"|";
help_text = p_text;
} else { } else {
help_text = "[i]" + TTR("No description.") + "[/i]"; help_symbol = "class|" + task_meta + "|";
} }
help_bit->set_custom_text(String(), String(), help_text); EditorHelpBit *help_bit = memnew(EditorHelpBit(help_symbol));
help_bit->set_content_height_limits(1, 360 * EDSCALE);
return help_bit; String desc = _module_get_help_description(task_meta);
if (desc.is_empty() && is_resource) {
// ! HACK: Force documentation parsing.
Ref<Script> s = ResourceLoader::load(task_meta);
if (s.is_valid()) {
Vector<DocData::ClassDoc> docs = s->get_documentation();
for (int i = 0; i < docs.size(); i++) {
const DocData::ClassDoc &doc = docs.get(i);
EditorHelp::get_doc_data()->add_doc(doc);
}
desc = _module_get_help_description(task_meta);
}
}
if (desc.is_empty() && help_bit->get_description().is_empty()) {
desc = "[i]" + TTR("No description.") + "[/i]";
}
if (!desc.is_empty()) {
help_bit->set_description(desc);
}
EditorHelpBitTooltip::show_tooltip(help_bit, const_cast<TaskButton *>(this));
#endif // LIMBOAI_MODULE #endif // LIMBOAI_MODULE
#ifdef LIMBOAI_GDEXTENSION #ifdef LIMBOAI_GDEXTENSION
// TODO: When we figure out how to retrieve documentation in GDEXTENSION, should add a tooltip control here. // TODO: When we figure out how to retrieve documentation in GDEXTENSION, should add a tooltip control here.
#endif // LIMBOAI_GDEXTENSION #endif // LIMBOAI_GDEXTENSION
return nullptr; return memnew(Control); // Make the standard tooltip invisible.
} }
#ifdef LIMBOAI_MODULE
String TaskButton::_module_get_help_description(const String &p_class_or_script_path) const {
String descr;
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::Iterator E;
if (p_class_or_script_path.begins_with("res://")) {
// Try to find by script path.
E = dd->class_list.find(vformat("\"%s\"", p_class_or_script_path.trim_prefix("res://")));
if (!E) {
// Try to guess global script class from filename.
String maybe_class_name = p_class_or_script_path.get_file().get_basename().to_pascal_case();
E = dd->class_list.find(maybe_class_name);
}
} else {
// Try to find core class or global class.
E = dd->class_list.find(p_class_or_script_path);
}
if (E) {
if (E->value.description.is_empty()) {
descr = DTR(E->value.brief_description);
} else {
descr = DTR(E->value.description);
}
}
// TODO: Documentation tooltips are only available in the module variant. Find a way to show em in GDExtension.
return descr;
}
#endif // LIMBOAI_MODULE
TaskButton::TaskButton() { TaskButton::TaskButton() {
set_focus_mode(FOCUS_NONE); set_focus_mode(FOCUS_NONE);
} }
@ -125,11 +181,12 @@ void TaskPaletteSection::set_filter(String p_filter_text) {
} }
} }
void TaskPaletteSection::add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_tooltip, Variant p_meta) { void TaskPaletteSection::add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_meta) {
TaskButton *btn = memnew(TaskButton); TaskButton *btn = memnew(TaskButton);
btn->set_text(p_name); btn->set_text(p_name);
BUTTON_SET_ICON(btn, icon); BUTTON_SET_ICON(btn, icon);
btn->set_tooltip_text(p_tooltip); btn->set_tooltip_text("dummy_text"); // Force tooltip to be shown.
btn->set_task_meta(p_meta);
btn->add_theme_constant_override(LW_NAME(icon_max_width), 16 * EDSCALE); // Force user icons to be of the proper size. btn->add_theme_constant_override(LW_NAME(icon_max_width), 16 * EDSCALE); // Force user icons to be of the proper size.
btn->connect(LW_NAME(pressed), callable_mp(this, &TaskPaletteSection::_on_task_button_pressed).bind(p_meta)); btn->connect(LW_NAME(pressed), callable_mp(this, &TaskPaletteSection::_on_task_button_pressed).bind(p_meta));
btn->connect(LW_NAME(gui_input), callable_mp(this, &TaskPaletteSection::_on_task_button_gui_input).bind(p_meta)); btn->connect(LW_NAME(gui_input), callable_mp(this, &TaskPaletteSection::_on_task_button_gui_input).bind(p_meta));
@ -422,11 +479,10 @@ void TaskPalette::refresh() {
TaskPaletteSection *sec = memnew(TaskPaletteSection()); TaskPaletteSection *sec = memnew(TaskPaletteSection());
sec->set_category_name(cat); sec->set_category_name(cat);
for (String task_meta : tasks) { for (const String &task_meta : tasks) {
Ref<Texture2D> icon = LimboUtility::get_singleton()->get_task_icon(task_meta); Ref<Texture2D> icon = LimboUtility::get_singleton()->get_task_icon(task_meta);
String tname; String tname;
String descr;
if (task_meta.begins_with("res:")) { if (task_meta.begins_with("res:")) {
if (filter_settings.type_filter == FilterSettings::TYPE_CORE) { if (filter_settings.type_filter == FilterSettings::TYPE_CORE) {
@ -440,34 +496,7 @@ void TaskPalette::refresh() {
tname = task_meta.trim_prefix("BT"); tname = task_meta.trim_prefix("BT");
} }
#ifdef LIMBOAI_MODULE sec->add_task_button(tname, icon, task_meta);
// Get documentation.
DocTools *dd = EditorHelp::get_doc_data();
HashMap<String, DocData::ClassDoc>::Iterator E;
// Try-find core class.
E = dd->class_list.find(task_meta);
if (!E) {
// Try to find by script filename.
E = dd->class_list.find(vformat("\"%s\"", task_meta.trim_prefix("res://")));
}
if (!E) {
// Try-find global script class.
String maybe_class_name = task_meta.get_file().get_basename().to_pascal_case();
E = dd->class_list.find(maybe_class_name);
}
if (E) {
if (E->value.description.is_empty() || E->value.description.length() > 1400) {
descr = DTR(E->value.brief_description);
} else {
descr = DTR(E->value.description);
}
}
#endif // LIMBOAI_MODULE
// TODO: Documentation tooltips are only available in the module. Find a way to show em in GDExtension.
sec->add_task_button(tname, icon, descr, task_meta);
} }
sec->set_filter(""); sec->set_filter("");
sec->connect(LW_NAME(task_button_pressed), callable_mp(this, &TaskPalette::_on_task_button_pressed)); sec->connect(LW_NAME(task_button_pressed), callable_mp(this, &TaskPalette::_on_task_button_pressed));

View File

@ -42,18 +42,27 @@ class TaskButton : public Button {
GDCLASS(TaskButton, Button); GDCLASS(TaskButton, Button);
private: private:
Control *_do_make_tooltip(const String &p_text) const; String task_meta;
Control *_do_make_tooltip() const;
#ifdef LIMBOAI_MODULE
String _module_get_help_description(const String &p_class_or_script_path) const;
#endif
protected: protected:
static void _bind_methods(); static void _bind_methods();
public: public:
#ifdef LIMBOAI_MODULE #ifdef LIMBOAI_MODULE
virtual Control *make_custom_tooltip(const String &p_text) const override { return _do_make_tooltip(p_text); } virtual Control *make_custom_tooltip(const String &p_text) const override { return _do_make_tooltip(); }
#elif LIMBOAI_GDEXTENSION #elif LIMBOAI_GDEXTENSION
virtual Object *_make_custom_tooltip(const String &p_text) const override { return _do_make_tooltip(p_text); } virtual Object *_make_custom_tooltip(const String &p_text) const override { return _do_make_tooltip(); }
#endif #endif
String get_task_meta() const { return task_meta; }
void set_task_meta(const String &p_task_meta) { task_meta = p_task_meta; }
TaskButton(); TaskButton();
}; };
@ -82,7 +91,7 @@ protected:
public: public:
void set_filter(String p_filter); void set_filter(String p_filter);
void add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_tooltip, Variant p_meta); void add_task_button(const String &p_name, const Ref<Texture> &icon, const String &p_meta);
void set_collapsed(bool p_collapsed); void set_collapsed(bool p_collapsed);
bool is_collapsed() const; bool is_collapsed() const;