Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GDScript Symbol Tooltip #80044

Closed
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f39a362
Implemented the initial V2 of a basic symbol tooltip. Created a new …
jwmcgettigan Jul 25, 2023
81335c9
Fixed _update_body_label()
jwmcgettigan Jul 25, 2023
bf0ef56
Added some additional logic for when a symbol is considered 'hovered'…
jwmcgettigan Jul 25, 2023
9fc8446
Some refactoring + improved hover logic.
jwmcgettigan Jul 27, 2023
ac17aa4
Fixed issue where the tooltip only appeared over the first occurrence…
jwmcgettigan Jul 29, 2023
ab5b542
Separated the SymbolTooltip from the CodeEditor layout. It is now pos…
jwmcgettigan Jul 29, 2023
3bfc609
Enabled hover delay.
jwmcgettigan Jul 29, 2023
9939cde
Fixed some code style failures.
jwmcgettigan Jul 29, 2023
9fb099d
Delay now resets when moving between different symbols + slight optim…
jwmcgettigan Jul 29, 2023
77b839e
Stopped duplicating the body content for scroll testing.
jwmcgettigan Jul 29, 2023
9333587
Merge branch 'master' into editor-symbols-tooltip-2
jwmcgettigan Jul 29, 2023
c740664
Update editor/symbol_tooltip.cpp
jwmcgettigan Jul 30, 2023
1b412f8
Misc spelling/formatting fixes.
jwmcgettigan Jul 31, 2023
2c6f065
Tooltip delay timer now derives wait time from the 'gui/timers/toolti…
jwmcgettigan Jul 31, 2023
25df5f8
SymbolTooltip can now fetch the AST tree root node and the GDScriptPa…
jwmcgettigan Jul 31, 2023
2961f93
Code style fixes + added member_symbol check.
jwmcgettigan Jul 31, 2023
cfd8172
Code style fix...
jwmcgettigan Jul 31, 2023
a15a408
Code style fix...
jwmcgettigan Jul 31, 2023
99a96f2
Merge remote-tracking branch 'origin/editor-symbols-tooltip-2' into e…
jwmcgettigan Jul 31, 2023
f67ebb6
Now retrieve symbol information from DocumentSymbol objects.
jwmcgettigan Aug 2, 2023
0bfe8cd
Enabled GDScript syntax highlighting for tooltip header.
jwmcgettigan Aug 4, 2023
14d82ea
Comment punctuation fixes.
jwmcgettigan Aug 4, 2023
30af6a0
Retrieved DocumentSymbol member should now always be the original.
jwmcgettigan Aug 5, 2023
26b0137
Fixed the formatting of document comments within the tooltip body.
jwmcgettigan Aug 5, 2023
ef19c73
GDScriptParser::DataType::to_string() now returns 'void' instead of '…
jwmcgettigan Aug 5, 2023
0d96a7a
Added a 'reduced_detail' property to DocumentSymbol for a more restri…
jwmcgettigan Aug 5, 2023
b441e3d
Added the parameter types to the DocumentSymbol 'detail' and 'reduced…
jwmcgettigan Aug 5, 2023
6a5e656
The DocumentSymbol 'name' is now populated for enums.
jwmcgettigan Aug 5, 2023
223c52b
The '_init' function will now have a 'void' return type instead of th…
jwmcgettigan Aug 5, 2023
790d563
Added the ability to disable scrollbars on TextEdit nodes.
jwmcgettigan Aug 5, 2023
98f8ecb
Added ENUM values as ENUM_VALUE child members.
jwmcgettigan Aug 6, 2023
27eb768
Fixed if condition.
jwmcgettigan Aug 6, 2023
a7e7ddf
Added a nullptr check.
jwmcgettigan Aug 6, 2023
2060968
Tooltip size now adapts to its contents - including the conditional v…
jwmcgettigan Aug 8, 2023
29e07cf
Partially implemented max width with line wrapping in header.
jwmcgettigan Aug 9, 2023
99b3ced
Misc minor improvements.
jwmcgettigan Aug 13, 2023
6a36bdc
Transitioned from PopupPanel to PanelContainer. Updated sizing calcu…
jwmcgettigan Sep 7, 2023
0cd17f4
A mouse exiting a tooltip now causes it to hide().
jwmcgettigan Sep 8, 2023
ea6bc7d
Removed unneeded code.
jwmcgettigan Sep 8, 2023
b154a41
Cleaned up issues caught by GitHub Actions.
jwmcgettigan Sep 10, 2023
e90ba29
Merge branch 'master' into editor-symbols-tooltip-2
jwmcgettigan Sep 10, 2023
82ab338
Formatting fixes.
jwmcgettigan Sep 10, 2023
4a2d88b
Fixed shadow variables.
jwmcgettigan Sep 10, 2023
73c5b89
Dynamic body width now actually adjusts to fit content.
jwmcgettigan Sep 10, 2023
26e7699
Moved content logic into separate functions.
jwmcgettigan Sep 10, 2023
e317b43
Added tooltip docs for built-in symbols + minor refactoring.
jwmcgettigan Sep 11, 2023
f348418
All parameters are now prefixed with 'p_'.
jwmcgettigan Sep 11, 2023
a4f68bc
Rearranged class methods.
jwmcgettigan Sep 11, 2023
e4b5d58
Update modules/gdscript/language_server/gdscript_extend_parser.cpp
jwmcgettigan Sep 11, 2023
6686fb9
Polished symbol info for built-in 'constant' and 'method' symbols.
jwmcgettigan Sep 11, 2023
3782e33
Polished symbol info for built-in 'enum' symbols.
jwmcgettigan Sep 11, 2023
f7d3155
Formatting adjustments
jwmcgettigan Sep 11, 2023
ea8a9a6
Formatting adjustments 2
jwmcgettigan Sep 11, 2023
517d25d
Removed potential cause of 'heap-use-after-free' error.
jwmcgettigan Sep 12, 2023
9597367
Enabled 'Copy' and 'Select All' context menu items on the tooltip body.
jwmcgettigan Sep 14, 2023
0c32564
Added the ability to compare Position variables and to check if one R…
jwmcgettigan Sep 29, 2023
0bda6e4
Sizeable refactor.
jwmcgettigan Sep 29, 2023
1c665af
Merge branch 'master' into editor-symbols-tooltip-2
jwmcgettigan Sep 30, 2023
41559e0
Post-merge adjustments.
jwmcgettigan Sep 30, 2023
3ddd23b
Updated all instances of DocumentSymbol to use pointers to allow the …
jwmcgettigan Oct 1, 2023
dc3f77a
Merge branch 'master' into editor-symbols-tooltip-2
jwmcgettigan Dec 15, 2023
037b646
Merge branch 'master' into editor-symbols-tooltip-2
jwmcgettigan Feb 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion editor/editor_help.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1928,7 +1928,8 @@ void EditorHelp::_help_callback(const String &p_topic) {
}
}

