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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,9 +10,10 @@
#*
@tool
extends BTAction
## MoveForward: Applies velocity each tick until duration is exceeded.
## Returns SUCCESS if elapsed time exceeded duration.
## Returns RUNNING if elapsed time didn't exceed duration.
## Applies velocity in the direction the agent is facing on each tick
## until the [member duration] is exceeded. [br]
## 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.
@export var speed_var: StringName = &"speed"

View File

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

View File

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

View File

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

View File

@ -24,8 +24,8 @@
#include "editor/editor_help.h"
#include "editor/editor_node.h"
#include "editor/editor_paths.h"
#include "editor/themes/editor_scale.h"
#include "editor/plugins/script_editor_plugin.h"
#include "editor/themes/editor_scale.h"
#include "scene/gui/check_box.h"
#endif // LIMBO_MODULE
@ -57,30 +57,86 @@ using namespace godot;
void TaskButton::_bind_methods() {
}
Control *TaskButton::_do_make_tooltip(const String &p_text) const {
Control *TaskButton::_do_make_tooltip() const {
#ifdef LIMBOAI_MODULE
EditorHelpBit *help_bit = memnew(EditorHelpBit);
help_bit->set_content_height_limits(1, 360 * EDSCALE);
String help_symbol;
bool is_resource = task_meta.begins_with("res://");
String help_text;
if (!p_text.is_empty()) {
help_text = p_text;
if (is_resource) {
help_symbol = "class|\"" + task_meta.lstrip("res://") + "\"|";
} 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
#ifdef LIMBOAI_GDEXTENSION
// TODO: When we figure out how to retrieve documentation in GDEXTENSION, should add a tooltip control here.
#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() {
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);
btn->set_text(p_name);
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->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));
@ -422,11 +479,10 @@ void TaskPalette::refresh() {
TaskPaletteSection *sec = memnew(TaskPaletteSection());
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);
String tname;
String descr;
if (task_meta.begins_with("res:")) {
if (filter_settings.type_filter == FilterSettings::TYPE_CORE) {
@ -440,34 +496,7 @@ void TaskPalette::refresh() {
tname = task_meta.trim_prefix("BT");
}
#ifdef LIMBOAI_MODULE
// 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->add_task_button(tname, icon, task_meta);
}
sec->set_filter("");
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);
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:
static void _bind_methods();
public:
#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
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
String get_task_meta() const { return task_meta; }
void set_task_meta(const String &p_task_meta) { task_meta = p_task_meta; }
TaskButton();
};
@ -82,7 +91,7 @@ protected:
public:
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);
bool is_collapsed() const;