@@ -3690,7 +3690,7 @@ void EditorHelpBit::_notification(int p_what) {
36903690 }
36913691}
36923692
3693- void EditorHelpBit::parse_symbol (const String &p_symbol) {
3693+ void EditorHelpBit::parse_symbol (const String &p_symbol, const String &p_prologue ) {
36943694 const PackedStringArray slices = p_symbol.split (" |" , true , 2 );
36953695 ERR_FAIL_COND_MSG (slices.size () < 3 , " Invalid doc id. The expected format is 'item_type|class_name|item_name'." );
36963696
@@ -3737,6 +3737,14 @@ void EditorHelpBit::parse_symbol(const String &p_symbol) {
37373737 symbol_visible_type = visible_type;
37383738 symbol_name = name;
37393739
3740+ if (!p_prologue.is_empty ()) {
3741+ if (help_data.description .is_empty ()) {
3742+ help_data.description = p_prologue;
3743+ } else {
3744+ help_data.description = p_prologue + " \n " + help_data.description ;
3745+ }
3746+ }
3747+
37403748 if (help_data.description .is_empty ()) {
37413749 help_data.description = " [color=<EditorHelpBitCommentColor>][i]" + TTR (" No description available." ) + " [/i][/color]" ;
37423750 }
@@ -3760,14 +3768,6 @@ void EditorHelpBit::set_custom_text(const String &p_type, const String &p_name,
37603768 }
37613769}
37623770
3763- void EditorHelpBit::set_description (const String &p_text) {
3764- help_data.description = p_text;
3765-
3766- if (is_inside_tree ()) {
3767- _update_labels ();
3768- }
3769- }
3770-
37713771void EditorHelpBit::set_content_height_limits (float p_min, float p_max) {
37723772 ERR_FAIL_COND (p_min > p_max);
37733773 content_min_height = p_min;
@@ -3787,15 +3787,15 @@ void EditorHelpBit::update_content_height() {
37873787 content->set_custom_minimum_size (Size2 (content->get_custom_minimum_size ().x , CLAMP (content_height, content_min_height, content_max_height)));
37883788}
37893789
3790- EditorHelpBit::EditorHelpBit (const String &p_symbol) {
3790+ EditorHelpBit::EditorHelpBit (const String &p_symbol, const String &p_prologue, bool p_allow_selection ) {
37913791 add_theme_constant_override (" separation" , 0 );
37923792
37933793 title = memnew (RichTextLabel);
37943794 title->set_theme_type_variation (" EditorHelpBitTitle" );
37953795 title->set_custom_minimum_size (Size2 (512 * EDSCALE, 0 )); // GH-93031. Set the minimum width even if `fit_content` is true.
37963796 title->set_fit_content (true );
3797- title->set_selection_enabled (true );
3798- // title->set_context_menu_enabled(true); // TODO: Fix opening context menu hides tooltip.
3797+ title->set_selection_enabled (p_allow_selection );
3798+ title->set_context_menu_enabled (p_allow_selection);
37993799 title->connect (" meta_clicked" , callable_mp (this , &EditorHelpBit::_meta_clicked));
38003800 title->hide ();
38013801 add_child (title);
@@ -3806,76 +3806,108 @@ EditorHelpBit::EditorHelpBit(const String &p_symbol) {
38063806 content = memnew (RichTextLabel);
38073807 content->set_theme_type_variation (" EditorHelpBitContent" );
38083808 content->set_custom_minimum_size (Size2 (512 * EDSCALE, content_min_height));
3809- content->set_selection_enabled (true );
3810- // content->set_context_menu_enabled(true); // TODO: Fix opening context menu hides tooltip.
3809+ content->set_selection_enabled (p_allow_selection );
3810+ content->set_context_menu_enabled (p_allow_selection);
38113811 content->connect (" meta_clicked" , callable_mp (this , &EditorHelpBit::_meta_clicked));
38123812 add_child (content);
38133813
38143814 if (!p_symbol.is_empty ()) {
3815- parse_symbol (p_symbol);
3815+ parse_symbol (p_symbol, p_prologue );
38163816 }
38173817}
38183818
38193819// / EditorHelpBitTooltip ///
38203820
3821+ bool EditorHelpBitTooltip::_is_tooltip_visible = false ;
3822+
3823+ Control *EditorHelpBitTooltip::_make_invisible_control () {
3824+ Control *control = memnew (Control);
3825+ control->set_visible (false );
3826+ return control;
3827+ }
3828+
38213829void EditorHelpBitTooltip::_start_timer () {
38223830 if (timer->is_inside_tree () && timer->is_stopped ()) {
38233831 timer->start ();
38243832 }
38253833}
38263834
3827- void EditorHelpBitTooltip::_safe_queue_free () {
3828- if (_pushing_input > 0 ) {
3829- _need_free = true ;
3830- } else {
3831- queue_free ();
3832- }
3833- }
3834-
38353835void EditorHelpBitTooltip::_target_gui_input (const Ref<InputEvent> &p_event) {
3836- const Ref<InputEventMouse> mouse_event = p_event;
3837- if (mouse_event.is_valid ()) {
3838- _start_timer ();
3836+ // Only scrolling is not checked in `NOTIFICATION_INTERNAL_PROCESS`.
3837+ const Ref<InputEventMouseButton> mb = p_event;
3838+ if (mb.is_valid ()) {
3839+ switch (mb->get_button_index ()) {
3840+ case MouseButton::WHEEL_UP:
3841+ case MouseButton::WHEEL_DOWN:
3842+ case MouseButton::WHEEL_LEFT:
3843+ case MouseButton::WHEEL_RIGHT:
3844+ queue_free ();
3845+ break ;
3846+ default :
3847+ break ;
3848+ }
38393849 }
38403850}
38413851
38423852void EditorHelpBitTooltip::_notification (int p_what) {
38433853 switch (p_what) {
3854+ case NOTIFICATION_ENTER_TREE:
3855+ _is_tooltip_visible = true ;
3856+ _enter_tree_time = OS::get_singleton ()->get_ticks_msec ();
3857+ break ;
3858+ case NOTIFICATION_EXIT_TREE:
3859+ _is_tooltip_visible = false ;
3860+ break ;
38443861 case NOTIFICATION_WM_MOUSE_ENTER:
3862+ _is_mouse_inside_tooltip = true ;
38453863 timer->stop ();
38463864 break ;
38473865 case NOTIFICATION_WM_MOUSE_EXIT:
3866+ _is_mouse_inside_tooltip = false ;
38483867 _start_timer ();
38493868 break ;
3869+ case NOTIFICATION_INTERNAL_PROCESS:
3870+ // A workaround to hide the tooltip since the window does not receive keyboard events
3871+ // with `FLAG_POPUP` and `FLAG_NO_FOCUS` flags, so we can't use `_input_from_window()`.
3872+ if (is_inside_tree ()) {
3873+ if (Input::get_singleton ()->is_action_just_pressed (SNAME (" ui_cancel" ), true )) {
3874+ queue_free ();
3875+ get_parent_viewport ()->set_input_as_handled ();
3876+ } else if (Input::get_singleton ()->is_anything_pressed_except_mouse ()) {
3877+ queue_free ();
3878+ } else if (!Input::get_singleton ()->get_mouse_button_mask ().is_empty ()) {
3879+ if (!_is_mouse_inside_tooltip) {
3880+ queue_free ();
3881+ }
3882+ } else if (!Input::get_singleton ()->get_last_mouse_velocity ().is_zero_approx ()) {
3883+ if (!_is_mouse_inside_tooltip && OS::get_singleton ()->get_ticks_msec () - _enter_tree_time > 250 ) {
3884+ _start_timer ();
3885+ }
3886+ }
3887+ }
3888+ break ;
38503889 }
38513890}
38523891
3853- // Forwards non-mouse input to the parent viewport.
3854- void EditorHelpBitTooltip::_input_from_window (const Ref<InputEvent> &p_event) {
3855- if (p_event->is_action_pressed (SNAME (" ui_cancel" ), false , true )) {
3856- _safe_queue_free ();
3857- } else {
3858- const Ref<InputEventMouse> mouse_event = p_event;
3859- if (mouse_event.is_null ()) {
3860- // GH-91652. Prevents use-after-free since `ProgressDialog` calls `Main::iteration()`.
3861- _pushing_input++;
3862- get_parent_viewport ()->push_input (p_event);
3863- _pushing_input--;
3864- if (_pushing_input <= 0 && _need_free) {
3865- queue_free ();
3866- }
3867- }
3892+ Control *EditorHelpBitTooltip::show_tooltip (Control *p_target, const String &p_symbol, const String &p_prologue) {
3893+ // Show the custom tooltip only if it is not already visible.
3894+ // The viewport will retrigger `make_custom_tooltip()` every few seconds
3895+ // because the return control is not visible even if the custom tooltip is displayed.
3896+ if (_is_tooltip_visible || Input::get_singleton ()->is_anything_pressed ()) {
3897+ return _make_invisible_control ();
38683898 }
3869- }
38703899
3871- void EditorHelpBitTooltip::show_tooltip (EditorHelpBit *p_help_bit, Control *p_target) {
3872- ERR_FAIL_NULL (p_help_bit);
3900+ EditorHelpBit *help_bit = memnew (EditorHelpBit (p_symbol, p_prologue, false ));
3901+
38733902 EditorHelpBitTooltip *tooltip = memnew (EditorHelpBitTooltip (p_target));
3874- p_help_bit ->connect (" request_hide" , callable_mp (tooltip, &EditorHelpBitTooltip::_safe_queue_free ));
3875- tooltip->add_child (p_help_bit );
3903+ help_bit ->connect (" request_hide" , callable_mp (static_cast <Node *>( tooltip) , &Node::queue_free ));
3904+ tooltip->add_child (help_bit );
38763905 p_target->add_child (tooltip);
3877- p_help_bit->update_content_height ();
3906+
3907+ help_bit->update_content_height ();
38783908 tooltip->popup_under_cursor ();
3909+
3910+ return _make_invisible_control ();
38793911}
38803912
38813913// Copy-paste from `Viewport::_gui_show_tooltip()`.
@@ -3915,6 +3947,9 @@ void EditorHelpBitTooltip::popup_under_cursor() {
39153947 r.position .y = vr.position .y ;
39163948 }
39173949
3950+ // When `FLAG_POPUP` is false, it prevents the editor from losing focus when displaying the tooltip.
3951+ // This way, clicks and double-clicks are still available outside the tooltip.
3952+ set_flag (Window::FLAG_POPUP, false );
39183953 set_flag (Window::FLAG_NO_FOCUS, true );
39193954 popup (r);
39203955}
@@ -3923,13 +3958,15 @@ EditorHelpBitTooltip::EditorHelpBitTooltip(Control *p_target) {
39233958 set_theme_type_variation (" TooltipPanel" );
39243959
39253960 timer = memnew (Timer);
3926- timer->set_wait_time (0.2 );
3927- timer->connect (" timeout" , callable_mp (this , &EditorHelpBitTooltip::_safe_queue_free ));
3961+ timer->set_wait_time (0.25 );
3962+ timer->connect (" timeout" , callable_mp (static_cast <Node *>( this ) , &Node::queue_free ));
39283963 add_child (timer);
39293964
39303965 ERR_FAIL_NULL (p_target);
39313966 p_target->connect (SceneStringName (mouse_exited), callable_mp (this , &EditorHelpBitTooltip::_start_timer));
39323967 p_target->connect (SceneStringName (gui_input), callable_mp (this , &EditorHelpBitTooltip::_target_gui_input));
3968+
3969+ set_process_internal (true );
39333970}
39343971
39353972#if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED)
0 commit comments