static void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control *p_owner_node, const String &p_class = "") {
// TODO: Should probably move this to a shared utility file/class so it can be accessed by both 'editor_help.cpp' and 'symbol_tooltip.cpp'
void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control *p_owner_node, const String &p_class) {
DocTools *doc = EditorHelp::get_doc_data();
String base_path;

Expand Down
2 changes: 2 additions & 0 deletions editor/editor_help.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,4 +251,6 @@ class EditorHelpBit : public MarginContainer {
EditorHelpBit();
};

void _add_text_to_rt(const String &p_bbcode, RichTextLabel *p_rt, Control *p_owner_node, const String &p_class = "");

#endif // EDITOR_HELP_H
6 changes: 6 additions & 0 deletions editor/plugins/script_text_editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,7 @@ void ScriptTextEditor::drop_data_fw(const Point2 &p_point, const Variant &p_data

void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
Ref<InputEventMouseButton> mb = ev;
Ref<InputEventMouseMotion> mm = ev;
Ref<InputEventKey> k = ev;
Point2 local_pos;
bool create_menu = false;
Expand All @@ -1867,6 +1868,8 @@ void ScriptTextEditor::_text_edit_gui_input(const Ref<InputEvent> &ev) {
if (mb.is_valid() && mb->get_button_index() == MouseButton::RIGHT && mb->is_pressed()) {
local_pos = mb->get_global_position() - tx->get_global_position();
create_menu = true;
} else if (mm.is_valid()) {
symbol_tooltip->update_symbol_tooltip(mm->get_position());
} else if (k.is_valid() && k->is_action("ui_menu", true)) {
tx->adjust_viewport_to_caret(0);
local_pos = tx->get_caret_draw_pos(0);
Expand Down Expand Up @@ -2322,6 +2325,9 @@ ScriptTextEditor::ScriptTextEditor() {
connection_info_dialog = memnew(ConnectionInfoDialog);

SET_DRAG_FORWARDING_GCD(code_editor->get_text_editor(), ScriptTextEditor);

symbol_tooltip = memnew(SymbolTooltip(code_editor));
add_child(symbol_tooltip);
}

ScriptTextEditor::~ScriptTextEditor() {
Expand Down
3 changes: 3 additions & 0 deletions editor/plugins/script_text_editor.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "script_editor_plugin.h"

#include "editor/code_editor.h"
#include "editor/symbol_tooltip.h"
#include "scene/gui/color_picker.h"
#include "scene/gui/dialogs.h"
#include "scene/gui/tree.h"
Expand Down Expand Up @@ -156,6 +157,8 @@ class ScriptTextEditor : public ScriptEditorBase {

void _enable_code_editor();

SymbolTooltip *symbol_tooltip = nullptr;

protected:
void _update_breakpoint_list();
void _breakpoint_item_pressed(int p_idx);
Expand Down
302 changes: 302 additions & 0 deletions editor/symbol_tooltip.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
/**************************************************************************/
/* symbol_tooltip.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 "symbol_tooltip.h"
#include "editor/plugins/script_text_editor.h"
#include "editor_help.h"

SymbolTooltip::SymbolTooltip(CodeTextEditor* code_editor) : code_editor(code_editor) {
// Initialize the tooltip components
jwmcgettigan marked this conversation as resolved.
Show resolved Hide resolved

// Set the tooltip's theme (PanelContainer's theme)
//set_theme(EditorNode::get_singleton()->get_gui_base()->get_theme());

set_transient(true);
set_flag(Window::FLAG_NO_FOCUS, true);
set_flag(Window::FLAG_POPUP, false);
set_flag(Window::FLAG_MOUSE_PASSTHROUGH, false);
set_theme(_create_popup_panel_theme());
panel_container = memnew(PanelContainer);
panel_container->set_theme(_create_panel_theme());
add_child(panel_container);

// Create VBoxContainer to hold the tooltip's header and body
layout_container = memnew(VBoxContainer);
panel_container->add_child(layout_container);

// Create RichTextLabel for the tooltip's header
header_label = memnew(RichTextLabel);
header_label->set_use_bbcode(true);
header_label->set_selection_enabled(true);
header_label->set_custom_minimum_size(Size2(0, 50));
header_label->set_focus_mode(Control::FOCUS_ALL);
header_label->set_theme(_create_header_label_theme());
layout_container->add_child(header_label);

// Create RichTextLabel for the tooltip's body
body_label = memnew(RichTextLabel);
body_label->set_use_bbcode(true);
body_label->set_selection_enabled(true);
body_label->set_focus_mode(Control::FOCUS_ALL);
body_label->set_v_size_flags(Control::SIZE_EXPAND_FILL);
body_label->set_theme(_create_body_label_theme());
layout_container->add_child(body_label);

tooltip_delay = memnew(Timer);
tooltip_delay->set_one_shot(true);
tooltip_delay->set_wait_time(0.5);
add_child(tooltip_delay);

tooltip_delay->connect("timeout", callable_mp(this, &SymbolTooltip::_on_tooltip_delay_timeout));

// Connect the tooltip's update function to the mouse motion signal
// connect("mouse_motion", callable_mp(this, &SymbolTooltip::_update_symbol_tooltip));
}

SymbolTooltip::~SymbolTooltip() {
memdelete(tooltip_delay);
}

void SymbolTooltip::_on_tooltip_delay_timeout() {
show();
}

void SymbolTooltip::update_symbol_tooltip(const Vector2 &mouse_position) {
CodeEdit *text_editor = code_editor->get_text_editor();
String symbol_word = _get_symbol_word(text_editor, mouse_position);
if (symbol_word.is_empty()) {
tooltip_delay->stop();
last_symbol_word = "";
hide();
return;
}

if (symbol_word == last_symbol_word && is_visible()) {
return;
} else {
// Symbol has changed, reset the timer.
tooltip_delay->stop();
last_symbol_word = symbol_word;
}

_update_tooltip_size();

// Get the documentation of the word under the mouse cursor
String documentation = _get_doc_of_word(symbol_word);
_update_tooltip_content(symbol_word, documentation);

Rect2 tooltip_rect = Rect2(get_position(), get_size());
bool mouse_over_tooltip = tooltip_rect.has_point(mouse_position);
if (!mouse_over_tooltip) {
Vector2 tooltip_position = _calculate_tooltip_position(symbol_word, mouse_position);
if (tooltip_position == Vector2(-1, -1)) { // If invalid position
tooltip_delay->stop();
hide();
return;
} else {
set_position(tooltip_position);
}
}

// Start the timer to show the tooltip after a delay
tooltip_delay->start();
}

String SymbolTooltip::_get_symbol_word(CodeEdit *text_editor, const Vector2 &mouse_position) {
// Get the word under the mouse cursor
return text_editor->get_word_at_pos(mouse_position);
}

Vector2 SymbolTooltip::_calculate_tooltip_position(const String &symbol_word, const Vector2 &mouse_position) {
CodeEdit *text_editor = code_editor->get_text_editor();
Vector2 line_col = text_editor->get_line_column_at_pos(mouse_position);
int row = line_col.y;
int col = line_col.x;
int num_lines = text_editor->get_line_count();
if (row >= 0 && row < num_lines) {
String line = text_editor->get_line(row);
int symbol_col = _get_word_pos_under_mouse(symbol_word, line, col);
if (symbol_col >= 0) {
Vector2 symbol_position = text_editor->get_pos_at_line_column(row, symbol_col);
return text_editor->get_screen_position() + symbol_position;
}
}
return Vector2(-1, -1); // indicates an invalid position
}

void SymbolTooltip::_update_tooltip_size() {
// Calculate and set the tooltip's size
set_size(Vector2(600, 300));
}

void SymbolTooltip::_update_tooltip_content(const String &header_content, const String &body_content) {
// Update the tooltip's header and body
_update_header_label(header_content);
_update_body_label(body_content);
}

void SymbolTooltip::_update_header_label(const String &header_content) {
// Set the tooltip's header text
header_label->set_text(header_content);
}

void SymbolTooltip::_update_body_label(const String &body_content) {
// Set the tooltip's body text
body_label->clear();
_add_text_to_rt(body_content, body_label, layout_container);
}

String SymbolTooltip::_get_doc_of_word(const String &symbol_word) {
String documentation;

const HashMap<String, DocData::ClassDoc> &class_list = EditorHelp::get_doc_data()->class_list;
for (const KeyValue<String, DocData::ClassDoc> &E : class_list) {
const DocData::ClassDoc &class_doc = E.value;

if (class_doc.name == symbol_word) {
documentation = class_doc.brief_description.strip_edges(); //class_doc.brief_description + "\n\n" + class_doc.description;
break;
}

for (int i = 0; i < class_doc.methods.size(); ++i) {
const DocData::MethodDoc &method_doc = class_doc.methods[i];

if (method_doc.name == symbol_word) {
documentation = method_doc.description.strip_edges();
break;
}
}

if (!documentation.is_empty()) {
break;
}
}

/*if (!documentation.is_empty()) {
print_line(vformat("Documentation for %s:\n%s", symbol_word, documentation));
}*/
return documentation;
}

Ref<Theme> SymbolTooltip::_create_popup_panel_theme() {
Ref<Theme> theme = memnew(Theme);

Ref<StyleBoxFlat> style_box = memnew(StyleBoxFlat);
style_box->set_bg_color(Color(0, 0, 0, 0)); // Set the background color (RGBA)
theme->set_stylebox("panel", "PopupPanel", style_box);

return theme;
}

Ref<Theme> SymbolTooltip::_create_panel_theme() {
Ref<Theme> theme = memnew(Theme); // TODO: Get the global theme instead (e.g. dark mode, light mode)

Ref<StyleBoxFlat> style_box = memnew(StyleBoxFlat);
style_box->set_bg_color(Color().html("#363d4a")); // Set the background color (RGBA)
style_box->set_border_color(Color(0.8, 0.81, 0.82, 0.47)); // Set the border color (RGBA)
style_box->set_border_width_all(1); // Set the border width
style_box->set_corner_radius_all(4); // Set the border radius for curved corners
//style_box->set_content_margin_all(20);
theme->set_stylebox("panel", "PanelContainer", style_box);

return theme;
}

Ref<Theme> SymbolTooltip::_create_header_label_theme() {
Ref<Theme> theme = memnew(Theme); // TODO: Get the global theme instead (e.g. dark mode, light mode)

Ref<StyleBoxFlat> style_box = memnew(StyleBoxFlat);
style_box->set_draw_center(false);
style_box->set_border_color(Color(0.8, 0.81, 0.82, 0.27)); // Set the border color (RGBA)
style_box->set_border_width(SIDE_BOTTOM, 1);
style_box->set_content_margin_individual(15, 10, 15, 10);
theme->set_stylebox("normal", "RichTextLabel", style_box);

return theme;
}

Ref<Theme> SymbolTooltip::_create_body_label_theme() {
Ref<Theme> theme = memnew(Theme); // TODO: Get the global theme instead (e.g. dark mode, light mode)

Ref<StyleBoxFlat> style_box = memnew(StyleBoxFlat);
style_box->set_draw_center(false);
style_box->set_content_margin_individual(15, 10, 15, 10);
theme->set_stylebox("normal", "RichTextLabel", style_box);

return theme;
}

int SymbolTooltip::_get_word_pos_under_mouse(const String &symbol_word, const String &p_search, int mouse_x) const {
// Created this because _get_column_pos_of_word() only gets the column position of the first occurrence of the word in the line.

// Early exit if the symbol word is empty, the search string is empty, or the mouse is outside the string.
if (symbol_word.is_empty() || p_search.is_empty() || mouse_x < 0 || mouse_x >= p_search.length()) {
return -1;
}

int start = mouse_x;
int end = mouse_x;

// Extend the start and end until they reach the beginning or end of the word.
while (start > 0 && is_ascii_identifier_char(p_search[start - 1])) {
start--;
}
while (end < p_search.length() && is_ascii_identifier_char(p_search[end])) {
end++;
}

String word_under_mouse = p_search.substr(start, end - start);

// If the word under the mouse matches the symbol word, return the start position.
if (word_under_mouse == symbol_word) {
return start + 1; // Note: +1 is added to account for zero-based indexing.
}

return -1; // Return -1 if no match is found.
}

// Copied from script_text_editor.cpp
static Node *_find_node_for_script(Node *p_base, Node *p_current, const Ref<Script> &p_script) {
if (p_current->get_owner() != p_base && p_base != p_current) {
return nullptr;
}
Ref<Script> c = p_current->get_script();
if (c == p_script) {
return p_current;
}
for (int i = 0; i < p_current->get_child_count(); i++) {
Node *found = _find_node_for_script(p_base, p_current->get_child(i), p_script);
if (found) {
return found;
}
}

return nullptr;
}
Loading