diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index a088ebf7f8..1c6b5634b3 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -42,12 +42,24 @@ #include "core/string/translation_server.h" #include "core/string/ustring.h" #include "scene/theme/theme_db.h" +#include "servers/display/accessibility_server.h" #include "servers/rendering/rendering_server.h" void CodeEdit::_apply_project_settings() { symbol_tooltip_timer->set_wait_time(GLOBAL_GET_CACHED(double, "gui/timers/tooltip_delay_sec")); } +RID CodeEdit::get_focused_accessibility_element() const { + bool draw_code_completion = code_completion_active && !code_completion_options.is_empty(); + if (draw_code_completion && code_completion_current_selected >= 0 && code_completion_current_selected < code_completion_options.size()) { + RID item = code_completion_ac_items[code_completion_current_selected]; + if (item.is_valid()) { + return item; + } + } + return TextEdit::get_focused_accessibility_element(); +} + void CodeEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_READY: { @@ -74,6 +86,141 @@ void CodeEdit::_notification(int p_what) { // Avoid having many hidden text editors with unused cache filling up memory. _clear_line_number_text_cache(); } break; + case NOTIFICATION_ACCESSIBILITY_INVALIDATE: { + code_completion_ac_items.clear(); + code_completion_ac_items.resize_initialized(code_completion_options.size()); + code_completion_ac_scroll_element = RID(); + code_completion_ac_root_element = RID(); + } break; + case NOTIFICATION_ACCESSIBILITY_UPDATE: { + RID ae = get_accessibility_element(); + ERR_FAIL_COND(ae.is_null()); + + if (code_completion_ac_root_element.is_null()) { + code_completion_ac_root_element = AccessibilityServer::get_singleton()->create_sub_element(ae, AccessibilityServerEnums::AccessibilityRole::ROLE_MENU); + } + + if (code_completion_ac_scroll_element.is_null()) { + code_completion_ac_scroll_element = AccessibilityServer::get_singleton()->create_sub_element(code_completion_ac_root_element, AccessibilityServerEnums::AccessibilityRole::ROLE_CONTAINER); + } + + bool draw_code_completion = code_completion_active && !code_completion_options.is_empty(); + if (draw_code_completion && code_completion_current_selected >= 0 && code_completion_current_selected < code_completion_options.size()) { + int options_count = code_completion_options.size(); + int row_height = get_line_height(); + const bool draw_code_hint = !code_hint.is_empty(); + + Size2 code_hint_minsize; + if (draw_code_hint) { + const int font_height = theme_cache.font->get_height(theme_cache.font_size); + + Vector code_hint_lines = code_hint.split("\n"); + int line_count = code_hint_lines.size(); + + int max_width = 0; + for (int i = 0; i < line_count; i++) { + max_width = MAX(max_width, theme_cache.font->get_string_size(code_hint_lines[i], HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).x); + } + code_hint_minsize = theme_cache.code_hint_style->get_minimum_size() + Size2(max_width, line_count * font_height + (theme_cache.line_spacing * line_count - 1)); + } + + int lines = MIN(options_count, theme_cache.code_completion_max_lines); + const Size2 icon_area_size(row_height, row_height); + + code_completion_rect.size.width = code_completion_longest_line + theme_cache.code_completion_icon_separation + icon_area_size.width + 2; + code_completion_rect.size.height = lines * row_height; + + const Point2 caret_pos = get_caret_draw_pos(); + int total_height = theme_cache.code_completion_style->get_minimum_size().y + code_completion_rect.size.height; + int min_y = caret_pos.y - row_height; + int max_y = caret_pos.y + row_height + total_height; + if (draw_code_hint) { + if (code_hint_draw_below) { + max_y += code_hint_minsize.y; + } else { + min_y -= code_hint_minsize.y; + } + } + + const bool can_fit_completion_above = min_y > total_height; + const bool can_fit_completion_below = max_y <= get_size().height; + + bool should_place_above = !can_fit_completion_below && can_fit_completion_above; + + if (!can_fit_completion_below && !can_fit_completion_above) { + const int space_above = caret_pos.y - row_height; + const int space_below = get_size().height - caret_pos.y; + should_place_above = space_above > space_below; + + // Reduce the line count and recalculate heights to better fit the completion popup. + int space_avail; + if (should_place_above) { + space_avail = space_above - theme_cache.code_completion_style->get_minimum_size().y; + } else { + space_avail = space_below - theme_cache.code_completion_style->get_minimum_size().y; + } + + int max_lines_fit = MAX(1, space_avail / row_height); + lines = MIN(lines, max_lines_fit); + code_completion_rect.size.height = lines * row_height; + total_height = theme_cache.code_completion_style->get_minimum_size().y + code_completion_rect.size.height; + } + + if (should_place_above) { + code_completion_rect.position.y = (caret_pos.y - total_height - row_height) + theme_cache.line_spacing; + if (draw_code_hint && !code_hint_draw_below) { + code_completion_rect.position.y -= code_hint_minsize.y; + } + } else { + code_completion_rect.position.y = caret_pos.y + (theme_cache.line_spacing / 2.0f); + if (draw_code_hint && code_hint_draw_below) { + code_completion_rect.position.y += code_hint_minsize.y; + } + } + + const int scroll_width = options_count > theme_cache.code_completion_max_lines ? theme_cache.code_completion_scroll_width : 0; + const int code_completion_base_width = theme_cache.font->get_string_size(code_completion_base, HORIZONTAL_ALIGNMENT_LEFT, -1, theme_cache.font_size).width; + if (caret_pos.x - code_completion_base_width + code_completion_rect.size.width + scroll_width > get_size().width) { + code_completion_rect.position.x = get_size().width - code_completion_rect.size.width - scroll_width; + } else { + code_completion_rect.position.x = caret_pos.x - code_completion_base_width; + } + + code_completion_line_ofs = CLAMP((code_completion_force_item_center < 0 ? code_completion_current_selected : code_completion_force_item_center) - lines / 2, 0, options_count - lines); + + Transform2D scroll_xform; + scroll_xform.set_origin(Vector2i(0, -row_height * code_completion_line_ofs)); + AccessibilityServer::get_singleton()->update_set_flag(code_completion_ac_root_element, AccessibilityServerEnums::AccessibilityFlags::FLAG_HIDDEN, false); + AccessibilityServer::get_singleton()->update_set_list_item_count(code_completion_ac_root_element, options_count); + AccessibilityServer::get_singleton()->update_set_bounds(code_completion_ac_root_element, code_completion_rect); + AccessibilityServer::get_singleton()->update_set_name(code_completion_ac_root_element, ETR("Code completion list:")); + + AccessibilityServer::get_singleton()->update_set_flag(code_completion_ac_scroll_element, AccessibilityServerEnums::AccessibilityFlags::FLAG_HIDDEN, false); + AccessibilityServer::get_singleton()->update_set_transform(code_completion_ac_scroll_element, scroll_xform); + AccessibilityServer::get_singleton()->update_set_bounds(code_completion_ac_scroll_element, code_completion_rect); + + for (int i = 0; i < options_count; i++) { + if (code_completion_ac_items[i].is_null()) { + RID item = AccessibilityServer::get_singleton()->create_sub_element(code_completion_ac_scroll_element, AccessibilityServerEnums::AccessibilityRole::ROLE_MENU_ITEM); + code_completion_ac_items.write[i] = item; + } + RID item = code_completion_ac_items[i]; + AccessibilityServer::get_singleton()->update_set_role(item, AccessibilityServerEnums::AccessibilityRole::ROLE_MENU_ITEM); + AccessibilityServer::get_singleton()->update_set_list_item_index(item, i); + AccessibilityServer::get_singleton()->update_set_list_item_level(item, 0); + AccessibilityServer::get_singleton()->update_set_list_item_selected(item, i == code_completion_current_selected); + AccessibilityServer::get_singleton()->update_set_name(item, code_completion_options[i].display); + AccessibilityServer::get_singleton()->update_set_bounds(item, Rect2(code_completion_rect.position.x, code_completion_rect.position.y + row_height * i, code_completion_rect.size.x, row_height)); + } + } else { + AccessibilityServer::get_singleton()->update_set_flag(code_completion_ac_root_element, AccessibilityServerEnums::AccessibilityFlags::FLAG_HIDDEN, true); + AccessibilityServer::get_singleton()->update_set_list_item_count(code_completion_ac_root_element, 0); + AccessibilityServer::get_singleton()->update_set_bounds(code_completion_ac_root_element, Rect2(0, 0, 1, 1)); + + AccessibilityServer::get_singleton()->update_set_flag(code_completion_ac_scroll_element, AccessibilityServerEnums::AccessibilityFlags::FLAG_HIDDEN, true); + AccessibilityServer::get_singleton()->update_set_bounds(code_completion_ac_scroll_element, Rect2(0, 0, 1, 1)); + } + } break; case NOTIFICATION_DRAW: { RID ci = get_text_canvas_item(); @@ -399,6 +546,7 @@ void CodeEdit::gui_input(const Ref &p_gui_input) { confirm_code_completion(); } queue_redraw(); + queue_accessibility_update(); } break; default: break; @@ -417,6 +565,7 @@ void CodeEdit::gui_input(const Ref &p_gui_input) { is_code_completion_scroll_pressed = true; _update_scroll_selected_line(mb->get_position().y); + queue_accessibility_update(); queue_redraw(); } @@ -506,6 +655,7 @@ void CodeEdit::gui_input(const Ref &p_gui_input) { if (is_code_completion_scroll_pressed) { _update_scroll_selected_line(mpos.y); accept_event(); + queue_accessibility_update(); queue_redraw(); return; } @@ -574,6 +724,7 @@ void CodeEdit::gui_input(const Ref &p_gui_input) { } code_completion_force_item_center = -1; code_completion_pan_offset = 0.0f; + queue_accessibility_update(); queue_redraw(); accept_event(); return; @@ -586,6 +737,7 @@ void CodeEdit::gui_input(const Ref &p_gui_input) { } code_completion_force_item_center = -1; code_completion_pan_offset = 0.0f; + queue_accessibility_update(); queue_redraw(); accept_event(); return; @@ -594,6 +746,7 @@ void CodeEdit::gui_input(const Ref &p_gui_input) { code_completion_current_selected = MAX(0, code_completion_current_selected - theme_cache.code_completion_max_lines); code_completion_force_item_center = -1; code_completion_pan_offset = 0.0f; + queue_accessibility_update(); queue_redraw(); accept_event(); return; @@ -602,6 +755,7 @@ void CodeEdit::gui_input(const Ref &p_gui_input) { code_completion_current_selected = MIN(code_completion_options.size() - 1, code_completion_current_selected + theme_cache.code_completion_max_lines); code_completion_force_item_center = -1; code_completion_pan_offset = 0.0f; + queue_accessibility_update(); queue_redraw(); accept_event(); return; @@ -2296,6 +2450,7 @@ void CodeEdit::request_code_completion(bool p_force) { } else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(line[ofs - 2])) { emit_signal(SNAME("code_completion_requested")); } + queue_accessibility_update(); } void CodeEdit::add_code_completion_option(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color, const Ref &p_icon, const Variant &p_value, int p_location) { @@ -2367,6 +2522,7 @@ void CodeEdit::set_code_completion_selected_index(int p_index) { code_completion_current_selected = p_index; code_completion_force_item_center = -1; code_completion_pan_offset = 0.0f; + queue_accessibility_update(); queue_redraw(); } @@ -2498,6 +2654,7 @@ void CodeEdit::cancel_code_completion() { code_completion_forced = false; code_completion_active = false; is_code_completion_drag_started = false; + queue_accessibility_update(); queue_redraw(); } @@ -3649,11 +3806,19 @@ void CodeEdit::_filter_code_completion_candidates_impl() { code_completion_current_selected = 0; code_completion_pan_offset = 0.0f; } + for (RID &E : code_completion_ac_items) { + if (E.is_valid()) { + AccessibilityServer::get_singleton()->free_element(E); + } + } + code_completion_ac_items.clear(); code_completion_options = code_completion_options_new; + code_completion_ac_items.resize_initialized(code_completion_options.size()); code_completion_longest_line = MIN(max_width, theme_cache.code_completion_max_width * theme_cache.font_size); code_completion_force_item_center = -1; code_completion_active = true; + queue_accessibility_update(); queue_redraw(); return; } @@ -3868,11 +4033,19 @@ void CodeEdit::_filter_code_completion_candidates_impl() { code_completion_current_selected = 0; code_completion_pan_offset = 0.0f; } + for (RID &E : code_completion_ac_items) { + if (E.is_valid()) { + AccessibilityServer::get_singleton()->free_element(E); + } + } + code_completion_ac_items.clear(); code_completion_options = code_completion_options_new; + code_completion_ac_items.resize_initialized(code_completion_options.size()); code_completion_longest_line = MIN(max_width, theme_cache.code_completion_max_width * theme_cache.font_size); code_completion_force_item_center = -1; code_completion_active = true; + queue_accessibility_update(); queue_redraw(); } diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index 4cf9ca9feb..0a458929f6 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -213,6 +213,9 @@ private: bool is_code_completion_scroll_pressed = false; bool is_code_completion_drag_started = false; Vector code_completion_options; + Vector code_completion_ac_items; + RID code_completion_ac_scroll_element; + RID code_completion_ac_root_element; int code_completion_line_ofs = 0; int code_completion_current_selected = 0; int code_completion_force_item_center = -1; @@ -336,6 +339,8 @@ protected: virtual void _backspace_internal(int p_caret) override; virtual void _cut_internal(int p_caret) override; + virtual RID get_focused_accessibility_element() const override; + GDVIRTUAL1(_confirm_code_completion, bool) GDVIRTUAL1(_request_code_completion, bool) GDVIRTUAL1RC(TypedArray, _filter_code_completion_candidates, TypedArray)