diff --git a/core/input/input.cpp b/core/input/input.cpp index a0c00d7716dc..5314e9f02d1c 100644 --- a/core/input/input.cpp +++ b/core/input/input.cpp @@ -308,6 +308,26 @@ bool Input::is_anything_pressed() const { return false; } +bool Input::is_anything_pressed_except_mouse() const { + _THREAD_SAFE_METHOD_ + + if (disable_input) { + return false; + } + + if (!keys_pressed.is_empty() || !joy_buttons_pressed.is_empty()) { + return true; + } + + for (const KeyValue &E : action_states) { + if (E.value.cache.pressed) { + return true; + } + } + + return false; +} + bool Input::is_key_pressed(Key p_keycode) const { _THREAD_SAFE_METHOD_ diff --git a/core/input/input.h b/core/input/input.h index a189ae7d9ada..005ddcca4fdb 100644 --- a/core/input/input.h +++ b/core/input/input.h @@ -294,6 +294,7 @@ class Input : public Object { static Input *get_singleton(); bool is_anything_pressed() const; + bool is_anything_pressed_except_mouse() const; bool is_key_pressed(Key p_keycode) const; bool is_physical_key_pressed(Key p_keycode) const; bool is_key_label_pressed(Key p_keycode) const; diff --git a/editor/connections_dialog.cpp b/editor/connections_dialog.cpp index 53b1f2a759a9..2b21b4979c7f 100644 --- a/editor/connections_dialog.cpp +++ b/editor/connections_dialog.cpp @@ -911,9 +911,7 @@ Control *ConnectionsDockTree::make_custom_tooltip(const String &p_text) const { return nullptr; } - EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text)); - EditorHelpBitTooltip::show_tooltip(help_bit, const_cast(this)); - return memnew(Control); // Make the standard tooltip invisible. + return EditorHelpBitTooltip::show_tooltip(const_cast(this), p_text); } struct _ConnectionsDockMethodInfoSort { diff --git a/editor/editor_help.cpp b/editor/editor_help.cpp index 2def625f8ce7..294efca560e9 100644 --- a/editor/editor_help.cpp +++ b/editor/editor_help.cpp @@ -3690,7 +3690,7 @@ void EditorHelpBit::_notification(int p_what) { } } -void EditorHelpBit::parse_symbol(const String &p_symbol) { +void EditorHelpBit::parse_symbol(const String &p_symbol, const String &p_prologue) { const PackedStringArray slices = p_symbol.split("|", true, 2); ERR_FAIL_COND_MSG(slices.size() < 3, "Invalid doc id. The expected format is 'item_type|class_name|item_name'."); @@ -3737,6 +3737,14 @@ void EditorHelpBit::parse_symbol(const String &p_symbol) { symbol_visible_type = visible_type; symbol_name = name; + if (!p_prologue.is_empty()) { + if (help_data.description.is_empty()) { + help_data.description = p_prologue; + } else { + help_data.description = p_prologue + "\n" + help_data.description; + } + } + if (help_data.description.is_empty()) { help_data.description = "[color=][i]" + TTR("No description available.") + "[/i][/color]"; } @@ -3760,14 +3768,6 @@ void EditorHelpBit::set_custom_text(const String &p_type, const String &p_name, } } -void EditorHelpBit::set_description(const String &p_text) { - help_data.description = p_text; - - if (is_inside_tree()) { - _update_labels(); - } -} - void EditorHelpBit::set_content_height_limits(float p_min, float p_max) { ERR_FAIL_COND(p_min > p_max); content_min_height = p_min; @@ -3787,15 +3787,15 @@ void EditorHelpBit::update_content_height() { content->set_custom_minimum_size(Size2(content->get_custom_minimum_size().x, CLAMP(content_height, content_min_height, content_max_height))); } -EditorHelpBit::EditorHelpBit(const String &p_symbol) { +EditorHelpBit::EditorHelpBit(const String &p_symbol, const String &p_prologue, bool p_allow_selection) { add_theme_constant_override("separation", 0); title = memnew(RichTextLabel); title->set_theme_type_variation("EditorHelpBitTitle"); title->set_custom_minimum_size(Size2(512 * EDSCALE, 0)); // GH-93031. Set the minimum width even if `fit_content` is true. title->set_fit_content(true); - title->set_selection_enabled(true); - //title->set_context_menu_enabled(true); // TODO: Fix opening context menu hides tooltip. + title->set_selection_enabled(p_allow_selection); + title->set_context_menu_enabled(p_allow_selection); title->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked)); title->hide(); add_child(title); @@ -3806,76 +3806,108 @@ EditorHelpBit::EditorHelpBit(const String &p_symbol) { content = memnew(RichTextLabel); content->set_theme_type_variation("EditorHelpBitContent"); content->set_custom_minimum_size(Size2(512 * EDSCALE, content_min_height)); - content->set_selection_enabled(true); - //content->set_context_menu_enabled(true); // TODO: Fix opening context menu hides tooltip. + content->set_selection_enabled(p_allow_selection); + content->set_context_menu_enabled(p_allow_selection); content->connect("meta_clicked", callable_mp(this, &EditorHelpBit::_meta_clicked)); add_child(content); if (!p_symbol.is_empty()) { - parse_symbol(p_symbol); + parse_symbol(p_symbol, p_prologue); } } /// EditorHelpBitTooltip /// +bool EditorHelpBitTooltip::_is_tooltip_visible = false; + +Control *EditorHelpBitTooltip::_make_invisible_control() { + Control *control = memnew(Control); + control->set_visible(false); + return control; +} + void EditorHelpBitTooltip::_start_timer() { if (timer->is_inside_tree() && timer->is_stopped()) { timer->start(); } } -void EditorHelpBitTooltip::_safe_queue_free() { - if (_pushing_input > 0) { - _need_free = true; - } else { - queue_free(); - } -} - void EditorHelpBitTooltip::_target_gui_input(const Ref &p_event) { - const Ref mouse_event = p_event; - if (mouse_event.is_valid()) { - _start_timer(); + // Only scrolling is not checked in `NOTIFICATION_INTERNAL_PROCESS`. + const Ref mb = p_event; + if (mb.is_valid()) { + switch (mb->get_button_index()) { + case MouseButton::WHEEL_UP: + case MouseButton::WHEEL_DOWN: + case MouseButton::WHEEL_LEFT: + case MouseButton::WHEEL_RIGHT: + queue_free(); + break; + default: + break; + } } } void EditorHelpBitTooltip::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_ENTER_TREE: + _is_tooltip_visible = true; + _enter_tree_time = OS::get_singleton()->get_ticks_msec(); + break; + case NOTIFICATION_EXIT_TREE: + _is_tooltip_visible = false; + break; case NOTIFICATION_WM_MOUSE_ENTER: + _is_mouse_inside_tooltip = true; timer->stop(); break; case NOTIFICATION_WM_MOUSE_EXIT: + _is_mouse_inside_tooltip = false; _start_timer(); break; + case NOTIFICATION_INTERNAL_PROCESS: + // A workaround to hide the tooltip since the window does not receive keyboard events + // with `FLAG_POPUP` and `FLAG_NO_FOCUS` flags, so we can't use `_input_from_window()`. + if (is_inside_tree()) { + if (Input::get_singleton()->is_action_just_pressed(SNAME("ui_cancel"), true)) { + queue_free(); + get_parent_viewport()->set_input_as_handled(); + } else if (Input::get_singleton()->is_anything_pressed_except_mouse()) { + queue_free(); + } else if (!Input::get_singleton()->get_mouse_button_mask().is_empty()) { + if (!_is_mouse_inside_tooltip) { + queue_free(); + } + } else if (!Input::get_singleton()->get_last_mouse_velocity().is_zero_approx()) { + if (!_is_mouse_inside_tooltip && OS::get_singleton()->get_ticks_msec() - _enter_tree_time > 250) { + _start_timer(); + } + } + } + break; } } -// Forwards non-mouse input to the parent viewport. -void EditorHelpBitTooltip::_input_from_window(const Ref &p_event) { - if (p_event->is_action_pressed(SNAME("ui_cancel"), false, true)) { - _safe_queue_free(); - } else { - const Ref mouse_event = p_event; - if (mouse_event.is_null()) { - // GH-91652. Prevents use-after-free since `ProgressDialog` calls `Main::iteration()`. - _pushing_input++; - get_parent_viewport()->push_input(p_event); - _pushing_input--; - if (_pushing_input <= 0 && _need_free) { - queue_free(); - } - } +Control *EditorHelpBitTooltip::show_tooltip(Control *p_target, const String &p_symbol, const String &p_prologue) { + // Show the custom tooltip only if it is not already visible. + // The viewport will retrigger `make_custom_tooltip()` every few seconds + // because the return control is not visible even if the custom tooltip is displayed. + if (_is_tooltip_visible || Input::get_singleton()->is_anything_pressed()) { + return _make_invisible_control(); } -} -void EditorHelpBitTooltip::show_tooltip(EditorHelpBit *p_help_bit, Control *p_target) { - ERR_FAIL_NULL(p_help_bit); + EditorHelpBit *help_bit = memnew(EditorHelpBit(p_symbol, p_prologue, false)); + EditorHelpBitTooltip *tooltip = memnew(EditorHelpBitTooltip(p_target)); - p_help_bit->connect("request_hide", callable_mp(tooltip, &EditorHelpBitTooltip::_safe_queue_free)); - tooltip->add_child(p_help_bit); + help_bit->connect("request_hide", callable_mp(static_cast(tooltip), &Node::queue_free)); + tooltip->add_child(help_bit); p_target->add_child(tooltip); - p_help_bit->update_content_height(); + + help_bit->update_content_height(); tooltip->popup_under_cursor(); + + return _make_invisible_control(); } // Copy-paste from `Viewport::_gui_show_tooltip()`. @@ -3915,6 +3947,9 @@ void EditorHelpBitTooltip::popup_under_cursor() { r.position.y = vr.position.y; } + // When `FLAG_POPUP` is false, it prevents the editor from losing focus when displaying the tooltip. + // This way, clicks and double-clicks are still available outside the tooltip. + set_flag(Window::FLAG_POPUP, false); set_flag(Window::FLAG_NO_FOCUS, true); popup(r); } @@ -3923,13 +3958,15 @@ EditorHelpBitTooltip::EditorHelpBitTooltip(Control *p_target) { set_theme_type_variation("TooltipPanel"); timer = memnew(Timer); - timer->set_wait_time(0.2); - timer->connect("timeout", callable_mp(this, &EditorHelpBitTooltip::_safe_queue_free)); + timer->set_wait_time(0.25); + timer->connect("timeout", callable_mp(static_cast(this), &Node::queue_free)); add_child(timer); ERR_FAIL_NULL(p_target); p_target->connect(SceneStringName(mouse_exited), callable_mp(this, &EditorHelpBitTooltip::_start_timer)); p_target->connect(SceneStringName(gui_input), callable_mp(this, &EditorHelpBitTooltip::_target_gui_input)); + + set_process_internal(true); } #if defined(MODULE_GDSCRIPT_ENABLED) || defined(MODULE_MONO_ENABLED) diff --git a/editor/editor_help.h b/editor/editor_help.h index 93f74cb2c123..5435b3822dd9 100644 --- a/editor/editor_help.h +++ b/editor/editor_help.h @@ -309,15 +309,13 @@ class EditorHelpBit : public VBoxContainer { void _notification(int p_what); public: - void parse_symbol(const String &p_symbol); + void parse_symbol(const String &p_symbol, const String &p_prologue = String()); void set_custom_text(const String &p_type, const String &p_name, const String &p_description); - void set_description(const String &p_text); - _FORCE_INLINE_ String get_description() const { return help_data.description; } void set_content_height_limits(float p_min, float p_max); void update_content_height(); - EditorHelpBit(const String &p_symbol = String()); + EditorHelpBit(const String &p_symbol = String(), const String &p_prologue = String(), bool p_allow_selection = true); }; // Standard tooltips do not allow you to hover over them. @@ -325,20 +323,22 @@ class EditorHelpBit : public VBoxContainer { class EditorHelpBitTooltip : public PopupPanel { GDCLASS(EditorHelpBitTooltip, PopupPanel); + static bool _is_tooltip_visible; + Timer *timer = nullptr; - int _pushing_input = 0; - bool _need_free = false; + uint64_t _enter_tree_time = 0; + bool _is_mouse_inside_tooltip = false; + + static Control *_make_invisible_control(); void _start_timer(); - void _safe_queue_free(); void _target_gui_input(const Ref &p_event); protected: void _notification(int p_what); - virtual void _input_from_window(const Ref &p_event) override; public: - static void show_tooltip(EditorHelpBit *p_help_bit, Control *p_target); + static Control *show_tooltip(Control *p_target, const String &p_symbol, const String &p_prologue = String()); void popup_under_cursor(); diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index e709646d739b..d3026bc61139 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -1005,36 +1005,33 @@ void EditorProperty::_update_flags() { } Control *EditorProperty::make_custom_tooltip(const String &p_text) const { - String custom_warning; + String symbol; + String prologue; + if (object->has_method("_get_property_warning")) { - custom_warning = object->call("_get_property_warning", property); + const String custom_warning = object->call("_get_property_warning", property); + if (!custom_warning.is_empty()) { + prologue = "[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + custom_warning + "[/color][/b]"; + } } - if (has_doc_tooltip || !custom_warning.is_empty()) { - EditorHelpBit *help_bit = memnew(EditorHelpBit); + if (has_doc_tooltip) { + symbol = p_text; - if (has_doc_tooltip) { - help_bit->parse_symbol(p_text); - - const EditorInspector *inspector = get_parent_inspector(); - if (inspector) { - const String custom_description = inspector->get_custom_property_description(p_text); - if (!custom_description.is_empty()) { - help_bit->set_description(custom_description); + const EditorInspector *inspector = get_parent_inspector(); + if (inspector) { + const String custom_description = inspector->get_custom_property_description(p_text); + if (!custom_description.is_empty()) { + if (!prologue.is_empty()) { + prologue += '\n'; } + prologue += custom_description; } } + } - if (!custom_warning.is_empty()) { - String description = "[b][color=" + get_theme_color(SNAME("warning_color")).to_html(false) + "]" + custom_warning + "[/color][/b]"; - if (!help_bit->get_description().is_empty()) { - description += "\n" + help_bit->get_description(); - } - help_bit->set_description(description); - } - - EditorHelpBitTooltip::show_tooltip(help_bit, const_cast(this)); - return memnew(Control); // Make the standard tooltip invisible. + if (!symbol.is_empty() || !prologue.is_empty()) { + return EditorHelpBitTooltip::show_tooltip(const_cast(this), symbol, prologue); } return nullptr; @@ -1331,9 +1328,7 @@ Control *EditorInspectorCategory::make_custom_tooltip(const String &p_text) cons return nullptr; } - EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text)); - EditorHelpBitTooltip::show_tooltip(help_bit, const_cast(this)); - return memnew(Control); // Make the standard tooltip invisible. + return EditorHelpBitTooltip::show_tooltip(const_cast(this), p_text); } void EditorInspectorCategory::set_as_favorite(EditorInspector *p_for_inspector) { diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp index c2f6896e3dcc..f21f70fa3aba 100644 --- a/editor/plugins/theme_editor_plugin.cpp +++ b/editor/plugins/theme_editor_plugin.cpp @@ -2284,9 +2284,7 @@ ThemeTypeDialog::ThemeTypeDialog() { /////////////////////// Control *ThemeItemLabel::make_custom_tooltip(const String &p_text) const { - EditorHelpBit *help_bit = memnew(EditorHelpBit(p_text)); - EditorHelpBitTooltip::show_tooltip(help_bit, const_cast(this)); - return memnew(Control); // Make the standard tooltip invisible. + return EditorHelpBitTooltip::show_tooltip(const_cast(this), p_text); } VBoxContainer *ThemeTypeEditor::_create_item_list(Theme::DataType p_data_type) { diff --git a/platform/linuxbsd/x11/display_server_x11.cpp b/platform/linuxbsd/x11/display_server_x11.cpp index 90bd3d57d4b7..be66ea30ae38 100644 --- a/platform/linuxbsd/x11/display_server_x11.cpp +++ b/platform/linuxbsd/x11/display_server_x11.cpp @@ -4857,14 +4857,7 @@ void DisplayServerX11::process_events() { WindowID window_id_other = INVALID_WINDOW_ID; Window wd_other_x11_window; - if (wd.focused) { - // Handle cases where an unfocused popup is open that needs to receive button-up events. - WindowID popup_id = _get_focused_window_or_popup(); - if (popup_id != INVALID_WINDOW_ID && popup_id != window_id) { - window_id_other = popup_id; - wd_other_x11_window = windows[popup_id].x11_window; - } - } else { + if (!wd.focused) { // Propagate the event to the focused window, // because it's received only on the topmost window. // Note: This is needed for drag & drop to work between windows, diff --git a/scene/main/viewport.cpp b/scene/main/viewport.cpp index bfa048ee0d38..4b89167ce99a 100644 --- a/scene/main/viewport.cpp +++ b/scene/main/viewport.cpp @@ -1455,7 +1455,14 @@ void Viewport::_gui_show_tooltip() { // Controls can implement `make_custom_tooltip` to provide their own tooltip. // This should be a Control node which will be added as child to a TooltipPanel. - Control *base_tooltip = tooltip_owner ? tooltip_owner->make_custom_tooltip(gui.tooltip_text) : nullptr; + Control *base_tooltip = tooltip_owner->make_custom_tooltip(gui.tooltip_text); + + // When the custom control is not visible, don't show any tooltip. + // This way, the custom tooltip from `ConnectionsDockTree` can create + // its own tooltip without conflicting with the default one, even an empty tooltip. + if (base_tooltip && !base_tooltip->is_visible()) { + return; + } if (gui.tooltip_text.is_empty() && !base_tooltip) { return; // Nothing to show.