diff options
Diffstat (limited to 'scene/gui')
78 files changed, 11359 insertions, 7701 deletions
diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index ac067aa001..bef1011364 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -52,7 +52,7 @@ void BaseButton::_unpress_group() { } } -void BaseButton::_gui_input(Ref<InputEvent> p_event) { +void BaseButton::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (status.disabled) { // no interaction with disabled button @@ -98,17 +98,14 @@ void BaseButton::_notification(int p_what) { } if (p_what == NOTIFICATION_FOCUS_ENTER) { - status.hovering = true; update(); } if (p_what == NOTIFICATION_FOCUS_EXIT) { if (status.press_attempt) { status.press_attempt = false; - status.hovering = false; update(); } else if (status.hovering) { - status.hovering = false; update(); } } @@ -124,31 +121,31 @@ void BaseButton::_notification(int p_what) { } void BaseButton::_pressed() { - if (get_script_instance()) { - get_script_instance()->call(SceneStringNames::get_singleton()->_pressed); - } + GDVIRTUAL_CALL(_pressed); pressed(); - emit_signal("pressed"); + emit_signal(SNAME("pressed")); } void BaseButton::_toggled(bool p_pressed) { - if (get_script_instance()) { - get_script_instance()->call(SceneStringNames::get_singleton()->_toggled, p_pressed); - } + GDVIRTUAL_CALL(_toggled, p_pressed); toggled(p_pressed); - emit_signal("toggled", p_pressed); + emit_signal(SNAME("toggled"), p_pressed); } void BaseButton::on_action_event(Ref<InputEvent> p_event) { if (p_event->is_pressed()) { status.press_attempt = true; status.pressing_inside = true; - emit_signal("button_down"); + emit_signal(SNAME("button_down")); } if (status.press_attempt && status.pressing_inside) { if (toggle_mode) { - if ((p_event->is_pressed() && action_mode == ACTION_MODE_BUTTON_PRESS) || (!p_event->is_pressed() && action_mode == ACTION_MODE_BUTTON_RELEASE)) { + bool is_pressed = p_event->is_pressed(); + if (Object::cast_to<InputEventShortcut>(*p_event)) { + is_pressed = false; + } + if ((is_pressed && action_mode == ACTION_MODE_BUTTON_PRESS) || (!is_pressed && action_mode == ACTION_MODE_BUTTON_RELEASE)) { if (action_mode == ACTION_MODE_BUTTON_PRESS) { status.press_attempt = false; status.pressing_inside = false; @@ -156,7 +153,7 @@ void BaseButton::on_action_event(Ref<InputEvent> p_event) { status.pressed = !status.pressed; _unpress_group(); if (button_group.is_valid()) { - button_group->emit_signal("pressed", this); + button_group->emit_signal(SNAME("pressed"), this); } _toggled(status.pressed); _pressed(); @@ -175,10 +172,9 @@ void BaseButton::on_action_event(Ref<InputEvent> p_event) { status.hovering = false; } } - // pressed state should be correct with button_up signal - emit_signal("button_up"); status.press_attempt = false; status.pressing_inside = false; + emit_signal(SNAME("button_up")); } update(); @@ -222,7 +218,7 @@ void BaseButton::set_pressed(bool p_pressed) { if (p_pressed) { _unpress_group(); if (button_group.is_valid()) { - button_group->emit_signal("pressed", this); + button_group->emit_signal(SNAME("pressed"), this); } } _toggled(status.pressed); @@ -230,6 +226,18 @@ void BaseButton::set_pressed(bool p_pressed) { update(); } +void BaseButton::set_pressed_no_signal(bool p_pressed) { + if (!toggle_mode) { + return; + } + if (status.pressed == p_pressed) { + return; + } + status.pressed = p_pressed; + + update(); +} + bool BaseButton::is_pressing() const { return status.press_attempt; } @@ -330,14 +338,14 @@ Ref<Shortcut> BaseButton::get_shortcut() const { return shortcut; } -void BaseButton::_unhandled_key_input(Ref<InputEvent> p_event) { +void BaseButton::unhandled_key_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (!_is_focus_owner_in_shorcut_context()) { return; } - if (!is_disabled() && is_visible_in_tree() && !p_event->is_echo() && shortcut.is_valid() && shortcut->is_shortcut(p_event)) { + if (!is_disabled() && is_visible_in_tree() && !p_event->is_echo() && shortcut.is_valid() && shortcut->matches_event(p_event)) { on_action_event(p_event); accept_event(); } @@ -345,7 +353,7 @@ void BaseButton::_unhandled_key_input(Ref<InputEvent> p_event) { String BaseButton::get_tooltip(const Point2 &p_pos) const { String tooltip = Control::get_tooltip(p_pos); - if (shortcut_in_tooltip && shortcut.is_valid() && shortcut->is_valid()) { + if (shortcut_in_tooltip && shortcut.is_valid() && shortcut->has_valid_event()) { String text = shortcut->get_name() + " (" + shortcut->get_as_text() + ")"; if (tooltip != String() && shortcut->get_name().nocasecmp_to(tooltip) != 0) { text += "\n" + tooltip; @@ -395,14 +403,13 @@ bool BaseButton::_is_focus_owner_in_shorcut_context() const { Control *vp_focus = get_focus_owner(); // If the context is valid and the viewport focus is valid, check if the context is the focus or is a parent of it. - return ctx_node && vp_focus && (ctx_node == vp_focus || ctx_node->is_a_parent_of(vp_focus)); + return ctx_node && vp_focus && (ctx_node == vp_focus || ctx_node->is_ancestor_of(vp_focus)); } void BaseButton::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &BaseButton::_gui_input); - ClassDB::bind_method(D_METHOD("_unhandled_key_input"), &BaseButton::_unhandled_key_input); ClassDB::bind_method(D_METHOD("set_pressed", "pressed"), &BaseButton::set_pressed); ClassDB::bind_method(D_METHOD("is_pressed"), &BaseButton::is_pressed); + ClassDB::bind_method(D_METHOD("set_pressed_no_signal", "pressed"), &BaseButton::set_pressed_no_signal); ClassDB::bind_method(D_METHOD("is_hovered"), &BaseButton::is_hovered); ClassDB::bind_method(D_METHOD("set_toggle_mode", "enabled"), &BaseButton::set_toggle_mode); ClassDB::bind_method(D_METHOD("is_toggle_mode"), &BaseButton::is_toggle_mode); @@ -427,8 +434,8 @@ void BaseButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_shortcut_context", "node"), &BaseButton::set_shortcut_context); ClassDB::bind_method(D_METHOD("get_shortcut_context"), &BaseButton::get_shortcut_context); - BIND_VMETHOD(MethodInfo("_pressed")); - BIND_VMETHOD(MethodInfo("_toggled", PropertyInfo(Variant::BOOL, "button_pressed"))); + GDVIRTUAL_BIND(_pressed); + GDVIRTUAL_BIND(_toggled, "button_pressed"); ADD_SIGNAL(MethodInfo("pressed")); ADD_SIGNAL(MethodInfo("button_up")); diff --git a/scene/gui/base_button.h b/scene/gui/base_button.h index 6c7a8f3433..cd6e78464f 100644 --- a/scene/gui/base_button.h +++ b/scene/gui/base_button.h @@ -75,12 +75,15 @@ protected: virtual void pressed(); virtual void toggled(bool p_pressed); static void _bind_methods(); - virtual void _gui_input(Ref<InputEvent> p_event); - virtual void _unhandled_key_input(Ref<InputEvent> p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; + virtual void unhandled_key_input(const Ref<InputEvent> &p_event) override; void _notification(int p_what); bool _is_focus_owner_in_shorcut_context() const; + GDVIRTUAL0(_pressed) + GDVIRTUAL1(_toggled, bool) + public: enum DrawMode { DRAW_NORMAL, @@ -98,7 +101,8 @@ public: bool is_pressing() const; ///< return whether button is pressed (toggled in) bool is_hovered() const; - void set_pressed(bool p_pressed); ///only works in toggle mode + void set_pressed(bool p_pressed); // Only works in toggle mode. + void set_pressed_no_signal(bool p_pressed); void set_toggle_mode(bool p_on); bool is_toggle_mode() const; diff --git a/scene/gui/box_container.cpp b/scene/gui/box_container.cpp index 7407ad5b8f..cf2df4e1a2 100644 --- a/scene/gui/box_container.cpp +++ b/scene/gui/box_container.cpp @@ -43,7 +43,7 @@ void BoxContainer::_resort() { Size2i new_size = get_size(); - int sep = get_theme_constant("separation"); //,vertical?"VBoxContainer":"HBoxContainer"); + int sep = get_theme_constant(SNAME("separation")); //,vertical?"VBoxContainer":"HBoxContainer"); bool rtl = is_layout_rtl(); bool first = true; @@ -247,7 +247,7 @@ Size2 BoxContainer::get_minimum_size() const { /* Calculate MINIMUM SIZE */ Size2i minimum; - int sep = get_theme_constant("separation"); //,vertical?"VBoxContainer":"HBoxContainer"); + int sep = get_theme_constant(SNAME("separation")); //,vertical?"VBoxContainer":"HBoxContainer"); bool first = true; @@ -349,12 +349,13 @@ void BoxContainer::_bind_methods() { MarginContainer *VBoxContainer::add_margin_child(const String &p_label, Control *p_control, bool p_expand) { Label *l = memnew(Label); + l->set_theme_type_variation("HeaderSmall"); l->set_text(p_label); - add_child(l); + add_child(l, false, INTERNAL_MODE_FRONT); MarginContainer *mc = memnew(MarginContainer); mc->add_theme_constant_override("margin_left", 0); mc->add_child(p_control); - add_child(mc); + add_child(mc, false, INTERNAL_MODE_FRONT); if (p_expand) { mc->set_v_size_flags(SIZE_EXPAND_FILL); } diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp index c0df5271b4..9cdf3bf210 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -41,27 +41,32 @@ Size2 Button::get_minimum_size() const { if (!expand_icon) { Ref<Texture2D> _icon; - if (icon.is_null() && has_theme_icon("icon")) { - _icon = Control::get_theme_icon("icon"); + if (icon.is_null() && has_theme_icon(SNAME("icon"))) { + _icon = Control::get_theme_icon(SNAME("icon")); } else { _icon = icon; } if (!_icon.is_null()) { minsize.height = MAX(minsize.height, _icon->get_height()); - minsize.width += _icon->get_width(); - if (xl_text != "") { - minsize.width += get_theme_constant("hseparation"); + + if (icon_align != ALIGN_CENTER) { + minsize.width += _icon->get_width(); + if (xl_text != "") { + minsize.width += get_theme_constant(SNAME("hseparation")); + } + } else { + minsize.width = MAX(minsize.width, _icon->get_width()); } } } - Ref<Font> font = get_theme_font("font"); - float font_height = font->get_height(get_theme_font_size("font_size")); + Ref<Font> font = get_theme_font(SNAME("font")); + float font_height = font->get_height(get_theme_font_size(SNAME("font_size"))); minsize.height = MAX(font_height, minsize.height); - return get_theme_stylebox("normal")->get_minimum_size() + minsize; + return get_theme_stylebox(SNAME("normal"))->get_minimum_size() + minsize; } void Button::_set_internal_margin(Side p_side, float p_value) { @@ -74,7 +79,7 @@ void Button::_notification(int p_what) { update(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { - xl_text = tr(text); + xl_text = atr(text); _shape(); minimum_size_changed(); @@ -92,43 +97,43 @@ void Button::_notification(int p_what) { Color color; Color color_icon(1, 1, 1, 1); - Ref<StyleBox> style = get_theme_stylebox("normal"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); bool rtl = is_layout_rtl(); switch (get_draw_mode()) { case DRAW_NORMAL: { - if (rtl && has_theme_stylebox("normal_mirrored")) { - style = get_theme_stylebox("normal_mirrored"); + if (rtl && has_theme_stylebox(SNAME("normal_mirrored"))) { + style = get_theme_stylebox(SNAME("normal_mirrored")); } else { - style = get_theme_stylebox("normal"); + style = get_theme_stylebox(SNAME("normal")); } if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - color = get_theme_color("font_color"); - if (has_theme_color("icon_normal_color")) { - color_icon = get_theme_color("icon_normal_color"); + color = get_theme_color(SNAME("font_color")); + if (has_theme_color(SNAME("icon_normal_color"))) { + color_icon = get_theme_color(SNAME("icon_normal_color")); } } break; case DRAW_HOVER_PRESSED: { - if (has_theme_stylebox("hover_pressed") && has_theme_stylebox_override("hover_pressed")) { - if (rtl && has_theme_stylebox("hover_pressed_mirrored")) { - style = get_theme_stylebox("hover_pressed_mirrored"); + if (has_theme_stylebox(SNAME("hover_pressed")) && has_theme_stylebox_override("hover_pressed")) { + if (rtl && has_theme_stylebox(SNAME("hover_pressed_mirrored"))) { + style = get_theme_stylebox(SNAME("hover_pressed_mirrored")); } else { - style = get_theme_stylebox("hover_pressed"); + style = get_theme_stylebox(SNAME("hover_pressed")); } if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - if (has_theme_color("font_hover_pressed_color")) { - color = get_theme_color("font_hover_pressed_color"); + if (has_theme_color(SNAME("font_hover_pressed_color"))) { + color = get_theme_color(SNAME("font_hover_pressed_color")); } else { - color = get_theme_color("font_color"); + color = get_theme_color(SNAME("font_color")); } - if (has_theme_color("icon_hover_pressed_color")) { - color_icon = get_theme_color("icon_hover_pressed_color"); + if (has_theme_color(SNAME("icon_hover_pressed_color"))) { + color_icon = get_theme_color(SNAME("icon_hover_pressed_color")); } break; @@ -136,72 +141,87 @@ void Button::_notification(int p_what) { [[fallthrough]]; } case DRAW_PRESSED: { - if (rtl && has_theme_stylebox("pressed_mirrored")) { - style = get_theme_stylebox("pressed_mirrored"); + if (rtl && has_theme_stylebox(SNAME("pressed_mirrored"))) { + style = get_theme_stylebox(SNAME("pressed_mirrored")); } else { - style = get_theme_stylebox("pressed"); + style = get_theme_stylebox(SNAME("pressed")); } if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - if (has_theme_color("font_pressed_color")) { - color = get_theme_color("font_pressed_color"); + if (has_theme_color(SNAME("font_pressed_color"))) { + color = get_theme_color(SNAME("font_pressed_color")); } else { - color = get_theme_color("font_color"); + color = get_theme_color(SNAME("font_color")); } - if (has_theme_color("icon_pressed_color")) { - color_icon = get_theme_color("icon_pressed_color"); + if (has_theme_color(SNAME("icon_pressed_color"))) { + color_icon = get_theme_color(SNAME("icon_pressed_color")); } } break; case DRAW_HOVER: { - if (rtl && has_theme_stylebox("hover_mirrored")) { - style = get_theme_stylebox("hover_mirrored"); + if (rtl && has_theme_stylebox(SNAME("hover_mirrored"))) { + style = get_theme_stylebox(SNAME("hover_mirrored")); } else { - style = get_theme_stylebox("hover"); + style = get_theme_stylebox(SNAME("hover")); } if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - color = get_theme_color("font_hover_color"); - if (has_theme_color("icon_hover_color")) { - color_icon = get_theme_color("icon_hover_color"); + color = get_theme_color(SNAME("font_hover_color")); + if (has_theme_color(SNAME("icon_hover_color"))) { + color_icon = get_theme_color(SNAME("icon_hover_color")); } } break; case DRAW_DISABLED: { - if (rtl && has_theme_stylebox("disabled_mirrored")) { - style = get_theme_stylebox("disabled_mirrored"); + if (rtl && has_theme_stylebox(SNAME("disabled_mirrored"))) { + style = get_theme_stylebox(SNAME("disabled_mirrored")); } else { - style = get_theme_stylebox("disabled"); + style = get_theme_stylebox(SNAME("disabled")); } if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - color = get_theme_color("font_disabled_color"); - if (has_theme_color("icon_disabled_color")) { - color_icon = get_theme_color("icon_disabled_color"); + color = get_theme_color(SNAME("font_disabled_color")); + if (has_theme_color(SNAME("icon_disabled_color"))) { + color_icon = get_theme_color(SNAME("icon_disabled_color")); } } break; } if (has_focus()) { - Ref<StyleBox> style2 = get_theme_stylebox("focus"); + Ref<StyleBox> style2 = get_theme_stylebox(SNAME("focus")); style2->draw(ci, Rect2(Point2(), size)); } Ref<Texture2D> _icon; - if (icon.is_null() && has_theme_icon("icon")) { - _icon = Control::get_theme_icon("icon"); + if (icon.is_null() && has_theme_icon(SNAME("icon"))) { + _icon = Control::get_theme_icon(SNAME("icon")); } else { _icon = icon; } Rect2 icon_region = Rect2(); + TextAlign icon_align_rtl_checked = icon_align; + TextAlign align_rtl_checked = align; + // Swap icon and text alignment sides if right-to-left layout is set. + if (rtl) { + if (icon_align == ALIGN_RIGHT) { + icon_align_rtl_checked = ALIGN_LEFT; + } else if (icon_align == ALIGN_LEFT) { + icon_align_rtl_checked = ALIGN_RIGHT; + } + if (align == ALIGN_RIGHT) { + align_rtl_checked = ALIGN_LEFT; + } else if (align == ALIGN_LEFT) { + align_rtl_checked = ALIGN_RIGHT; + } + } if (!_icon.is_null()) { int valign = size.height - style->get_minimum_size().y; if (is_disabled()) { @@ -209,20 +229,27 @@ void Button::_notification(int p_what) { } float icon_ofs_region = 0.0; - if (rtl) { - if (_internal_margin[SIDE_RIGHT] > 0) { - icon_ofs_region = _internal_margin[SIDE_RIGHT] + get_theme_constant("hseparation"); - } - } else { + Point2 style_offset; + Size2 icon_size = _icon->get_size(); + if (icon_align_rtl_checked == ALIGN_LEFT) { + style_offset.x = style->get_margin(SIDE_LEFT); if (_internal_margin[SIDE_LEFT] > 0) { - icon_ofs_region = _internal_margin[SIDE_LEFT] + get_theme_constant("hseparation"); + icon_ofs_region = _internal_margin[SIDE_LEFT] + get_theme_constant(SNAME("hseparation")); + } + } else if (icon_align_rtl_checked == ALIGN_CENTER) { + style_offset.x = 0.0; + } else if (icon_align_rtl_checked == ALIGN_RIGHT) { + style_offset.x = -style->get_margin(SIDE_RIGHT); + if (_internal_margin[SIDE_RIGHT] > 0) { + icon_ofs_region = -_internal_margin[SIDE_RIGHT] - get_theme_constant(SNAME("hseparation")); } } + style_offset.y = style->get_margin(SIDE_TOP); if (expand_icon) { Size2 _size = get_size() - style->get_offset() * 2; - _size.width -= get_theme_constant("hseparation") + icon_ofs_region; - if (!clip_text) { + _size.width -= get_theme_constant(SNAME("hseparation")) + icon_ofs_region; + if (!clip_text && icon_align_rtl_checked != ALIGN_CENTER) { _size.width -= text_buf->get_size().width; } float icon_width = _icon->get_width() * _size.height / _icon->get_height(); @@ -233,49 +260,49 @@ void Button::_notification(int p_what) { icon_height = _icon->get_height() * icon_width / _icon->get_width(); } - if (rtl) { - icon_region = Rect2(Point2(size.width - (icon_ofs_region + icon_width + style->get_margin(SIDE_RIGHT)), style->get_margin(SIDE_TOP) + (_size.height - icon_height) / 2), Size2(icon_width, icon_height)); - } else { - icon_region = Rect2(style->get_offset() + Point2(icon_ofs_region, (_size.height - icon_height) / 2), Size2(icon_width, icon_height)); - } + icon_size = Size2(icon_width, icon_height); + } + + if (icon_align_rtl_checked == ALIGN_LEFT) { + icon_region = Rect2(style_offset + Point2(icon_ofs_region, Math::floor((valign - icon_size.y) * 0.5)), icon_size); + } else if (icon_align_rtl_checked == ALIGN_CENTER) { + icon_region = Rect2(style_offset + Point2(icon_ofs_region + Math::floor((size.x - icon_size.x) * 0.5), Math::floor((valign - icon_size.y) * 0.5)), icon_size); } else { - if (rtl) { - icon_region = Rect2(Point2(size.width - (icon_ofs_region + _icon->get_size().width + style->get_margin(SIDE_RIGHT)), style->get_margin(SIDE_TOP) + Math::floor((valign - _icon->get_height()) / 2.0)), _icon->get_size()); - } else { - icon_region = Rect2(style->get_offset() + Point2(icon_ofs_region, Math::floor((valign - _icon->get_height()) / 2.0)), _icon->get_size()); - } + icon_region = Rect2(style_offset + Point2(icon_ofs_region + size.x - icon_size.x, Math::floor((valign - icon_size.y) * 0.5)), icon_size); + } + + if (icon_region.size.width > 0) { + draw_texture_rect_region(_icon, icon_region, Rect2(Point2(), _icon->get_size()), color_icon); } } - Point2 icon_ofs = !_icon.is_null() ? Point2(icon_region.size.width + get_theme_constant("hseparation"), 0) : Point2(); + Point2 icon_ofs = !_icon.is_null() ? Point2(icon_region.size.width + get_theme_constant(SNAME("hseparation")), 0) : Point2(); + if (align_rtl_checked == ALIGN_CENTER && icon_align_rtl_checked == ALIGN_CENTER) { + icon_ofs.x = 0.0; + } int text_clip = size.width - style->get_minimum_size().width - icon_ofs.width; text_buf->set_width(clip_text ? text_clip : -1); int text_width = clip_text ? MIN(text_clip, text_buf->get_size().x) : text_buf->get_size().x; if (_internal_margin[SIDE_LEFT] > 0) { - text_clip -= _internal_margin[SIDE_LEFT] + get_theme_constant("hseparation"); + text_clip -= _internal_margin[SIDE_LEFT] + get_theme_constant(SNAME("hseparation")); } if (_internal_margin[SIDE_RIGHT] > 0) { - text_clip -= _internal_margin[SIDE_RIGHT] + get_theme_constant("hseparation"); + text_clip -= _internal_margin[SIDE_RIGHT] + get_theme_constant(SNAME("hseparation")); } Point2 text_ofs = (size - style->get_minimum_size() - icon_ofs - text_buf->get_size() - Point2(_internal_margin[SIDE_RIGHT] - _internal_margin[SIDE_LEFT], 0)) / 2.0; - switch (align) { + switch (align_rtl_checked) { case ALIGN_LEFT: { - if (rtl) { - if (_internal_margin[SIDE_RIGHT] > 0) { - text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width - _internal_margin[SIDE_RIGHT] - get_theme_constant("hseparation"); - } else { - text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width; - } + if (icon_align_rtl_checked != ALIGN_LEFT) { + icon_ofs.x = 0.0; + } + if (_internal_margin[SIDE_LEFT] > 0) { + text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x + _internal_margin[SIDE_LEFT] + get_theme_constant(SNAME("hseparation")); } else { - if (_internal_margin[SIDE_LEFT] > 0) { - text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x + _internal_margin[SIDE_LEFT] + get_theme_constant("hseparation"); - } else { - text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x; - } + text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x; } text_ofs.y += style->get_offset().y; } break; @@ -283,49 +310,38 @@ void Button::_notification(int p_what) { if (text_ofs.x < 0) { text_ofs.x = 0; } - text_ofs += icon_ofs; + if (icon_align_rtl_checked == ALIGN_LEFT) { + text_ofs += icon_ofs; + } text_ofs += style->get_offset(); } break; case ALIGN_RIGHT: { - if (rtl) { - if (_internal_margin[SIDE_LEFT] > 0) { - text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x + _internal_margin[SIDE_LEFT] + get_theme_constant("hseparation"); - } else { - text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x; - } + if (_internal_margin[SIDE_RIGHT] > 0) { + text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width - _internal_margin[SIDE_RIGHT] - get_theme_constant(SNAME("hseparation")); } else { - if (_internal_margin[SIDE_RIGHT] > 0) { - text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width - _internal_margin[SIDE_RIGHT] - get_theme_constant("hseparation"); - } else { - text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width; - } + text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width; } text_ofs.y += style->get_offset().y; + if (icon_align_rtl_checked == ALIGN_RIGHT) { + text_ofs.x -= icon_ofs.x; + } } break; } - if (rtl) { - text_ofs.x -= icon_ofs.x; - } - - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); if (outline_size > 0 && font_outline_color.a > 0) { text_buf->draw_outline(ci, text_ofs, outline_size, font_outline_color); } text_buf->draw(ci, text_ofs, color); - - if (!_icon.is_null() && icon_region.size.width > 0) { - draw_texture_rect_region(_icon, icon_region, Rect2(Point2(), _icon->get_size()), color_icon); - } } break; } } void Button::_shape() { - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); text_buf->clear(); if (text_direction == Control::TEXT_DIRECTION_INHERITED) { @@ -339,7 +355,7 @@ void Button::_shape() { void Button::set_text(const String &p_text) { if (text != p_text) { text = p_text; - xl_text = tr(text); + xl_text = atr(text); _shape(); update(); @@ -457,6 +473,16 @@ Button::TextAlign Button::get_text_align() const { return align; } +void Button::set_icon_align(TextAlign p_align) { + icon_align = p_align; + minimum_size_changed(); + update(); +} + +Button::TextAlign Button::get_icon_align() const { + return icon_align; +} + bool Button::_set(const StringName &p_name, const Variant &p_value) { String str = p_name; if (str.begins_with("opentype_features/")) { @@ -519,14 +545,16 @@ void Button::_bind_methods() { ClassDB::bind_method(D_METHOD("get_language"), &Button::get_language); ClassDB::bind_method(D_METHOD("set_button_icon", "texture"), &Button::set_icon); ClassDB::bind_method(D_METHOD("get_button_icon"), &Button::get_icon); - ClassDB::bind_method(D_METHOD("set_expand_icon"), &Button::set_expand_icon); - ClassDB::bind_method(D_METHOD("is_expand_icon"), &Button::is_expand_icon); ClassDB::bind_method(D_METHOD("set_flat", "enabled"), &Button::set_flat); + ClassDB::bind_method(D_METHOD("is_flat"), &Button::is_flat); ClassDB::bind_method(D_METHOD("set_clip_text", "enabled"), &Button::set_clip_text); ClassDB::bind_method(D_METHOD("get_clip_text"), &Button::get_clip_text); ClassDB::bind_method(D_METHOD("set_text_align", "align"), &Button::set_text_align); ClassDB::bind_method(D_METHOD("get_text_align"), &Button::get_text_align); - ClassDB::bind_method(D_METHOD("is_flat"), &Button::is_flat); + ClassDB::bind_method(D_METHOD("set_icon_align", "icon_align"), &Button::set_icon_align); + ClassDB::bind_method(D_METHOD("get_icon_align"), &Button::get_icon_align); + ClassDB::bind_method(D_METHOD("set_expand_icon"), &Button::set_expand_icon); + ClassDB::bind_method(D_METHOD("is_expand_icon"), &Button::is_expand_icon); BIND_ENUM_CONSTANT(ALIGN_LEFT); BIND_ENUM_CONSTANT(ALIGN_CENTER); @@ -539,11 +567,12 @@ void Button::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flat"), "set_flat", "is_flat"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_text"), "set_clip_text", "get_clip_text"); ADD_PROPERTY(PropertyInfo(Variant::INT, "align", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_text_align", "get_text_align"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "icon_align", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_icon_align", "get_icon_align"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand_icon"), "set_expand_icon", "is_expand_icon"); } Button::Button(const String &p_text) { - text_buf.instance(); + text_buf.instantiate(); text_buf->set_flags(TextServer::BREAK_MANDATORY); set_mouse_filter(MOUSE_FILTER_STOP); diff --git a/scene/gui/button.h b/scene/gui/button.h index d968f63f51..253d079680 100644 --- a/scene/gui/button.h +++ b/scene/gui/button.h @@ -58,6 +58,7 @@ private: bool expand_icon = false; bool clip_text = false; TextAlign align = ALIGN_CENTER; + TextAlign icon_align = ALIGN_LEFT; float _internal_margin[4] = {}; void _shape(); @@ -102,6 +103,9 @@ public: void set_text_align(TextAlign p_align); TextAlign get_text_align() const; + void set_icon_align(TextAlign p_align); + TextAlign get_icon_align() const; + Button(const String &p_text = String()); ~Button(); }; diff --git a/scene/gui/check_box.cpp b/scene/gui/check_box.cpp index c0650a8f3f..411fb2e1f0 100644 --- a/scene/gui/check_box.cpp +++ b/scene/gui/check_box.cpp @@ -33,12 +33,14 @@ #include "servers/rendering_server.h" Size2 CheckBox::get_icon_size() const { - Ref<Texture2D> checked = Control::get_theme_icon("checked"); - Ref<Texture2D> checked_disabled = Control::get_theme_icon("checked_disabled"); - Ref<Texture2D> unchecked = Control::get_theme_icon("unchecked"); - Ref<Texture2D> unchecked_disabled = Control::get_theme_icon("unchecked_disabled"); - Ref<Texture2D> radio_checked = Control::get_theme_icon("radio_checked"); - Ref<Texture2D> radio_unchecked = Control::get_theme_icon("radio_unchecked"); + Ref<Texture2D> checked = Control::get_theme_icon(SNAME("checked")); + Ref<Texture2D> unchecked = Control::get_theme_icon(SNAME("unchecked")); + Ref<Texture2D> radio_checked = Control::get_theme_icon(SNAME("radio_checked")); + Ref<Texture2D> radio_unchecked = Control::get_theme_icon(SNAME("radio_unchecked")); + Ref<Texture2D> checked_disabled = Control::get_theme_icon(SNAME("checked_disabled")); + Ref<Texture2D> unchecked_disabled = Control::get_theme_icon(SNAME("unchecked_disabled")); + Ref<Texture2D> radio_checked_disabled = Control::get_theme_icon(SNAME("radio_checked_disabled")); + Ref<Texture2D> radio_unchecked_disabled = Control::get_theme_icon(SNAME("radio_unchecked_disabled")); Size2 tex_size = Size2(0, 0); if (!checked.is_null()) { @@ -53,6 +55,18 @@ Size2 CheckBox::get_icon_size() const { if (!radio_unchecked.is_null()) { tex_size = Size2(MAX(tex_size.width, radio_unchecked->get_width()), MAX(tex_size.height, radio_unchecked->get_height())); } + if (!checked_disabled.is_null()) { + tex_size = Size2(MAX(tex_size.width, checked_disabled->get_width()), MAX(tex_size.height, checked_disabled->get_height())); + } + if (!unchecked_disabled.is_null()) { + tex_size = Size2(MAX(tex_size.width, unchecked_disabled->get_width()), MAX(tex_size.height, unchecked_disabled->get_height())); + } + if (!radio_checked_disabled.is_null()) { + tex_size = Size2(MAX(tex_size.width, radio_checked_disabled->get_width()), MAX(tex_size.height, radio_checked_disabled->get_height())); + } + if (!radio_unchecked_disabled.is_null()) { + tex_size = Size2(MAX(tex_size.width, radio_unchecked_disabled->get_width()), MAX(tex_size.height, radio_unchecked_disabled->get_height())); + } return tex_size; } @@ -61,9 +75,9 @@ Size2 CheckBox::get_minimum_size() const { Size2 tex_size = get_icon_size(); minsize.width += tex_size.width; if (get_text().length() > 0) { - minsize.width += get_theme_constant("hseparation"); + minsize.width += get_theme_constant(SNAME("hseparation")); } - Ref<StyleBox> sb = get_theme_stylebox("normal"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("normal")); minsize.height = MAX(minsize.height, tex_size.height + sb->get_margin(SIDE_TOP) + sb->get_margin(SIDE_BOTTOM)); return minsize; @@ -83,7 +97,7 @@ void CheckBox::_notification(int p_what) { Ref<Texture2D> on = Control::get_theme_icon(vformat("%s%s", is_radio() ? "radio_checked" : "checked", is_disabled() ? "_disabled" : "")); Ref<Texture2D> off = Control::get_theme_icon(vformat("%s%s", is_radio() ? "radio_unchecked" : "unchecked", is_disabled() ? "_disabled" : "")); - Ref<StyleBox> sb = get_theme_stylebox("normal"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("normal")); Vector2 ofs; if (is_layout_rtl()) { @@ -91,7 +105,7 @@ void CheckBox::_notification(int p_what) { } else { ofs.x = sb->get_margin(SIDE_LEFT); } - ofs.y = int((get_size().height - get_icon_size().height) / 2) + get_theme_constant("check_vadjust"); + ofs.y = int((get_size().height - get_icon_size().height) / 2) + get_theme_constant(SNAME("check_vadjust")); if (is_pressed()) { on->draw(ci, ofs); diff --git a/scene/gui/check_button.cpp b/scene/gui/check_button.cpp index a8bf449355..162a256d23 100644 --- a/scene/gui/check_button.cpp +++ b/scene/gui/check_button.cpp @@ -52,9 +52,9 @@ Size2 CheckButton::get_minimum_size() const { Size2 tex_size = get_icon_size(); minsize.width += tex_size.width; if (get_text().length() > 0) { - minsize.width += get_theme_constant("hseparation"); + minsize.width += get_theme_constant(SNAME("hseparation")); } - Ref<StyleBox> sb = get_theme_stylebox("normal"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("normal")); minsize.height = MAX(minsize.height, tex_size.height + sb->get_margin(SIDE_TOP) + sb->get_margin(SIDE_BOTTOM)); return minsize; @@ -86,7 +86,7 @@ void CheckButton::_notification(int p_what) { off = Control::get_theme_icon(is_disabled() ? "off_disabled" : "off"); } - Ref<StyleBox> sb = get_theme_stylebox("normal"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("normal")); Vector2 ofs; Size2 tex_size = get_icon_size(); @@ -95,7 +95,7 @@ void CheckButton::_notification(int p_what) { } else { ofs.x = get_size().width - (tex_size.width + sb->get_margin(SIDE_RIGHT)); } - ofs.y = (get_size().height - tex_size.height) / 2 + get_theme_constant("check_vadjust"); + ofs.y = (get_size().height - tex_size.height) / 2 + get_theme_constant(SNAME("check_vadjust")); if (is_pressed()) { on->draw(ci, ofs); diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index 28a0ea0100..e7769f9372 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -30,34 +30,1088 @@ #include "code_edit.h" +#include "core/os/keyboard.h" +#include "core/string/string_builder.h" +#include "core/string/ustring.h" + +static bool _is_whitespace(char32_t c) { + return c == '\t' || c == ' '; +} + +static bool _is_char(char32_t c) { + return !is_symbol(c); +} + void CodeEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_ENTER_TREE: { - set_gutter_width(main_gutter, get_row_height()); - set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0', 0, cache.font_size).width); - set_gutter_width(fold_gutter, get_row_height() / 1.2); + style_normal = get_theme_stylebox(SNAME("normal")); + + font = get_theme_font(SNAME("font")); + font_size = get_theme_font_size(SNAME("font_size")); + + line_spacing = get_theme_constant(SNAME("line_spacing")); - breakpoint_color = get_theme_color("breakpoint_color"); - breakpoint_icon = get_theme_icon("breakpoint"); + set_gutter_width(main_gutter, get_line_height()); + set_gutter_width(line_number_gutter, (line_number_digits + 1) * font->get_char_size('0', 0, font_size).width); + set_gutter_width(fold_gutter, get_line_height() / 1.2); - bookmark_color = get_theme_color("bookmark_color"); - bookmark_icon = get_theme_icon("bookmark"); + breakpoint_color = get_theme_color(SNAME("breakpoint_color")); + breakpoint_icon = get_theme_icon(SNAME("breakpoint")); - executing_line_color = get_theme_color("executing_line_color"); - executing_line_icon = get_theme_icon("executing_line"); + bookmark_color = get_theme_color(SNAME("bookmark_color")); + bookmark_icon = get_theme_icon(SNAME("bookmark")); - line_number_color = get_theme_color("line_number_color"); + executing_line_color = get_theme_color(SNAME("executing_line_color")); + executing_line_icon = get_theme_icon(SNAME("executing_line")); - folding_color = get_theme_color("code_folding_color"); - can_fold_icon = get_theme_icon("can_fold"); - folded_icon = get_theme_icon("folded"); + line_number_color = get_theme_color(SNAME("line_number_color")); + + folding_color = get_theme_color(SNAME("code_folding_color")); + can_fold_icon = get_theme_icon(SNAME("can_fold")); + folded_icon = get_theme_icon(SNAME("folded")); + + code_completion_max_width = get_theme_constant(SNAME("completion_max_width")); + code_completion_max_lines = get_theme_constant(SNAME("completion_lines")); + code_completion_scroll_width = get_theme_constant(SNAME("completion_scroll_width")); + code_completion_scroll_color = get_theme_color(SNAME("completion_scroll_color")); + code_completion_background_color = get_theme_color(SNAME("completion_background_color")); + code_completion_selected_color = get_theme_color(SNAME("completion_selected_color")); + code_completion_existing_color = get_theme_color(SNAME("completion_existing_color")); + + line_length_guideline_color = get_theme_color(SNAME("line_length_guideline_color")); } break; case NOTIFICATION_DRAW: { + RID ci = get_canvas_item(); + const Size2 size = get_size(); + const bool caret_visible = is_caret_visible(); + const bool rtl = is_layout_rtl(); + const int row_height = get_line_height(); + + if (line_length_guideline_columns.size() > 0) { + const int xmargin_beg = style_normal->get_margin(SIDE_LEFT) + get_total_gutter_width(); + const int xmargin_end = size.width - style_normal->get_margin(SIDE_RIGHT) - (is_drawing_minimap() ? get_minimap_width() : 0); + const int char_size = (int)font->get_char_size('0', 0, font_size).width; + + for (int i = 0; i < line_length_guideline_columns.size(); i++) { + const int xoffset = xmargin_beg + char_size * (int)line_length_guideline_columns[i] - get_h_scroll(); + if (xoffset > xmargin_beg && xoffset < xmargin_end) { + Color guideline_color = (i == 0) ? line_length_guideline_color : line_length_guideline_color * Color(1, 1, 1, 0.5); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(size.width - xoffset, 0), Point2(size.width - xoffset, size.height), guideline_color); + continue; + } + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(xoffset, 0), Point2(xoffset, size.height), guideline_color); + } + } + } + + bool code_completion_below = false; + if (caret_visible && code_completion_active && code_completion_options.size() > 0) { + Ref<StyleBox> csb = get_theme_stylebox(SNAME("completion")); + + const int code_completion_options_count = code_completion_options.size(); + const int lines = MIN(code_completion_options_count, code_completion_max_lines); + const int icon_hsep = get_theme_constant(SNAME("hseparation"), SNAME("ItemList")); + const Size2 icon_area_size(row_height, row_height); + + code_completion_rect.size.width = code_completion_longest_line + icon_hsep + icon_area_size.width + 2; + code_completion_rect.size.height = lines * row_height; + + const Point2 caret_pos = get_caret_draw_pos(); + const int total_height = csb->get_minimum_size().y + code_completion_rect.size.height; + if (caret_pos.y + row_height + total_height > get_size().height) { + code_completion_rect.position.y = (caret_pos.y - total_height - row_height) + line_spacing; + } else { + code_completion_rect.position.y = caret_pos.y + (line_spacing / 2.0f); + code_completion_below = true; + } + + const int scroll_width = code_completion_options_count > code_completion_max_lines ? code_completion_scroll_width : 0; + const int code_completion_base_width = font->get_string_size(code_completion_base).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; + } + + draw_style_box(csb, Rect2(code_completion_rect.position - csb->get_offset(), code_completion_rect.size + csb->get_minimum_size() + Size2(scroll_width, 0))); + if (code_completion_background_color.a > 0.01) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(code_completion_rect.position, code_completion_rect.size + Size2(scroll_width, 0)), code_completion_background_color); + } + + code_completion_line_ofs = CLAMP(code_completion_current_selected - lines / 2, 0, code_completion_options_count - lines); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(code_completion_rect.position.x, code_completion_rect.position.y + (code_completion_current_selected - code_completion_line_ofs) * row_height), Size2(code_completion_rect.size.width, row_height)), code_completion_selected_color); + draw_rect(Rect2(code_completion_rect.position + Vector2(icon_area_size.x + icon_hsep, 0), Size2(MIN(code_completion_base_width, code_completion_rect.size.width - (icon_area_size.x + icon_hsep)), code_completion_rect.size.height)), code_completion_existing_color); + + for (int i = 0; i < lines; i++) { + int l = code_completion_line_ofs + i; + ERR_CONTINUE(l < 0 || l >= code_completion_options_count); + + Ref<TextLine> tl; + tl.instantiate(); + tl->add_string(code_completion_options[l].display, font, font_size); + + int yofs = (row_height - tl->get_size().y) / 2; + Point2 title_pos(code_completion_rect.position.x, code_completion_rect.position.y + i * row_height + yofs); + + /* Draw completion icon if it is valid. */ + const Ref<Texture2D> &icon = code_completion_options[l].icon; + Rect2 icon_area(code_completion_rect.position.x, code_completion_rect.position.y + i * row_height, icon_area_size.width, icon_area_size.height); + if (icon.is_valid()) { + Size2 icon_size = icon_area.size * 0.7; + icon->draw_rect(ci, Rect2(icon_area.position + (icon_area.size - icon_size) / 2, icon_size)); + } + title_pos.x = icon_area.position.x + icon_area.size.width + icon_hsep; + + tl->set_width(code_completion_rect.size.width - (icon_area_size.x + icon_hsep)); + if (rtl) { + if (code_completion_options[l].default_value.get_type() == Variant::COLOR) { + draw_rect(Rect2(Point2(code_completion_rect.position.x, icon_area.position.y), icon_area_size), (Color)code_completion_options[l].default_value); + } + tl->set_align(HALIGN_RIGHT); + } else { + if (code_completion_options[l].default_value.get_type() == Variant::COLOR) { + draw_rect(Rect2(Point2(code_completion_rect.position.x + code_completion_rect.size.width - icon_area_size.x, icon_area.position.y), icon_area_size), (Color)code_completion_options[l].default_value); + } + tl->set_align(HALIGN_LEFT); + } + tl->draw(ci, title_pos, code_completion_options[l].font_color); + } + + /* Draw a small scroll rectangle to show a position in the options. */ + if (scroll_width) { + float r = (float)code_completion_max_lines / code_completion_options_count; + float o = (float)code_completion_line_ofs / code_completion_options_count; + draw_rect(Rect2(code_completion_rect.position.x + code_completion_rect.size.width, code_completion_rect.position.y + o * code_completion_rect.size.y, scroll_width, code_completion_rect.size.y * r), code_completion_scroll_color); + } + } + + /* Code hint */ + if (caret_visible && code_hint != "" && (!code_completion_active || (code_completion_below != code_hint_draw_below))) { + const int font_height = font->get_height(font_size); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("panel"), SNAME("TooltipPanel")); + Color font_color = get_theme_color(SNAME("font_color"), SNAME("TooltipLabel")); + + Vector<String> 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, font->get_string_size(code_hint_lines[i], font_size).x); + } + Size2 minsize = sb->get_minimum_size() + Size2(max_width, line_count * font_height + (line_spacing * line_count - 1)); + + int offset = font->get_string_size(code_hint_lines[0].substr(0, code_hint_lines[0].find(String::chr(0xFFFF))), font_size).x; + if (code_hint_xpos == -0xFFFF) { + code_hint_xpos = get_caret_draw_pos().x - offset; + } + Point2 hint_ofs = Vector2(code_hint_xpos, get_caret_draw_pos().y); + if (code_hint_draw_below) { + hint_ofs.y += line_spacing / 2.0f; + } else { + hint_ofs.y -= (minsize.y + row_height) - line_spacing; + } + + draw_style_box(sb, Rect2(hint_ofs, minsize)); + + int yofs = 0; + for (int i = 0; i < line_count; i++) { + const String &line = code_hint_lines[i]; + + int begin = 0; + int end = 0; + if (line.find(String::chr(0xFFFF)) != -1) { + begin = font->get_string_size(line.substr(0, line.find(String::chr(0xFFFF))), font_size).x; + end = font->get_string_size(line.substr(0, line.rfind(String::chr(0xFFFF))), font_size).x; + } + + Point2 round_ofs = hint_ofs + sb->get_offset() + Vector2(0, font->get_ascent() + font_height * i + yofs); + round_ofs = round_ofs.round(); + draw_string(font, round_ofs, line.replace(String::chr(0xFFFF), ""), HALIGN_LEFT, -1, font_size, font_color); + if (end > 0) { + // Draw an underline for the currently edited function parameter. + const Vector2 b = hint_ofs + sb->get_offset() + Vector2(begin, font_height + font_height * i + line_spacing); + draw_line(b, b + Vector2(end - begin, 0), font_color, 2); + + // Draw a translucent text highlight as well. + const Rect2 highlight_rect = Rect2( + hint_ofs + sb->get_offset() + Vector2(begin, 0), + Vector2(end - begin, font_height)); + draw_rect(highlight_rect, font_color * Color(1, 1, 1, 0.2)); + } + yofs += line_spacing; + } + } } break; } } +void CodeEdit::gui_input(const Ref<InputEvent> &p_gui_input) { + Ref<InputEventMouseButton> mb = p_gui_input; + + if (mb.is_valid()) { + /* Ignore mouse clicks in IME input mode. */ + if (has_ime_text()) { + return; + } + + if (code_completion_active && code_completion_rect.has_point(mb->get_position())) { + if (!mb->is_pressed()) { + return; + } + + switch (mb->get_button_index()) { + case MOUSE_BUTTON_WHEEL_UP: { + if (code_completion_current_selected > 0) { + code_completion_current_selected--; + update(); + } + } break; + case MOUSE_BUTTON_WHEEL_DOWN: { + if (code_completion_current_selected < code_completion_options.size() - 1) { + code_completion_current_selected++; + update(); + } + } break; + case MOUSE_BUTTON_LEFT: { + code_completion_current_selected = CLAMP(code_completion_line_ofs + (mb->get_position().y - code_completion_rect.position.y) / get_line_height(), 0, code_completion_options.size() - 1); + if (mb->is_double_click()) { + confirm_code_completion(); + } + update(); + } break; + default: + break; + } + return; + } + cancel_code_completion(); + set_code_hint(""); + + if (mb->is_pressed()) { + Vector2i mpos = mb->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; + } + + Point2i pos = get_line_column_at_pos(Point2i(mpos.x, mpos.y)); + int line = pos.y; + int col = pos.x; + + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { + if (is_line_folded(line)) { + int wrap_index = get_line_wrap_index_at_column(line, col); + if (wrap_index == get_line_wrap_count(line)) { + int eol_icon_width = folded_eol_icon->get_width(); + int left_margin = get_total_gutter_width() + eol_icon_width + get_line_width(line, wrap_index) - get_h_scroll(); + if (mpos.x > left_margin && mpos.x <= left_margin + eol_icon_width + 3) { + unfold_line(line); + return; + } + } + } + } + } else { + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { + if (mb->is_command_pressed() && symbol_lookup_word != String()) { + Vector2i mpos = mb->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; + } + + Point2i pos = get_line_column_at_pos(Point2i(mpos.x, mpos.y)); + int line = pos.y; + int col = pos.x; + + emit_signal(SNAME("symbol_lookup"), symbol_lookup_word, line, col); + return; + } + } + } + } + + Ref<InputEventMouseMotion> mm = p_gui_input; + if (mm.is_valid()) { + Vector2i mpos = mm->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; + } + + if (symbol_lookup_on_click_enabled) { + if (mm->is_command_pressed() && mm->get_button_mask() == 0 && !is_dragging_cursor()) { + symbol_lookup_new_word = get_word_at_pos(mpos); + if (symbol_lookup_new_word != symbol_lookup_word) { + emit_signal(SNAME("symbol_validate"), symbol_lookup_new_word); + } + } else { + set_symbol_lookup_word_as_valid(false); + } + } + } + + Ref<InputEventKey> k = p_gui_input; + bool update_code_completion = false; + if (!k.is_valid()) { + TextEdit::gui_input(p_gui_input); + return; + } + + /* Ctrl + Hover symbols */ +#ifdef OSX_ENABLED + if (k->get_keycode() == KEY_META) { +#else + if (k->get_keycode() == KEY_CTRL) { +#endif + if (symbol_lookup_on_click_enabled) { + if (k->is_pressed() && !is_dragging_cursor()) { + symbol_lookup_new_word = get_word_at_pos(get_local_mouse_pos()); + if (symbol_lookup_new_word != symbol_lookup_word) { + emit_signal(SNAME("symbol_validate"), symbol_lookup_new_word); + } + } else { + set_symbol_lookup_word_as_valid(false); + } + } + return; + } + + /* If a modifier has been pressed, and nothing else, return. */ + if (!k->is_pressed() || k->get_keycode() == KEY_CTRL || k->get_keycode() == KEY_ALT || k->get_keycode() == KEY_SHIFT || k->get_keycode() == KEY_META) { + return; + } + + /* Allow unicode handling if: */ + /* No Modifiers are pressed (except shift) */ + bool allow_unicode_handling = !(k->is_command_pressed() || k->is_ctrl_pressed() || k->is_alt_pressed() || k->is_meta_pressed()); + + /* AUTO-COMPLETE */ + if (code_completion_enabled && k->is_action("ui_text_completion_query", true)) { + request_code_completion(true); + accept_event(); + return; + } + + if (code_completion_active) { + if (k->is_action("ui_up", true)) { + if (code_completion_current_selected > 0) { + code_completion_current_selected--; + } else { + code_completion_current_selected = code_completion_options.size() - 1; + } + update(); + accept_event(); + return; + } + if (k->is_action("ui_down", true)) { + if (code_completion_current_selected < code_completion_options.size() - 1) { + code_completion_current_selected++; + } else { + code_completion_current_selected = 0; + } + update(); + accept_event(); + return; + } + if (k->is_action("ui_page_up", true)) { + code_completion_current_selected = MAX(0, code_completion_current_selected - code_completion_max_lines); + update(); + accept_event(); + return; + } + if (k->is_action("ui_page_down", true)) { + code_completion_current_selected = MIN(code_completion_options.size() - 1, code_completion_current_selected + code_completion_max_lines); + update(); + accept_event(); + return; + } + if (k->is_action("ui_home", true)) { + code_completion_current_selected = 0; + update(); + accept_event(); + return; + } + if (k->is_action("ui_end", true)) { + code_completion_current_selected = MIN(code_completion_options.size() - 1, code_completion_current_selected + code_completion_max_lines); + update(); + accept_event(); + return; + } + if (k->is_action("ui_text_completion_replace", true) || k->is_action("ui_text_completion_accept", true)) { + confirm_code_completion(k->is_action("ui_text_completion_replace", true)); + accept_event(); + return; + } + if (k->is_action("ui_cancel", true)) { + cancel_code_completion(); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace", true)) { + backspace(); + _filter_code_completion_candidates_impl(); + accept_event(); + return; + } + + if (k->is_action("ui_left", true) || k->is_action("ui_right", true)) { + update_code_completion = true; + } else { + update_code_completion = (allow_unicode_handling && k->get_unicode() >= 32); + } + + if (!update_code_completion) { + cancel_code_completion(); + } + } + + /* MISC */ + if (k->is_action("ui_cancel", true)) { + set_code_hint(""); + accept_event(); + return; + } + if (allow_unicode_handling && k->get_unicode() == ')') { + set_code_hint(""); + } + + /* Indentation */ + if (k->is_action("ui_text_indent", true)) { + do_indent(); + accept_event(); + return; + } + + if (k->is_action("ui_text_dedent", true)) { + do_unindent(); + accept_event(); + return; + } + + // Override new line actions, for auto indent + if (k->is_action("ui_text_newline_above", true)) { + _new_line(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_newline_blank", true)) { + _new_line(false); + accept_event(); + return; + } + if (k->is_action("ui_text_newline", true)) { + _new_line(); + accept_event(); + return; + } + + /* Remove shift otherwise actions will not match. */ + k = k->duplicate(); + k->set_shift_pressed(false); + + if (k->is_action("ui_text_caret_up", true) || + k->is_action("ui_text_caret_down", true) || + k->is_action("ui_text_caret_line_start", true) || + k->is_action("ui_text_caret_line_end", true) || + k->is_action("ui_text_caret_page_up", true) || + k->is_action("ui_text_caret_page_down", true)) { + set_code_hint(""); + } + + TextEdit::gui_input(p_gui_input); + + if (update_code_completion) { + _filter_code_completion_candidates_impl(); + } +} + +/* General overrides */ +Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { + if (symbol_lookup_word != String()) { + return CURSOR_POINTING_HAND; + } + + if ((code_completion_active && code_completion_rect.has_point(p_pos)) || (!is_editable() && (!is_selecting_enabled() || get_line_count() == 0))) { + return CURSOR_ARROW; + } + + Point2i pos = get_line_column_at_pos(p_pos); + int line = pos.y; + int col = pos.x; + + if (is_line_folded(line)) { + int wrap_index = get_line_wrap_index_at_column(line, col); + if (wrap_index == get_line_wrap_count(line)) { + int eol_icon_width = folded_eol_icon->get_width(); + int left_margin = get_total_gutter_width() + eol_icon_width + get_line_width(line, wrap_index) - get_h_scroll(); + if (p_pos.x > left_margin && p_pos.x <= left_margin + eol_icon_width + 3) { + return CURSOR_POINTING_HAND; + } + } + } + + return TextEdit::get_cursor_shape(p_pos); +} + +/* Text manipulation */ + +// Overridable actions +void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { + bool had_selection = has_selection(); + if (had_selection) { + begin_complex_operation(); + delete_selection(); + } + + // Remove the old character if in overtype mode and no selection. + if (is_overtype_mode_enabled() && !had_selection) { + begin_complex_operation(); + + /* Make sure we don't try and remove empty space. */ + if (get_caret_column() < get_line(get_caret_line()).length()) { + remove_text(get_caret_line(), get_caret_column(), get_caret_line(), get_caret_column() + 1); + } + } + + const char32_t chr[2] = { (char32_t)p_unicode, 0 }; + + if (auto_brace_completion_enabled) { + int cl = get_caret_line(); + int cc = get_caret_column(); + int caret_move_offset = 1; + + int post_brace_pair = cc < get_line(cl).length() ? _get_auto_brace_pair_close_at_pos(cl, cc) : -1; + + if (has_string_delimiter(chr) && cc > 0 && _is_char(get_line(cl)[cc - 1]) && post_brace_pair == -1) { + insert_text_at_caret(chr); + } else if (cc < get_line(cl).length() && _is_char(get_line(cl)[cc])) { + insert_text_at_caret(chr); + } else if (post_brace_pair != -1 && auto_brace_completion_pairs[post_brace_pair].close_key[0] == chr[0]) { + caret_move_offset = auto_brace_completion_pairs[post_brace_pair].close_key.length(); + } else if (is_in_comment(cl, cc) != -1 || (is_in_string(cl, cc) != -1 && has_string_delimiter(chr))) { + insert_text_at_caret(chr); + } else { + insert_text_at_caret(chr); + + int pre_brace_pair = _get_auto_brace_pair_open_at_pos(cl, cc + 1); + if (pre_brace_pair != -1) { + insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key); + } + } + set_caret_column(cc + caret_move_offset); + } else { + insert_text_at_caret(chr); + } + + if ((is_overtype_mode_enabled() && !had_selection) || (had_selection)) { + end_complex_operation(); + } +} + +void CodeEdit::_backspace_internal() { + if (!is_editable()) { + return; + } + + int cc = get_caret_column(); + int cl = get_caret_line(); + + if (cc == 0 && cl == 0) { + return; + } + + if (has_selection()) { + delete_selection(); + return; + } + + if (cl > 0 && _is_line_hidden(cl - 1)) { + unfold_line(get_caret_line() - 1); + } + + int prev_line = cc ? cl : cl - 1; + int prev_column = cc ? (cc - 1) : (get_line(cl - 1).length()); + + merge_gutters(prev_line, cl); + + if (auto_brace_completion_enabled && cc > 0) { + int idx = _get_auto_brace_pair_open_at_pos(cl, cc); + if (idx != -1) { + prev_column = cc - auto_brace_completion_pairs[idx].open_key.length(); + + if (_get_auto_brace_pair_close_at_pos(cl, cc) == idx) { + remove_text(prev_line, prev_column, cl, cc + auto_brace_completion_pairs[idx].close_key.length()); + } else { + remove_text(prev_line, prev_column, cl, cc); + } + set_caret_line(prev_line, false, true); + set_caret_column(prev_column); + return; + } + } + + // For space indentation we need to do a simple unindent if there are no chars to the left, acting in the + // same way as tabs. + if (indent_using_spaces && cc != 0) { + if (get_first_non_whitespace_column(cl) >= cc) { + prev_column = cc - _calculate_spaces_till_next_left_indent(cc); + prev_line = cl; + } + } + + remove_text(prev_line, prev_column, cl, cc); + + set_caret_line(prev_line, false, true); + set_caret_column(prev_column); +} + +/* Indent management */ +void CodeEdit::set_indent_size(const int p_size) { + ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0."); + if (indent_size == p_size) { + return; + } + + indent_size = p_size; + if (indent_using_spaces) { + indent_text = String(" ").repeat(p_size); + } else { + indent_text = "\t"; + } + set_tab_size(p_size); +} + +int CodeEdit::get_indent_size() const { + return indent_size; +} + +void CodeEdit::set_indent_using_spaces(const bool p_use_spaces) { + indent_using_spaces = p_use_spaces; + if (indent_using_spaces) { + indent_text = String(" ").repeat(indent_size); + } else { + indent_text = "\t"; + } +} + +bool CodeEdit::is_indent_using_spaces() const { + return indent_using_spaces; +} + +void CodeEdit::set_auto_indent_enabled(bool p_enabled) { + auto_indent = p_enabled; +} + +bool CodeEdit::is_auto_indent_enabled() const { + return auto_indent; +} + +void CodeEdit::set_auto_indent_prefixes(const TypedArray<String> &p_prefixes) { + auto_indent_prefixes.clear(); + for (int i = 0; i < p_prefixes.size(); i++) { + const String prefix = p_prefixes[i]; + auto_indent_prefixes.insert(prefix[0]); + } +} + +TypedArray<String> CodeEdit::get_auto_indent_prefixes() const { + TypedArray<String> prefixes; + for (const Set<char32_t>::Element *E = auto_indent_prefixes.front(); E; E = E->next()) { + prefixes.push_back(String::chr(E->get())); + } + return prefixes; +} + +void CodeEdit::do_indent() { + if (!is_editable()) { + return; + } + + if (has_selection()) { + indent_lines(); + return; + } + + if (!indent_using_spaces) { + insert_text_at_caret("\t"); + return; + } + + int spaces_to_add = _calculate_spaces_till_next_right_indent(get_caret_column()); + if (spaces_to_add > 0) { + insert_text_at_caret(String(" ").repeat(spaces_to_add)); + } +} + +void CodeEdit::indent_lines() { + if (!is_editable()) { + return; + } + + begin_complex_operation(); + + /* This value informs us by how much we changed selection position by indenting right. */ + /* Default is 1 for tab indentation. */ + int selection_offset = 1; + + int start_line = get_caret_line(); + int end_line = start_line; + if (has_selection()) { + start_line = get_selection_from_line(); + end_line = get_selection_to_line(); + + /* Ignore the last line if the selection is not past the first column. */ + if (get_selection_to_column() == 0) { + selection_offset = 0; + end_line--; + } + } + + for (int i = start_line; i <= end_line; i++) { + const String line_text = get_line(i); + if (line_text.size() == 0 && has_selection()) { + continue; + } + + if (!indent_using_spaces) { + set_line(i, '\t' + line_text); + continue; + } + + /* We don't really care where selection is - we just need to know indentation level at the beginning of the line. */ + /* Since we will add this many spaces, we want to move the whole selection and caret by this much. */ + int spaces_to_add = _calculate_spaces_till_next_right_indent(get_first_non_whitespace_column(i)); + set_line(i, String(" ").repeat(spaces_to_add) + line_text); + selection_offset = spaces_to_add; + } + + /* Fix selection and caret being off after shifting selection right.*/ + if (has_selection()) { + select(start_line, get_selection_from_column() + selection_offset, get_selection_to_line(), get_selection_to_column() + selection_offset); + } + set_caret_column(get_caret_column() + selection_offset, false); + + end_complex_operation(); +} + +void CodeEdit::do_unindent() { + if (!is_editable()) { + return; + } + + int cc = get_caret_column(); + + if (has_selection() || cc <= 0) { + unindent_lines(); + return; + } + + int cl = get_caret_line(); + const String &line = get_line(cl); + + if (line[cc - 1] == '\t') { + remove_text(cl, cc - 1, cl, cc); + set_caret_column(MAX(0, cc - 1)); + return; + } + + if (line[cc - 1] != ' ') { + return; + } + + int spaces_to_remove = _calculate_spaces_till_next_left_indent(cc); + if (spaces_to_remove > 0) { + for (int i = 1; i <= spaces_to_remove; i++) { + if (line[cc - i] != ' ') { + spaces_to_remove = i - 1; + break; + } + } + remove_text(cl, cc - spaces_to_remove, cl, cc); + set_caret_column(MAX(0, cc - spaces_to_remove)); + } +} + +void CodeEdit::unindent_lines() { + if (!is_editable()) { + return; + } + + begin_complex_operation(); + + /* Moving caret and selection after unindenting can get tricky because */ + /* changing content of line can move caret and selection on its own (if new line ends before previous position of either), */ + /* therefore we just remember initial values and at the end of the operation offset them by number of removed characters. */ + int removed_characters = 0; + int initial_selection_end_column = 0; + int initial_cursor_column = get_caret_column(); + + int start_line = get_caret_line(); + int end_line = start_line; + if (has_selection()) { + start_line = get_selection_from_line(); + end_line = get_selection_to_line(); + + /* Ignore the last line if the selection is not past the first column. */ + initial_selection_end_column = get_selection_to_column(); + if (initial_selection_end_column == 0) { + end_line--; + } + } + + bool first_line_edited = false; + bool last_line_edited = false; + + for (int i = start_line; i <= end_line; i++) { + String line_text = get_line(i); + + if (line_text.begins_with("\t")) { + line_text = line_text.substr(1, line_text.length()); + + set_line(i, line_text); + removed_characters = 1; + + first_line_edited = (i == start_line) ? true : first_line_edited; + last_line_edited = (i == end_line) ? true : last_line_edited; + continue; + } + + if (line_text.begins_with(" ")) { + /* When unindenting we aim to remove spaces before line that has selection no matter what is selected, */ + /* Here we remove only enough spaces to align text to nearest full multiple of indentation_size. */ + /* In case where selection begins at the start of indentation_size multiple we remove whole indentation level. */ + int spaces_to_remove = _calculate_spaces_till_next_left_indent(get_first_non_whitespace_column(i)); + line_text = line_text.substr(spaces_to_remove, line_text.length()); + + set_line(i, line_text); + removed_characters = spaces_to_remove; + + first_line_edited = (i == start_line) ? true : first_line_edited; + last_line_edited = (i == end_line) ? true : last_line_edited; + } + } + + if (has_selection()) { + /* Fix selection being off by one on the first line. */ + if (first_line_edited) { + select(get_selection_from_line(), get_selection_from_column() - removed_characters, get_selection_to_line(), initial_selection_end_column); + } + + /* Fix selection being off by one on the last line. */ + if (last_line_edited) { + select(get_selection_from_line(), get_selection_from_column(), get_selection_to_line(), initial_selection_end_column - removed_characters); + } + } + set_caret_column(initial_cursor_column - removed_characters, false); + + end_complex_operation(); +} + +int CodeEdit::_calculate_spaces_till_next_left_indent(int p_column) const { + int spaces_till_indent = p_column % indent_size; + if (spaces_till_indent == 0) { + spaces_till_indent = indent_size; + } + return spaces_till_indent; +} + +int CodeEdit::_calculate_spaces_till_next_right_indent(int p_column) const { + return indent_size - p_column % indent_size; +} + +void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { + if (!is_editable()) { + return; + } + + const int cc = get_caret_column(); + const int cl = get_caret_line(); + const String line = get_line(cl); + + String ins = "\n"; + + /* Append current indentation. */ + int space_count = 0; + int line_col = 0; + for (; line_col < cc; line_col++) { + if (line[line_col] == '\t') { + ins += indent_text; + space_count = 0; + continue; + } + + if (line[line_col] == ' ') { + space_count++; + + if (space_count == indent_size) { + ins += indent_text; + space_count = 0; + } + continue; + } + break; + } + + if (is_line_folded(cl)) { + unfold_line(cl); + } + + /* Indent once again if the previous line needs it, ie ':'. */ + /* Then add an addition new line for any closing pairs aka '()'. */ + /* Skip this in comments or if we are going above. */ + bool brace_indent = false; + if (auto_indent && !p_above && cc > 0 && is_in_comment(cl) == -1) { + bool should_indent = false; + char32_t indent_char = ' '; + + for (; line_col < cc; line_col++) { + char32_t c = line[line_col]; + if (auto_indent_prefixes.has(c)) { + should_indent = true; + indent_char = c; + continue; + } + + /* Make sure this is the last char, trailing whitespace or comments are okay. */ + if (should_indent && (!_is_whitespace(c) && is_in_comment(cl, cc) == -1)) { + should_indent = false; + } + } + + if (should_indent) { + ins += indent_text; + + String closing_pair = get_auto_brace_completion_close_key(String::chr(indent_char)); + if (!closing_pair.is_empty() && line.find(closing_pair, cc) == cc) { + /* No need to move the brace below if we are not taking the text with us. */ + if (p_split_current_line) { + brace_indent = true; + ins += "\n" + ins.substr(indent_text.size(), ins.length() - 2); + } else { + brace_indent = false; + ins = "\n" + ins.substr(indent_text.size(), ins.length() - 2); + } + } + } + } + + begin_complex_operation(); + + bool first_line = false; + if (!p_split_current_line) { + if (p_above) { + if (cl > 0) { + set_caret_line(cl - 1, false); + set_caret_column(get_line(get_caret_line()).length()); + } else { + set_caret_column(0); + first_line = true; + } + } else { + set_caret_column(line.length()); + } + } + + insert_text_at_caret(ins); + + if (first_line) { + set_caret_line(0); + } else if (brace_indent) { + set_caret_line(get_caret_line() - 1, false); + set_caret_column(get_line(get_caret_line()).length()); + } + + end_complex_operation(); +} + +/* Auto brace completion */ +void CodeEdit::set_auto_brace_completion_enabled(bool p_enabled) { + auto_brace_completion_enabled = p_enabled; +} + +bool CodeEdit::is_auto_brace_completion_enabled() const { + return auto_brace_completion_enabled; +} + +void CodeEdit::set_highlight_matching_braces_enabled(bool p_enabled) { + highlight_matching_braces_enabled = p_enabled; + update(); +} + +bool CodeEdit::is_highlight_matching_braces_enabled() const { + return highlight_matching_braces_enabled; +} + +void CodeEdit::add_auto_brace_completion_pair(const String &p_open_key, const String &p_close_key) { + ERR_FAIL_COND_MSG(p_open_key.is_empty(), "auto brace completion open key cannot be empty"); + ERR_FAIL_COND_MSG(p_close_key.is_empty(), "auto brace completion close key cannot be empty"); + + for (int i = 0; i < p_open_key.length(); i++) { + ERR_FAIL_COND_MSG(!is_symbol(p_open_key[i]), "auto brace completion open key must be a symbol"); + } + for (int i = 0; i < p_close_key.length(); i++) { + ERR_FAIL_COND_MSG(!is_symbol(p_close_key[i]), "auto brace completion close key must be a symbol"); + } + + int at = 0; + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + ERR_FAIL_COND_MSG(auto_brace_completion_pairs[i].open_key == p_open_key, "auto brace completion open key '" + p_open_key + "' already exists."); + if (p_open_key.length() < auto_brace_completion_pairs[i].open_key.length()) { + at++; + } + } + + BracePair brace_pair; + brace_pair.open_key = p_open_key; + brace_pair.close_key = p_close_key; + auto_brace_completion_pairs.insert(at, brace_pair); +} + +void CodeEdit::set_auto_brace_completion_pairs(const Dictionary &p_auto_brace_completion_pairs) { + auto_brace_completion_pairs.clear(); + + Array keys = p_auto_brace_completion_pairs.keys(); + for (int i = 0; i < keys.size(); i++) { + add_auto_brace_completion_pair(keys[i], p_auto_brace_completion_pairs[keys[i]]); + } +} + +Dictionary CodeEdit::get_auto_brace_completion_pairs() const { + Dictionary brace_pairs; + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + brace_pairs[auto_brace_completion_pairs[i].open_key] = auto_brace_completion_pairs[i].close_key; + } + return brace_pairs; +} + +bool CodeEdit::has_auto_brace_completion_open_key(const String &p_open_key) const { + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + if (auto_brace_completion_pairs[i].open_key == p_open_key) { + return true; + } + } + return false; +} + +bool CodeEdit::has_auto_brace_completion_close_key(const String &p_close_key) const { + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + if (auto_brace_completion_pairs[i].close_key == p_close_key) { + return true; + } + } + return false; +} + +String CodeEdit::get_auto_brace_completion_close_key(const String &p_open_key) const { + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + if (auto_brace_completion_pairs[i].open_key == p_open_key) { + return auto_brace_completion_pairs[i].close_key; + } + } + return String(); +} + /* Main Gutter */ void CodeEdit::_update_draw_main_gutter() { set_gutter_draw(main_gutter, draw_breakpoints || draw_bookmarks || draw_executing_lines); @@ -124,6 +1178,8 @@ void CodeEdit::_main_gutter_draw_callback(int p_line, int p_gutter, const Rect2 // Breakpoints void CodeEdit::set_line_as_breakpoint(int p_line, bool p_breakpointed) { + ERR_FAIL_INDEX(p_line, get_line_count()); + int mask = get_line_gutter_metadata(p_line, main_gutter); set_line_gutter_metadata(p_line, main_gutter, p_breakpointed ? mask | MAIN_GUTTER_BREAKPOINT : mask & ~MAIN_GUTTER_BREAKPOINT); if (p_breakpointed) { @@ -131,7 +1187,7 @@ void CodeEdit::set_line_as_breakpoint(int p_line, bool p_breakpointed) { } else if (breakpointed_lines.has(p_line)) { breakpointed_lines.erase(p_line); } - emit_signal("breakpoint_toggled", p_line); + emit_signal(SNAME("breakpoint_toggled"), p_line); update(); } @@ -236,9 +1292,9 @@ bool CodeEdit::is_line_numbers_zero_padded() const { void CodeEdit::_line_number_draw_callback(int p_line, int p_gutter, const Rect2 &p_region) { String fc = TS->format_number(String::num(p_line + 1).lpad(line_number_digits, line_number_padding)); Ref<TextLine> tl; - tl.instance(); - tl->add_string(fc, cache.font, cache.font_size); - int yofs = p_region.position.y + (get_row_height() - tl->get_size().y) / 2; + tl.instantiate(); + tl->add_string(fc, font, font_size); + int yofs = p_region.position.y + (get_line_height() - tl->get_size().y) / 2; Color number_color = get_line_gutter_item_color(p_line, line_number_gutter); if (number_color == Color(1, 1, 1)) { number_color = line_number_color; @@ -256,7 +1312,7 @@ bool CodeEdit::is_drawing_fold_gutter() const { } void CodeEdit::_fold_gutter_draw_callback(int p_line, int p_gutter, Rect2 p_region) { - if (!can_fold(p_line) && !is_folded(p_line)) { + if (!can_fold_line(p_line) && !is_line_folded(p_line)) { set_line_gutter_clickable(p_line, fold_gutter, false); return; } @@ -268,14 +1324,757 @@ void CodeEdit::_fold_gutter_draw_callback(int p_line, int p_gutter, Rect2 p_regi p_region.position += Point2(horizontal_padding, vertical_padding); p_region.size -= Point2(horizontal_padding, vertical_padding) * 2; - if (can_fold(p_line)) { + if (can_fold_line(p_line)) { can_fold_icon->draw_rect(get_canvas_item(), p_region, false, folding_color); return; } folded_icon->draw_rect(get_canvas_item(), p_region, false, folding_color); } +/* Line Folding */ +void CodeEdit::set_line_folding_enabled(bool p_enabled) { + line_folding_enabled = p_enabled; + _set_hiding_enabled(p_enabled); +} + +bool CodeEdit::is_line_folding_enabled() const { + return line_folding_enabled; +} + +bool CodeEdit::can_fold_line(int p_line) const { + ERR_FAIL_INDEX_V(p_line, get_line_count(), false); + if (!line_folding_enabled) { + return false; + } + + if (p_line + 1 >= get_line_count() || get_line(p_line).strip_edges().size() == 0) { + return false; + } + + if (_is_line_hidden(p_line) || is_line_folded(p_line)) { + return false; + } + + /* Check for full multiline line or block strings / comments. */ + int in_comment = is_in_comment(p_line); + int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; + if (in_string != -1 || in_comment != -1) { + if (get_delimiter_start_position(p_line, get_line(p_line).size() - 1).y != p_line) { + return false; + } + + int delimter_end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; + /* No end line, therefore we have a multiline region over the rest of the file. */ + if (delimter_end_line == -1) { + return true; + } + /* End line is the same therefore we have a block. */ + if (delimter_end_line == p_line) { + /* Check we are the start of the block. */ + if (p_line - 1 >= 0) { + if ((in_string != -1 && is_in_string(p_line - 1) != -1) || (in_comment != -1 && is_in_comment(p_line - 1) != -1)) { + return false; + } + } + /* Check it continues for at least one line. */ + return ((in_string != -1 && is_in_string(p_line + 1) != -1) || (in_comment != -1 && is_in_comment(p_line + 1) != -1)); + } + return ((in_string != -1 && is_in_string(delimter_end_line) != -1) || (in_comment != -1 && is_in_comment(delimter_end_line) != -1)); + } + + /* Otherwise check indent levels. */ + int start_indent = get_indent_level(p_line); + for (int i = p_line + 1; i < get_line_count(); i++) { + if (is_in_string(i) != -1 || is_in_comment(i) != -1 || get_line(i).strip_edges().size() == 0) { + continue; + } + return (get_indent_level(i) > start_indent); + } + return false; +} + +void CodeEdit::fold_line(int p_line) { + ERR_FAIL_INDEX(p_line, get_line_count()); + if (!is_line_folding_enabled() || !can_fold_line(p_line)) { + return; + } + + /* Find the last line to be hidden. */ + const int line_count = get_line_count() - 1; + int end_line = line_count; + + int in_comment = is_in_comment(p_line); + int in_string = (in_comment == -1) ? is_in_string(p_line) : -1; + if (in_string != -1 || in_comment != -1) { + end_line = get_delimiter_end_position(p_line, get_line(p_line).size() - 1).y; + /* End line is the same therefore we have a block of single line delimiters. */ + if (end_line == p_line) { + for (int i = p_line + 1; i <= line_count; i++) { + if (i == line_count) { + end_line = line_count; + break; + } + + if ((in_string != -1 && is_in_string(i) == -1) || (in_comment != -1 && is_in_comment(i) == -1)) { + end_line = i - 1; + break; + } + } + } + } else { + int start_indent = get_indent_level(p_line); + for (int i = p_line + 1; i <= line_count; i++) { + if (get_line(p_line).strip_edges().size() == 0 || is_in_string(i) != -1 || is_in_comment(i) != -1) { + end_line = i; + continue; + } + + if (i == line_count) { + /* Do not fold empty last line of script if any */ + end_line = i; + if (get_line(i).strip_edges().size() == 0) { + end_line--; + } + break; + } + + if ((get_indent_level(i) <= start_indent && get_line(i).strip_edges().size() != 0)) { + /* Keep an empty line unfolded if any */ + end_line = i - 1; + if (get_line(i - 1).strip_edges().size() == 0 && i - 2 > p_line) { + end_line = i - 2; + } + break; + } + } + } + + for (int i = p_line + 1; i <= end_line; i++) { + _set_line_as_hidden(i, true); + } + + /* Fix selection. */ + if (has_selection()) { + if (_is_line_hidden(get_selection_from_line()) && _is_line_hidden(get_selection_to_line())) { + deselect(); + } else if (_is_line_hidden(get_selection_from_line())) { + select(p_line, 9999, get_selection_to_line(), get_selection_to_column()); + } else if (_is_line_hidden(get_selection_to_line())) { + select(get_selection_from_line(), get_selection_from_column(), p_line, 9999); + } + } + + /* Reset caret. */ + if (_is_line_hidden(get_caret_line())) { + set_caret_line(p_line, false, false); + set_caret_column(get_line(p_line).length(), false); + } + update(); +} + +void CodeEdit::unfold_line(int p_line) { + ERR_FAIL_INDEX(p_line, get_line_count()); + if (!is_line_folded(p_line) && !_is_line_hidden(p_line)) { + return; + } + + int fold_start = p_line; + for (; fold_start > 0; fold_start--) { + if (is_line_folded(fold_start)) { + break; + } + } + fold_start = is_line_folded(fold_start) ? fold_start : p_line; + + for (int i = fold_start + 1; i < get_line_count(); i++) { + if (!_is_line_hidden(i)) { + break; + } + _set_line_as_hidden(i, false); + } + update(); +} + +void CodeEdit::fold_all_lines() { + for (int i = 0; i < get_line_count(); i++) { + fold_line(i); + } + update(); +} + +void CodeEdit::unfold_all_lines() { + _unhide_all_lines(); +} + +void CodeEdit::toggle_foldable_line(int p_line) { + ERR_FAIL_INDEX(p_line, get_line_count()); + if (is_line_folded(p_line)) { + unfold_line(p_line); + return; + } + fold_line(p_line); +} + +bool CodeEdit::is_line_folded(int p_line) const { + ERR_FAIL_INDEX_V(p_line, get_line_count(), false); + return p_line + 1 < get_line_count() && !_is_line_hidden(p_line) && _is_line_hidden(p_line + 1); +} + +TypedArray<int> CodeEdit::get_folded_lines() const { + TypedArray<int> folded_lines; + for (int i = 0; i < get_line_count(); i++) { + if (is_line_folded(i)) { + folded_lines.push_back(i); + } + } + return folded_lines; +} + +/* Delimiters */ +// Strings +void CodeEdit::add_string_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only) { + _add_delimiter(p_start_key, p_end_key, p_line_only, TYPE_STRING); +} + +void CodeEdit::remove_string_delimiter(const String &p_start_key) { + _remove_delimiter(p_start_key, TYPE_STRING); +} + +bool CodeEdit::has_string_delimiter(const String &p_start_key) const { + return _has_delimiter(p_start_key, TYPE_STRING); +} + +void CodeEdit::set_string_delimiters(const TypedArray<String> &p_string_delimiters) { + _set_delimiters(p_string_delimiters, TYPE_STRING); +} + +void CodeEdit::clear_string_delimiters() { + _clear_delimiters(TYPE_STRING); +} + +TypedArray<String> CodeEdit::get_string_delimiters() const { + return _get_delimiters(TYPE_STRING); +} + +int CodeEdit::is_in_string(int p_line, int p_column) const { + return _is_in_delimiter(p_line, p_column, TYPE_STRING); +} + +// Comments +void CodeEdit::add_comment_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only) { + _add_delimiter(p_start_key, p_end_key, p_line_only, TYPE_COMMENT); +} + +void CodeEdit::remove_comment_delimiter(const String &p_start_key) { + _remove_delimiter(p_start_key, TYPE_COMMENT); +} + +bool CodeEdit::has_comment_delimiter(const String &p_start_key) const { + return _has_delimiter(p_start_key, TYPE_COMMENT); +} + +void CodeEdit::set_comment_delimiters(const TypedArray<String> &p_comment_delimiters) { + _set_delimiters(p_comment_delimiters, TYPE_COMMENT); +} + +void CodeEdit::clear_comment_delimiters() { + _clear_delimiters(TYPE_COMMENT); +} + +TypedArray<String> CodeEdit::get_comment_delimiters() const { + return _get_delimiters(TYPE_COMMENT); +} + +int CodeEdit::is_in_comment(int p_line, int p_column) const { + return _is_in_delimiter(p_line, p_column, TYPE_COMMENT); +} + +String CodeEdit::get_delimiter_start_key(int p_delimiter_idx) const { + ERR_FAIL_INDEX_V(p_delimiter_idx, delimiters.size(), ""); + return delimiters[p_delimiter_idx].start_key; +} + +String CodeEdit::get_delimiter_end_key(int p_delimiter_idx) const { + ERR_FAIL_INDEX_V(p_delimiter_idx, delimiters.size(), ""); + return delimiters[p_delimiter_idx].end_key; +} + +Point2 CodeEdit::get_delimiter_start_position(int p_line, int p_column) const { + if (delimiters.size() == 0) { + return Point2(-1, -1); + } + ERR_FAIL_INDEX_V(p_line, get_line_count(), Point2(-1, -1)); + ERR_FAIL_COND_V(p_column - 1 > get_line(p_line).size(), Point2(-1, -1)); + + Point2 start_position; + start_position.y = -1; + start_position.x = -1; + + bool in_region = ((p_line <= 0 || delimiter_cache[p_line - 1].size() < 1) ? -1 : delimiter_cache[p_line - 1].back()->value()) != -1; + + /* Check the keys for this line. */ + for (Map<int, int>::Element *E = delimiter_cache[p_line].front(); E; E = E->next()) { + if (E->key() > p_column) { + break; + } + in_region = E->value() != -1; + start_position.x = in_region ? E->key() : -1; + } + + /* Region was found on this line and is not a multiline continuation. */ + int line_length = get_line(p_line).length(); + if (start_position.x != -1 && line_length > 0 && start_position.x != line_length + 1) { + start_position.y = p_line; + return start_position; + } + + /* Not in a region */ + if (!in_region) { + return start_position; + } + + /* Region starts on a previous line */ + for (int i = p_line - 1; i >= 0; i--) { + if (delimiter_cache[i].size() < 1) { + continue; + } + start_position.y = i; + start_position.x = delimiter_cache[i].back()->key(); + + /* Make sure it's not a multiline continuation. */ + line_length = get_line(i).length(); + if (line_length > 0 && start_position.x != line_length + 1) { + break; + } + } + return start_position; +} + +Point2 CodeEdit::get_delimiter_end_position(int p_line, int p_column) const { + if (delimiters.size() == 0) { + return Point2(-1, -1); + } + ERR_FAIL_INDEX_V(p_line, get_line_count(), Point2(-1, -1)); + ERR_FAIL_COND_V(p_column - 1 > get_line(p_line).size(), Point2(-1, -1)); + + Point2 end_position; + end_position.y = -1; + end_position.x = -1; + + int region = (p_line <= 0 || delimiter_cache[p_line - 1].size() < 1) ? -1 : delimiter_cache[p_line - 1].back()->value(); + + /* Check the keys for this line. */ + for (Map<int, int>::Element *E = delimiter_cache[p_line].front(); E; E = E->next()) { + end_position.x = (E->value() == -1) ? E->key() : -1; + if (E->key() > p_column) { + break; + } + region = E->value(); + } + + /* Region was found on this line and is not a multiline continuation. */ + if (region != -1 && end_position.x != -1 && (delimiters[region].line_only || end_position.x != get_line(p_line).length() + 1)) { + end_position.y = p_line; + return end_position; + } + + /* Not in a region */ + if (region == -1) { + end_position.x = -1; + return end_position; + } + + /* Region ends on a later line */ + for (int i = p_line + 1; i < get_line_count(); i++) { + if (delimiter_cache[i].size() < 1 || delimiter_cache[i].front()->value() != -1) { + continue; + } + end_position.x = delimiter_cache[i].front()->key(); + + /* Make sure it's not a multiline continuation. */ + if (get_line(i).length() > 0 && end_position.x != get_line(i).length() + 1) { + end_position.y = i; + break; + } + end_position.x = -1; + } + return end_position; +} + +/* Code hint */ +void CodeEdit::set_code_hint(const String &p_hint) { + code_hint = p_hint; + code_hint_xpos = -0xFFFF; + update(); +} + +void CodeEdit::set_code_hint_draw_below(bool p_below) { + code_hint_draw_below = p_below; + update(); +} + +/* Code Completion */ +void CodeEdit::set_code_completion_enabled(bool p_enable) { + code_completion_enabled = p_enable; +} + +bool CodeEdit::is_code_completion_enabled() const { + return code_completion_enabled; +} + +void CodeEdit::set_code_completion_prefixes(const TypedArray<String> &p_prefixes) { + code_completion_prefixes.clear(); + for (int i = 0; i < p_prefixes.size(); i++) { + code_completion_prefixes.insert(p_prefixes[i]); + } +} + +TypedArray<String> CodeEdit::get_code_completion_prefixes() const { + TypedArray<String> prefixes; + for (Set<String>::Element *E = code_completion_prefixes.front(); E; E = E->next()) { + prefixes.push_back(E->get()); + } + return prefixes; +} + +String CodeEdit::get_text_for_code_completion() const { + StringBuilder completion_text; + const int text_size = get_line_count(); + for (int i = 0; i < text_size; i++) { + String line = get_line(i); + + if (i == get_caret_line()) { + completion_text += line.substr(0, get_caret_column()); + /* Not unicode, represents the caret. */ + completion_text += String::chr(0xFFFF); + completion_text += line.substr(get_caret_column(), line.size()); + } else { + completion_text += line; + } + + if (i != text_size - 1) { + completion_text += "\n"; + } + } + return completion_text.as_string(); +} + +void CodeEdit::request_code_completion(bool p_force) { + if (GDVIRTUAL_CALL(_request_code_completion, p_force)) { + return; + } + + /* Don't re-query if all existing options are quoted types, eg path, signal. */ + bool ignored = code_completion_active && !code_completion_options.is_empty(); + if (ignored) { + ScriptCodeCompletionOption::Kind kind = ScriptCodeCompletionOption::KIND_PLAIN_TEXT; + const ScriptCodeCompletionOption *previous_option = nullptr; + for (int i = 0; i < code_completion_options.size(); i++) { + const ScriptCodeCompletionOption ¤t_option = code_completion_options[i]; + if (!previous_option) { + previous_option = ¤t_option; + kind = current_option.kind; + } + if (previous_option->kind != current_option.kind) { + ignored = false; + break; + } + } + ignored = ignored && (kind == ScriptCodeCompletionOption::KIND_FILE_PATH || kind == ScriptCodeCompletionOption::KIND_NODE_PATH || kind == ScriptCodeCompletionOption::KIND_SIGNAL); + } + + if (ignored) { + return; + } + + if (p_force) { + emit_signal(SNAME("request_code_completion")); + return; + } + + String line = get_line(get_caret_line()); + int ofs = CLAMP(get_caret_column(), 0, line.length()); + + if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || _is_char(line[ofs - 1]) || code_completion_prefixes.has(String::chr(line[ofs - 1])))) { + emit_signal(SNAME("request_code_completion")); + } else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(String::chr(line[ofs - 2]))) { + emit_signal(SNAME("request_code_completion")); + } +} + +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 RES &p_icon, const Variant &p_value) { + ScriptCodeCompletionOption completion_option; + completion_option.kind = (ScriptCodeCompletionOption::Kind)p_type; + completion_option.display = p_display_text; + completion_option.insert_text = p_insert_text; + completion_option.font_color = p_text_color; + completion_option.icon = p_icon; + completion_option.default_value = p_value; + code_completion_option_submitted.push_back(completion_option); +} + +void CodeEdit::update_code_completion_options(bool p_forced) { + code_completion_forced = p_forced; + code_completion_option_sources = code_completion_option_submitted; + code_completion_option_submitted.clear(); + _filter_code_completion_candidates_impl(); +} + +TypedArray<Dictionary> CodeEdit::get_code_completion_options() const { + if (!code_completion_active) { + return TypedArray<Dictionary>(); + } + + TypedArray<Dictionary> completion_options; + completion_options.resize(code_completion_options.size()); + for (int i = 0; i < code_completion_options.size(); i++) { + Dictionary option; + option["kind"] = code_completion_options[i].kind; + option["display_text"] = code_completion_options[i].display; + option["insert_text"] = code_completion_options[i].insert_text; + option["font_color"] = code_completion_options[i].font_color; + option["icon"] = code_completion_options[i].icon; + option["default_value"] = code_completion_options[i].default_value; + completion_options[i] = option; + } + return completion_options; +} + +Dictionary CodeEdit::get_code_completion_option(int p_index) const { + if (!code_completion_active) { + return Dictionary(); + } + ERR_FAIL_INDEX_V(p_index, code_completion_options.size(), Dictionary()); + + Dictionary option; + option["kind"] = code_completion_options[p_index].kind; + option["display_text"] = code_completion_options[p_index].display; + option["insert_text"] = code_completion_options[p_index].insert_text; + option["font_color"] = code_completion_options[p_index].font_color; + option["icon"] = code_completion_options[p_index].icon; + option["default_value"] = code_completion_options[p_index].default_value; + return option; +} + +int CodeEdit::get_code_completion_selected_index() const { + return (code_completion_active) ? code_completion_current_selected : -1; +} + +void CodeEdit::set_code_completion_selected_index(int p_index) { + if (!code_completion_active) { + return; + } + ERR_FAIL_INDEX(p_index, code_completion_options.size()); + code_completion_current_selected = p_index; + update(); +} + +void CodeEdit::confirm_code_completion(bool p_replace) { + if (!is_editable() || !code_completion_active) { + return; + } + + if (GDVIRTUAL_CALL(_confirm_code_completion, p_replace)) { + return; + } + + begin_complex_operation(); + + int caret_line = get_caret_line(); + + const String &insert_text = code_completion_options[code_completion_current_selected].insert_text; + const String &display_text = code_completion_options[code_completion_current_selected].display; + + if (p_replace) { + /* Find end of current section */ + const String line = get_line(caret_line); + int caret_col = get_caret_column(); + int caret_remove_line = caret_line; + + bool merge_text = true; + int in_string = is_in_string(caret_line, caret_col); + if (in_string != -1) { + Point2 string_end = get_delimiter_end_position(caret_line, caret_col); + if (string_end.x != -1) { + merge_text = false; + caret_remove_line = string_end.y; + caret_col = string_end.x - 1; + } + } + + if (merge_text) { + for (; caret_col < line.length(); caret_col++) { + if (!_is_char(line[caret_col])) { + break; + } + } + } + + /* Replace. */ + remove_text(caret_line, get_caret_column() - code_completion_base.length(), caret_remove_line, caret_col); + set_caret_column(get_caret_column() - code_completion_base.length(), false); + insert_text_at_caret(insert_text); + } else { + /* Get first non-matching char. */ + const String line = get_line(caret_line); + int caret_col = get_caret_column(); + int matching_chars = code_completion_base.length(); + for (; matching_chars <= insert_text.length(); matching_chars++) { + if (caret_col >= line.length() || line[caret_col] != insert_text[matching_chars]) { + break; + } + caret_col++; + } + + /* Remove base completion text. */ + remove_text(caret_line, get_caret_column() - code_completion_base.length(), caret_line, get_caret_column()); + set_caret_column(get_caret_column() - code_completion_base.length(), false); + + /* Merge with text. */ + insert_text_at_caret(insert_text.substr(0, code_completion_base.length())); + set_caret_column(caret_col, false); + insert_text_at_caret(insert_text.substr(matching_chars)); + } + + /* Handle merging of symbols eg strings, brackets. */ + const String line = get_line(caret_line); + char32_t next_char = line[get_caret_column()]; + char32_t last_completion_char = insert_text[insert_text.length() - 1]; + char32_t last_completion_char_display = display_text[display_text.length() - 1]; + + int pre_brace_pair = get_caret_column() > 0 ? _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column()) : -1; + int post_brace_pair = get_caret_column() < get_line(caret_line).length() ? _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column()) : -1; + + if (post_brace_pair != -1 && (last_completion_char == next_char || last_completion_char_display == next_char)) { + remove_text(caret_line, get_caret_column(), caret_line, get_caret_column() + 1); + } + + if (pre_brace_pair != -1 && pre_brace_pair != post_brace_pair && (last_completion_char == next_char || last_completion_char_display == next_char)) { + remove_text(caret_line, get_caret_column(), caret_line, get_caret_column() + 1); + } else if (auto_brace_completion_enabled && pre_brace_pair != -1 && post_brace_pair == -1) { + insert_text_at_caret(auto_brace_completion_pairs[pre_brace_pair].close_key); + set_caret_column(get_caret_column() - auto_brace_completion_pairs[pre_brace_pair].close_key.length()); + } + + if (pre_brace_pair == -1 && post_brace_pair == -1 && get_caret_column() > 0 && get_caret_column() < get_line(caret_line).length()) { + pre_brace_pair = _get_auto_brace_pair_open_at_pos(caret_line, get_caret_column() + 1); + if (pre_brace_pair != -1 && pre_brace_pair == _get_auto_brace_pair_close_at_pos(caret_line, get_caret_column() - 1)) { + remove_text(caret_line, get_caret_column() - 2, caret_line, get_caret_column()); + if (_get_auto_brace_pair_close_at_pos(caret_line, get_caret_column() - 1) != pre_brace_pair) { + set_caret_column(get_caret_column() - 1); + } + } + } + + end_complex_operation(); + + cancel_code_completion(); + if (code_completion_prefixes.has(String::chr(last_completion_char))) { + request_code_completion(); + } +} + +void CodeEdit::cancel_code_completion() { + if (!code_completion_active) { + return; + } + code_completion_forced = false; + code_completion_active = false; + update(); +} + +/* Line length guidelines */ +void CodeEdit::set_line_length_guidelines(TypedArray<int> p_guideline_columns) { + line_length_guideline_columns = p_guideline_columns; + update(); +} + +TypedArray<int> CodeEdit::get_line_length_guidelines() const { + return line_length_guideline_columns; +} + +/* Symbol lookup */ +void CodeEdit::set_symbol_lookup_on_click_enabled(bool p_enabled) { + symbol_lookup_on_click_enabled = p_enabled; + set_symbol_lookup_word_as_valid(false); +} + +bool CodeEdit::is_symbol_lookup_on_click_enabled() const { + return symbol_lookup_on_click_enabled; +} + +String CodeEdit::get_text_for_symbol_lookup() { + Point2i mp = get_local_mouse_pos(); + + Point2i pos = get_line_column_at_pos(mp); + int line = pos.y; + int col = pos.x; + + StringBuilder lookup_text; + const int text_size = get_line_count(); + for (int i = 0; i < text_size; i++) { + String text = get_line(i); + + if (i == line) { + lookup_text += text.substr(0, col); + /* Not unicode, represents the cursor. */ + lookup_text += String::chr(0xFFFF); + lookup_text += text.substr(col, text.size()); + } else { + lookup_text += text; + } + + if (i != text_size - 1) { + lookup_text += "\n"; + } + } + return lookup_text.as_string(); +} + +void CodeEdit::set_symbol_lookup_word_as_valid(bool p_valid) { + symbol_lookup_word = p_valid ? symbol_lookup_new_word : ""; + symbol_lookup_new_word = ""; + if (lookup_symbol_word != symbol_lookup_word) { + _set_symbol_lookup_word(symbol_lookup_word); + } +} + void CodeEdit::_bind_methods() { + /* Indent management */ + ClassDB::bind_method(D_METHOD("set_indent_size", "size"), &CodeEdit::set_indent_size); + ClassDB::bind_method(D_METHOD("get_indent_size"), &CodeEdit::get_indent_size); + + ClassDB::bind_method(D_METHOD("set_indent_using_spaces", "use_spaces"), &CodeEdit::set_indent_using_spaces); + ClassDB::bind_method(D_METHOD("is_indent_using_spaces"), &CodeEdit::is_indent_using_spaces); + + ClassDB::bind_method(D_METHOD("set_auto_indent_enabled", "enable"), &CodeEdit::set_auto_indent_enabled); + ClassDB::bind_method(D_METHOD("is_auto_indent_enabled"), &CodeEdit::is_auto_indent_enabled); + + ClassDB::bind_method(D_METHOD("set_auto_indent_prefixes", "prefixes"), &CodeEdit::set_auto_indent_prefixes); + ClassDB::bind_method(D_METHOD("get_auto_indent_prefixes"), &CodeEdit::get_auto_indent_prefixes); + + ClassDB::bind_method(D_METHOD("do_indent"), &CodeEdit::do_indent); + ClassDB::bind_method(D_METHOD("do_unindent"), &CodeEdit::do_unindent); + + ClassDB::bind_method(D_METHOD("indent_lines"), &CodeEdit::indent_lines); + ClassDB::bind_method(D_METHOD("unindent_lines"), &CodeEdit::unindent_lines); + + /* Auto brace completion */ + ClassDB::bind_method(D_METHOD("set_auto_brace_completion_enabled", "enable"), &CodeEdit::set_auto_brace_completion_enabled); + ClassDB::bind_method(D_METHOD("is_auto_brace_completion_enabled"), &CodeEdit::is_auto_brace_completion_enabled); + + ClassDB::bind_method(D_METHOD("set_highlight_matching_braces_enabled", "enable"), &CodeEdit::set_highlight_matching_braces_enabled); + ClassDB::bind_method(D_METHOD("is_highlight_matching_braces_enabled"), &CodeEdit::is_highlight_matching_braces_enabled); + + ClassDB::bind_method(D_METHOD("add_auto_brace_completion_pair", "start_key", "end_key"), &CodeEdit::add_auto_brace_completion_pair); + ClassDB::bind_method(D_METHOD("set_auto_brace_completion_pairs", "pairs"), &CodeEdit::set_auto_brace_completion_pairs); + ClassDB::bind_method(D_METHOD("get_auto_brace_completion_pairs"), &CodeEdit::get_auto_brace_completion_pairs); + + ClassDB::bind_method(D_METHOD("has_auto_brace_completion_open_key", "open_key"), &CodeEdit::has_auto_brace_completion_open_key); + ClassDB::bind_method(D_METHOD("has_auto_brace_completion_close_key", "close_key"), &CodeEdit::has_auto_brace_completion_close_key); + + ClassDB::bind_method(D_METHOD("get_auto_brace_completion_close_key", "open_key"), &CodeEdit::get_auto_brace_completion_close_key); + /* Main Gutter */ ClassDB::bind_method(D_METHOD("_main_gutter_draw_callback"), &CodeEdit::_main_gutter_draw_callback); @@ -320,20 +2119,203 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_draw_fold_gutter", "enable"), &CodeEdit::set_draw_fold_gutter); ClassDB::bind_method(D_METHOD("is_drawing_fold_gutter"), &CodeEdit::is_drawing_fold_gutter); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_breakpoints_gutter"), "set_draw_breakpoints_gutter", "is_drawing_breakpoints_gutter"); + /* Line folding */ + ClassDB::bind_method(D_METHOD("set_line_folding_enabled", "enabled"), &CodeEdit::set_line_folding_enabled); + ClassDB::bind_method(D_METHOD("is_line_folding_enabled"), &CodeEdit::is_line_folding_enabled); + + ClassDB::bind_method(D_METHOD("can_fold_line", "line"), &CodeEdit::can_fold_line); + + ClassDB::bind_method(D_METHOD("fold_line", "line"), &CodeEdit::fold_line); + ClassDB::bind_method(D_METHOD("unfold_line", "line"), &CodeEdit::unfold_line); + ClassDB::bind_method(D_METHOD("fold_all_lines"), &CodeEdit::fold_all_lines); + ClassDB::bind_method(D_METHOD("unfold_all_lines"), &CodeEdit::unfold_all_lines); + ClassDB::bind_method(D_METHOD("toggle_foldable_line", "line"), &CodeEdit::toggle_foldable_line); + + ClassDB::bind_method(D_METHOD("is_line_folded", "line"), &CodeEdit::is_line_folded); + ClassDB::bind_method(D_METHOD("get_folded_lines"), &CodeEdit::get_folded_lines); + + /* Delimiters */ + // Strings + ClassDB::bind_method(D_METHOD("add_string_delimiter", "start_key", "end_key", "line_only"), &CodeEdit::add_string_delimiter, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("remove_string_delimiter", "start_key"), &CodeEdit::remove_string_delimiter); + ClassDB::bind_method(D_METHOD("has_string_delimiter", "start_key"), &CodeEdit::has_string_delimiter); + + ClassDB::bind_method(D_METHOD("set_string_delimiters", "string_delimiters"), &CodeEdit::set_string_delimiters); + ClassDB::bind_method(D_METHOD("clear_string_delimiters"), &CodeEdit::clear_string_delimiters); + ClassDB::bind_method(D_METHOD("get_string_delimiters"), &CodeEdit::get_string_delimiters); + + ClassDB::bind_method(D_METHOD("is_in_string", "line", "column"), &CodeEdit::is_in_string, DEFVAL(-1)); + + // Comments + ClassDB::bind_method(D_METHOD("add_comment_delimiter", "start_key", "end_key", "line_only"), &CodeEdit::add_comment_delimiter, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("remove_comment_delimiter", "start_key"), &CodeEdit::remove_comment_delimiter); + ClassDB::bind_method(D_METHOD("has_comment_delimiter", "start_key"), &CodeEdit::has_comment_delimiter); + + ClassDB::bind_method(D_METHOD("set_comment_delimiters", "comment_delimiters"), &CodeEdit::set_comment_delimiters); + ClassDB::bind_method(D_METHOD("clear_comment_delimiters"), &CodeEdit::clear_comment_delimiters); + ClassDB::bind_method(D_METHOD("get_comment_delimiters"), &CodeEdit::get_comment_delimiters); + + ClassDB::bind_method(D_METHOD("is_in_comment", "line", "column"), &CodeEdit::is_in_comment, DEFVAL(-1)); + + // Util + ClassDB::bind_method(D_METHOD("get_delimiter_start_key", "delimiter_index"), &CodeEdit::get_delimiter_start_key); + ClassDB::bind_method(D_METHOD("get_delimiter_end_key", "delimiter_index"), &CodeEdit::get_delimiter_end_key); + + ClassDB::bind_method(D_METHOD("get_delimiter_start_position", "line", "column"), &CodeEdit::get_delimiter_start_position); + ClassDB::bind_method(D_METHOD("get_delimiter_end_position", "line", "column"), &CodeEdit::get_delimiter_end_position); + + /* Code hint */ + ClassDB::bind_method(D_METHOD("set_code_hint", "code_hint"), &CodeEdit::set_code_hint); + ClassDB::bind_method(D_METHOD("set_code_hint_draw_below", "draw_below"), &CodeEdit::set_code_hint_draw_below); + + /* Code Completion */ + BIND_ENUM_CONSTANT(KIND_CLASS); + BIND_ENUM_CONSTANT(KIND_FUNCTION); + BIND_ENUM_CONSTANT(KIND_SIGNAL); + BIND_ENUM_CONSTANT(KIND_VARIABLE); + BIND_ENUM_CONSTANT(KIND_MEMBER); + BIND_ENUM_CONSTANT(KIND_ENUM); + BIND_ENUM_CONSTANT(KIND_CONSTANT); + BIND_ENUM_CONSTANT(KIND_NODE_PATH); + BIND_ENUM_CONSTANT(KIND_FILE_PATH); + BIND_ENUM_CONSTANT(KIND_PLAIN_TEXT); + + ClassDB::bind_method(D_METHOD("get_text_for_code_completion"), &CodeEdit::get_text_for_code_completion); + ClassDB::bind_method(D_METHOD("request_code_completion", "force"), &CodeEdit::request_code_completion, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("add_code_completion_option", "type", "display_text", "insert_text", "text_color", "icon", "value"), &CodeEdit::add_code_completion_option, DEFVAL(Color(1, 1, 1)), DEFVAL(RES()), DEFVAL(Variant::NIL)); + ClassDB::bind_method(D_METHOD("update_code_completion_options", "force"), &CodeEdit::update_code_completion_options); + ClassDB::bind_method(D_METHOD("get_code_completion_options"), &CodeEdit::get_code_completion_options); + ClassDB::bind_method(D_METHOD("get_code_completion_option", "index"), &CodeEdit::get_code_completion_option); + ClassDB::bind_method(D_METHOD("get_code_completion_selected_index"), &CodeEdit::get_code_completion_selected_index); + ClassDB::bind_method(D_METHOD("set_code_completion_selected_index", "index"), &CodeEdit::set_code_completion_selected_index); + + ClassDB::bind_method(D_METHOD("confirm_code_completion", "replace"), &CodeEdit::confirm_code_completion, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("cancel_code_completion"), &CodeEdit::cancel_code_completion); + + ClassDB::bind_method(D_METHOD("set_code_completion_enabled", "enable"), &CodeEdit::set_code_completion_enabled); + ClassDB::bind_method(D_METHOD("is_code_completion_enabled"), &CodeEdit::is_code_completion_enabled); + + ClassDB::bind_method(D_METHOD("set_code_completion_prefixes", "prefixes"), &CodeEdit::set_code_completion_prefixes); + ClassDB::bind_method(D_METHOD("get_code_comletion_prefixes"), &CodeEdit::get_code_completion_prefixes); + + // Overridable + + GDVIRTUAL_BIND(_confirm_code_completion, "replace") + GDVIRTUAL_BIND(_request_code_completion, "force") + GDVIRTUAL_BIND(_filter_code_completion_candidates, "candidates") + + /* Line length guidelines */ + ClassDB::bind_method(D_METHOD("set_line_length_guidelines", "guideline_columns"), &CodeEdit::set_line_length_guidelines); + ClassDB::bind_method(D_METHOD("get_line_length_guidelines"), &CodeEdit::get_line_length_guidelines); + + /* Symbol lookup */ + ClassDB::bind_method(D_METHOD("set_symbol_lookup_on_click_enabled", "enable"), &CodeEdit::set_symbol_lookup_on_click_enabled); + ClassDB::bind_method(D_METHOD("is_symbol_lookup_on_click_enabled"), &CodeEdit::is_symbol_lookup_on_click_enabled); + + ClassDB::bind_method(D_METHOD("get_text_for_symbol_lookup"), &CodeEdit::get_text_for_symbol_lookup); + + ClassDB::bind_method(D_METHOD("set_symbol_lookup_word_as_valid", "valid"), &CodeEdit::set_symbol_lookup_word_as_valid); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_bookmarks"), "set_draw_bookmarks_gutter", "is_drawing_bookmarks_gutter"); + /* Inspector */ + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "symbol_lookup_on_click"), "set_symbol_lookup_on_click_enabled", "is_symbol_lookup_on_click_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "line_folding"), "set_line_folding_enabled", "is_line_folding_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_executing_lines"), "set_draw_executing_lines_gutter", "is_drawing_executing_lines_gutter"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_INT32_ARRAY, "line_length_guidelines"), "set_line_length_guidelines", "get_line_length_guidelines"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_line_numbers"), "set_draw_line_numbers", "is_draw_line_numbers_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "zero_pad_line_numbers"), "set_line_numbers_zero_padded", "is_line_numbers_zero_padded"); + ADD_GROUP("Gutters", "gutters_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_breakpoints_gutter"), "set_draw_breakpoints_gutter", "is_drawing_breakpoints_gutter"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_fold_gutter"), "set_draw_fold_gutter", "is_drawing_fold_gutter"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_bookmarks"), "set_draw_bookmarks_gutter", "is_drawing_bookmarks_gutter"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_executing_lines"), "set_draw_executing_lines_gutter", "is_drawing_executing_lines_gutter"); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_line_numbers"), "set_draw_line_numbers", "is_draw_line_numbers_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_zero_pad_line_numbers"), "set_line_numbers_zero_padded", "is_line_numbers_zero_padded"); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "gutters_draw_fold_gutter"), "set_draw_fold_gutter", "is_drawing_fold_gutter"); + + ADD_GROUP("Delimiters", "delimiter_"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "delimiter_strings"), "set_string_delimiters", "get_string_delimiters"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "delimiter_comments"), "set_comment_delimiters", "get_comment_delimiters"); + + ADD_GROUP("Code Completion", "code_completion_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "code_completion_enabled"), "set_code_completion_enabled", "is_code_completion_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "code_completion_prefixes"), "set_code_completion_prefixes", "get_code_comletion_prefixes"); + + ADD_GROUP("Indentation", "indent_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "indent_size"), "set_indent_size", "get_indent_size"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "indent_use_spaces"), "set_indent_using_spaces", "is_indent_using_spaces"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "indent_automatic"), "set_auto_indent_enabled", "is_auto_indent_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "indent_automatic_prefixes"), "set_auto_indent_prefixes", "get_auto_indent_prefixes"); + + ADD_GROUP("Auto brace completion", "auto_brace_completion_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_brace_completion_enabled"), "set_auto_brace_completion_enabled", "is_auto_brace_completion_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_brace_completion_highlight_matching"), "set_highlight_matching_braces_enabled", "is_highlight_matching_braces_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "auto_brace_completion_pairs"), "set_auto_brace_completion_pairs", "get_auto_brace_completion_pairs"); + + /* Signals */ + /* Gutters */ ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::INT, "line"))); + + /* Code Completion */ + ADD_SIGNAL(MethodInfo("request_code_completion")); + + /* Symbol lookup */ + ADD_SIGNAL(MethodInfo("symbol_lookup", PropertyInfo(Variant::STRING, "symbol"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::INT, "column"))); + ADD_SIGNAL(MethodInfo("symbol_validate", PropertyInfo(Variant::STRING, "symbol"))); +} + +/* Auto brace completion */ +int CodeEdit::_get_auto_brace_pair_open_at_pos(int p_line, int p_col) { + const String &line = get_line(p_line); + + /* Should be fast enough, expecting low amount of pairs... */ + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + const String &open_key = auto_brace_completion_pairs[i].open_key; + if (p_col - open_key.length() < 0) { + continue; + } + + bool is_match = true; + for (int j = 0; j < open_key.length(); j++) { + if (line[(p_col - 1) - j] != open_key[(open_key.length() - 1) - j]) { + is_match = false; + break; + } + } + + if (is_match) { + return i; + } + } + return -1; +} + +int CodeEdit::_get_auto_brace_pair_close_at_pos(int p_line, int p_col) { + const String &line = get_line(p_line); + + /* Should be fast enough, expecting low amount of pairs... */ + for (int i = 0; i < auto_brace_completion_pairs.size(); i++) { + if (p_col + auto_brace_completion_pairs[i].close_key.length() > line.length()) { + continue; + } + + bool is_match = true; + for (int j = 0; j < auto_brace_completion_pairs[i].close_key.length(); j++) { + if (line[p_col + j] != auto_brace_completion_pairs[i].close_key[j]) { + is_match = false; + break; + } + } + + if (is_match) { + return i; + } + } + return -1; } +/* Gutters */ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { if (p_gutter == main_gutter) { if (draw_breakpoints) { @@ -345,73 +2327,673 @@ void CodeEdit::_gutter_clicked(int p_line, int p_gutter) { if (p_gutter == line_number_gutter) { set_selection_mode(TextEdit::SelectionMode::SELECTION_MODE_LINE, p_line, 0); select(p_line, 0, p_line + 1, 0); - cursor_set_line(p_line + 1); - cursor_set_column(0); + set_caret_line(p_line + 1); + set_caret_column(0); return; } if (p_gutter == fold_gutter) { - if (is_folded(p_line)) { + if (is_line_folded(p_line)) { unfold_line(p_line); - } else if (can_fold(p_line)) { + } else if (can_fold_line(p_line)) { fold_line(p_line); } return; } } -void CodeEdit::_lines_edited_from(int p_from_line, int p_to_line) { - if (p_from_line == p_to_line) { +void CodeEdit::_update_gutter_indexes() { + for (int i = 0; i < get_gutter_count(); i++) { + if (get_gutter_name(i) == "main_gutter") { + main_gutter = i; + continue; + } + + if (get_gutter_name(i) == "line_numbers") { + line_number_gutter = i; + continue; + } + + if (get_gutter_name(i) == "fold_gutter") { + fold_gutter = i; + continue; + } + } +} + +/* Delimiters */ +void CodeEdit::_update_delimiter_cache(int p_from_line, int p_to_line) { + if (delimiters.size() == 0) { return; } - int lc = get_line_count(); - line_number_digits = 1; - while (lc /= 10) { - line_number_digits++; + int line_count = get_line_count(); + if (p_to_line == -1) { + p_to_line = line_count; } - set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0', 0, cache.font_size).width); - int from_line = MIN(p_from_line, p_to_line); - int line_count = (p_to_line - p_from_line); - List<int> breakpoints; - breakpointed_lines.get_key_list(&breakpoints); - for (const List<int>::Element *E = breakpoints.front(); E; E = E->next()) { - int line = E->get(); - if (line <= from_line) { + int start_line = MIN(p_from_line, p_to_line); + int end_line = MAX(p_from_line, p_to_line); + + /* Make sure delimiter_cache has all the lines. */ + if (start_line != end_line) { + if (p_to_line < p_from_line) { + for (int i = end_line; i > start_line; i--) { + delimiter_cache.remove(i); + } + } else { + for (int i = start_line; i < end_line; i++) { + delimiter_cache.insert(i, Map<int, int>()); + } + } + } + + int in_region = -1; + for (int i = start_line; i < MIN(end_line + 1, line_count); i++) { + int current_end_region = (i <= 0 || delimiter_cache[i].size() < 1) ? -1 : delimiter_cache[i].back()->value(); + in_region = (i <= 0 || delimiter_cache[i - 1].size() < 1) ? -1 : delimiter_cache[i - 1].back()->value(); + + const String &str = get_line(i); + const int line_length = str.length(); + delimiter_cache.write[i].clear(); + + if (str.length() == 0) { + if (in_region != -1) { + delimiter_cache.write[i][0] = in_region; + } + if (i == end_line && current_end_region != in_region) { + end_line++; + end_line = MIN(end_line, line_count); + } continue; } - breakpointed_lines.erase(line); - emit_signal("breakpoint_toggled", line); - if (line_count > 0 || line >= p_from_line) { - emit_signal("breakpoint_toggled", line + line_count); - breakpointed_lines[line + line_count] = true; + int end_region = -1; + for (int j = 0; j < line_length; j++) { + int from = j; + for (; from < line_length; from++) { + if (str[from] == '\\') { + from++; + continue; + } + break; + } + + /* check if we are in entering a region */ + bool same_line = false; + if (in_region == -1) { + for (int d = 0; d < delimiters.size(); d++) { + /* check there is enough room */ + int chars_left = line_length - from; + int start_key_length = delimiters[d].start_key.length(); + int end_key_length = delimiters[d].end_key.length(); + if (chars_left < start_key_length) { + continue; + } + + /* search the line */ + bool match = true; + const char32_t *start_key = delimiters[d].start_key.get_data(); + for (int k = 0; k < start_key_length; k++) { + if (start_key[k] != str[from + k]) { + match = false; + break; + } + } + if (!match) { + continue; + } + same_line = true; + in_region = d; + delimiter_cache.write[i][from + 1] = d; + from += start_key_length; + + /* check if it's the whole line */ + if (end_key_length == 0 || delimiters[d].line_only || from + end_key_length > line_length) { + j = line_length; + if (delimiters[d].line_only) { + delimiter_cache.write[i][line_length + 1] = -1; + } else { + end_region = in_region; + } + } + break; + } + + if (j == line_length || in_region == -1) { + continue; + } + } + + /* if we are in one find the end key */ + /* search the line */ + int region_end_index = -1; + int end_key_length = delimiters[in_region].end_key.length(); + const char32_t *end_key = delimiters[in_region].end_key.get_data(); + for (; from < line_length; from++) { + if (line_length - from < end_key_length) { + break; + } + + if (!is_symbol(str[from])) { + continue; + } + + if (str[from] == '\\') { + from++; + continue; + } + + region_end_index = from; + for (int k = 0; k < end_key_length; k++) { + if (end_key[k] != str[from + k]) { + region_end_index = -1; + break; + } + } + + if (region_end_index != -1) { + break; + } + } + + j = from + (end_key_length - 1); + end_region = (region_end_index == -1) ? in_region : -1; + if (!same_line || region_end_index != -1) { + delimiter_cache.write[i][j + 1] = end_region; + } + in_region = -1; + } + + if (i == end_line && current_end_region != end_region) { + end_line++; + end_line = MIN(end_line, line_count); + } + } +} + +int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) const { + if (delimiters.size() == 0) { + return -1; + } + ERR_FAIL_INDEX_V(p_line, get_line_count(), 0); + + int region = (p_line <= 0 || delimiter_cache[p_line - 1].size() < 1) ? -1 : delimiter_cache[p_line - 1].back()->value(); + bool in_region = region != -1 && delimiters[region].type == p_type; + for (Map<int, int>::Element *E = delimiter_cache[p_line].front(); E; E = E->next()) { + /* If column is specified, loop until the key is larger then the column. */ + if (p_column != -1) { + if (E->key() > p_column) { + break; + } + in_region = E->value() != -1 && delimiters[E->value()].type == p_type; + region = in_region ? E->value() : -1; continue; } + + /* If no column, calculate if the entire line is a region */ + /* excluding whitespace. */ + const String line = get_line(p_line); + if (!in_region) { + if (E->value() == -1 || delimiters[E->value()].type != p_type) { + break; + } + + region = E->value(); + in_region = true; + for (int i = E->key() - 2; i >= 0; i--) { + if (!_is_whitespace(line[i])) { + return -1; + } + } + } + + if (delimiters[region].line_only) { + return region; + } + + int end_col = E->key(); + if (E->value() != -1) { + if (!E->next()) { + return region; + } + end_col = E->next()->key(); + } + + for (int i = end_col; i < line.length(); i++) { + if (!_is_whitespace(line[i])) { + return -1; + } + } + return region; } + return in_region ? region : -1; } -void CodeEdit::_update_gutter_indexes() { - for (int i = 0; i < get_gutter_count(); i++) { - if (get_gutter_name(i) == "main_gutter") { - main_gutter = i; +void CodeEdit::_add_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only, DelimiterType p_type) { + // If we are the editor allow "null" as a valid start key, otherwise users cannot add delimiters via the inspector. + if (!(Engine::get_singleton()->is_editor_hint() && p_start_key == "null")) { + ERR_FAIL_COND_MSG(p_start_key.is_empty(), "delimiter start key cannot be empty"); + + for (int i = 0; i < p_start_key.length(); i++) { + ERR_FAIL_COND_MSG(!is_symbol(p_start_key[i]), "delimiter must start with a symbol"); + } + } + + if (p_end_key.length() > 0) { + for (int i = 0; i < p_end_key.length(); i++) { + ERR_FAIL_COND_MSG(!is_symbol(p_end_key[i]), "delimiter must end with a symbol"); + } + } + + int at = 0; + for (int i = 0; i < delimiters.size(); i++) { + ERR_FAIL_COND_MSG(delimiters[i].start_key == p_start_key, "delimiter with start key '" + p_start_key + "' already exists."); + if (p_start_key.length() < delimiters[i].start_key.length()) { + at++; + } + } + + Delimiter delimiter; + delimiter.type = p_type; + delimiter.start_key = p_start_key; + delimiter.end_key = p_end_key; + delimiter.line_only = p_line_only || p_end_key == ""; + delimiters.insert(at, delimiter); + if (!setting_delimiters) { + delimiter_cache.clear(); + _update_delimiter_cache(); + } +} + +void CodeEdit::_remove_delimiter(const String &p_start_key, DelimiterType p_type) { + for (int i = 0; i < delimiters.size(); i++) { + if (delimiters[i].start_key != p_start_key) { continue; } - if (get_gutter_name(i) == "line_numbers") { - line_number_gutter = i; + if (delimiters[i].type != p_type) { + break; + } + + delimiters.remove(i); + if (!setting_delimiters) { + delimiter_cache.clear(); + _update_delimiter_cache(); + } + break; + } +} + +bool CodeEdit::_has_delimiter(const String &p_start_key, DelimiterType p_type) const { + for (int i = 0; i < delimiters.size(); i++) { + if (delimiters[i].start_key == p_start_key) { + return delimiters[i].type == p_type; + } + } + return false; +} + +void CodeEdit::_set_delimiters(const TypedArray<String> &p_delimiters, DelimiterType p_type) { + setting_delimiters = true; + _clear_delimiters(p_type); + + for (int i = 0; i < p_delimiters.size(); i++) { + String key = p_delimiters[i]; + + if (key.is_empty()) { continue; } - if (get_gutter_name(i) == "fold_gutter") { - fold_gutter = i; + const String start_key = key.get_slice(" ", 0); + const String end_key = key.get_slice_count(" ") > 1 ? key.get_slice(" ", 1) : String(); + + _add_delimiter(start_key, end_key, end_key == "", p_type); + } + setting_delimiters = false; + _update_delimiter_cache(); +} + +void CodeEdit::_clear_delimiters(DelimiterType p_type) { + for (int i = delimiters.size() - 1; i >= 0; i--) { + if (delimiters[i].type == p_type) { + delimiters.remove(i); + } + } + delimiter_cache.clear(); + if (!setting_delimiters) { + _update_delimiter_cache(); + } +} + +TypedArray<String> CodeEdit::_get_delimiters(DelimiterType p_type) const { + TypedArray<String> r_delimiters; + for (int i = 0; i < delimiters.size(); i++) { + if (delimiters[i].type != p_type) { + continue; + } + r_delimiters.push_back(delimiters[i].start_key + (delimiters[i].end_key.is_empty() ? "" : " " + delimiters[i].end_key)); + } + return r_delimiters; +} + +/* Code Completion */ +void CodeEdit::_filter_code_completion_candidates_impl() { + int line_height = get_line_height(); + + if (GDVIRTUAL_IS_OVERRIDDEN(_filter_code_completion_candidates)) { + code_completion_options.clear(); + code_completion_base = ""; + + /* Build options argument. */ + TypedArray<Dictionary> completion_options_sources; + completion_options_sources.resize(code_completion_option_sources.size()); + int i = 0; + for (const ScriptCodeCompletionOption &E : code_completion_option_sources) { + Dictionary option; + option["kind"] = E.kind; + option["display_text"] = E.display; + option["insert_text"] = E.insert_text; + option["font_color"] = E.font_color; + option["icon"] = E.icon; + option["default_value"] = E.default_value; + completion_options_sources[i] = option; + i++; + } + + Array completion_options; + + GDVIRTUAL_CALL(_filter_code_completion_candidates, completion_options_sources, completion_options); + + /* No options to complete, cancel. */ + if (completion_options.size() == 0) { + cancel_code_completion(); + return; + } + + /* Convert back into options. */ + int max_width = 0; + for (i = 0; i < completion_options.size(); i++) { + ScriptCodeCompletionOption option; + option.kind = (ScriptCodeCompletionOption::Kind)(int)completion_options[i].get("kind"); + option.display = completion_options[i].get("display_text"); + option.insert_text = completion_options[i].get("insert_text"); + option.font_color = completion_options[i].get("font_color"); + option.icon = completion_options[i].get("icon"); + option.default_value = completion_options[i].get("default_value"); + + int offset = 0; + if (option.default_value.get_type() == Variant::COLOR) { + offset = line_height; + } + + max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); + code_completion_options.push_back(option); + } + + code_completion_longest_line = MIN(max_width, code_completion_max_width * font_size); + code_completion_current_selected = 0; + code_completion_active = true; + update(); + return; + } + + const int caret_line = get_caret_line(); + const int caret_column = get_caret_column(); + const String line = get_line(caret_line); + + if (caret_column > 0 && line[caret_column - 1] == '(' && !code_completion_forced) { + cancel_code_completion(); + return; + } + + /* Get string status, are we in one or at the close. */ + int in_string = is_in_string(caret_line, caret_column); + int first_quote_col = -1; + if (in_string != -1) { + Point2 string_start_pos = get_delimiter_start_position(caret_line, caret_column); + first_quote_col = (string_start_pos.y == caret_line) ? string_start_pos.x : -1; + } else if (caret_column > 0) { + if (is_in_string(caret_line, caret_column - 1) != -1) { + first_quote_col = caret_column - 1; + } + } + + int cofs = caret_column; + String string_to_complete; + bool prev_is_word = false; + + /* Cancel if we are at the close of a string. */ + if (in_string == -1 && first_quote_col == cofs - 1) { + cancel_code_completion(); + return; + /* In a string, therefore we are trying to complete the string text. */ + } else if (in_string != -1 && first_quote_col != -1) { + int key_length = delimiters[in_string].start_key.length(); + string_to_complete = line.substr(first_quote_col - key_length, (cofs - first_quote_col) + key_length); + /* If we have a space, previous word might be a keyword. eg "func |". */ + } else if (cofs > 0 && line[cofs - 1] == ' ') { + int ofs = cofs - 1; + while (ofs > 0 && line[ofs] == ' ') { + ofs--; + } + prev_is_word = _is_char(line[ofs]); + /* Otherwise get current word and set cofs to the start. */ + } else { + int start_cofs = cofs; + while (cofs > 0 && line[cofs - 1] > 32 && (line[cofs - 1] == '/' || _is_char(line[cofs - 1]))) { + cofs--; + } + string_to_complete = line.substr(cofs, start_cofs - cofs); + } + + /* If all else fails, check for a prefix. */ + /* Single space between caret and prefix is okay. */ + bool prev_is_prefix = false; + if (cofs > 0 && code_completion_prefixes.has(String::chr(line[cofs - 1]))) { + prev_is_prefix = true; + } else if (cofs > 1 && line[cofs - 1] == ' ' && code_completion_prefixes.has(String::chr(line[cofs - 2]))) { + prev_is_prefix = true; + } + + if (!prev_is_word && string_to_complete.is_empty() && (cofs == 0 || !prev_is_prefix)) { + cancel_code_completion(); + return; + } + + /* Filter Options. */ + /* For now handle only tradional quoted strings. */ + bool single_quote = in_string != -1 && first_quote_col > 0 && delimiters[in_string].start_key == "'"; + + code_completion_options.clear(); + code_completion_base = string_to_complete; + + Vector<ScriptCodeCompletionOption> completion_options_casei; + Vector<ScriptCodeCompletionOption> completion_options_subseq; + Vector<ScriptCodeCompletionOption> completion_options_subseq_casei; + + int max_width = 0; + String string_to_complete_lower = string_to_complete.to_lower(); + for (ScriptCodeCompletionOption &option : code_completion_option_sources) { + if (single_quote && option.display.is_quoted()) { + option.display = option.display.unquote().quote("'"); + } + + int offset = 0; + if (option.default_value.get_type() == Variant::COLOR) { + offset = line_height; + } + + if (in_string != -1) { + String quote = single_quote ? "'" : "\""; + option.display = option.display.unquote().quote(quote); + option.insert_text = option.insert_text.unquote().quote(quote); + } + + if (option.display.length() == 0) { continue; } + + if (string_to_complete.length() == 0) { + code_completion_options.push_back(option); + max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); + continue; + } + + /* This code works the same as: + + if (option.display.begins_with(s)) { + completion_options.push_back(option); + } else if (option.display.to_lower().begins_with(s.to_lower())) { + completion_options_casei.push_back(option); + } else if (s.is_subsequence_of(option.display)) { + completion_options_subseq.push_back(option); + } else if (s.is_subsequence_ofi(option.display)) { + completion_options_subseq_casei.push_back(option); + } + + But is more performant due to being inlined and looping over the characters only once + */ + + String display_lower = option.display.to_lower(); + + const char32_t *ssq = &string_to_complete[0]; + const char32_t *ssq_lower = &string_to_complete_lower[0]; + + const char32_t *tgt = &option.display[0]; + const char32_t *tgt_lower = &display_lower[0]; + + const char32_t *ssq_last_tgt = nullptr; + const char32_t *ssq_lower_last_tgt = nullptr; + + for (; *tgt; tgt++, tgt_lower++) { + if (*ssq == *tgt) { + ssq++; + ssq_last_tgt = tgt; + } + if (*ssq_lower == *tgt_lower) { + ssq_lower++; + ssq_lower_last_tgt = tgt; + } + } + + /* Matched the whole subsequence in s. */ + if (!*ssq) { + /* Finished matching in the first s.length() characters. */ + if (ssq_last_tgt == &option.display[string_to_complete.length() - 1]) { + code_completion_options.push_back(option); + } else { + completion_options_subseq.push_back(option); + } + max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); + /* Matched the whole subsequence in s_lower. */ + } else if (!*ssq_lower) { + /* Finished matching in the first s.length() characters. */ + if (ssq_lower_last_tgt == &option.display[string_to_complete.length() - 1]) { + completion_options_casei.push_back(option); + } else { + completion_options_subseq_casei.push_back(option); + } + max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); + } + } + + code_completion_options.append_array(completion_options_casei); + code_completion_options.append_array(completion_options_subseq); + code_completion_options.append_array(completion_options_subseq_casei); + + /* No options to complete, cancel. */ + if (code_completion_options.size() == 0) { + cancel_code_completion(); + return; } + + /* A perfect match, stop completion. */ + if (code_completion_options.size() == 1 && string_to_complete == code_completion_options[0].display) { + cancel_code_completion(); + return; + } + + code_completion_longest_line = MIN(max_width, code_completion_max_width * font_size); + code_completion_current_selected = 0; + code_completion_active = true; + update(); +} + +void CodeEdit::_lines_edited_from(int p_from_line, int p_to_line) { + _update_delimiter_cache(p_from_line, p_to_line); + + if (p_from_line == p_to_line) { + return; + } + + lines_edited_from = (lines_edited_from == -1) ? MIN(p_from_line, p_to_line) : MIN(lines_edited_from, MIN(p_from_line, p_to_line)); + lines_edited_to = (lines_edited_to == -1) ? MAX(p_from_line, p_to_line) : MAX(lines_edited_from, MAX(p_from_line, p_to_line)); +} + +void CodeEdit::_text_set() { + lines_edited_from = 0; + lines_edited_to = 9999; + _text_changed(); +} + +void CodeEdit::_text_changed() { + if (lines_edited_from == -1) { + return; + } + + int lc = get_line_count(); + line_number_digits = 1; + while (lc /= 10) { + line_number_digits++; + } + + if (font.is_valid()) { + set_gutter_width(line_number_gutter, (line_number_digits + 1) * font->get_char_size('0', 0, font_size).width); + } + + lc = get_line_count(); + int line_change_size = (lines_edited_to - lines_edited_from); + List<int> breakpoints; + breakpointed_lines.get_key_list(&breakpoints); + for (const int &line : breakpoints) { + if (line < lines_edited_from || (line < lc && is_line_breakpointed(line))) { + continue; + } + + breakpointed_lines.erase(line); + emit_signal(SNAME("breakpoint_toggled"), line); + + int next_line = line + line_change_size; + if (next_line < lc && is_line_breakpointed(next_line)) { + emit_signal(SNAME("breakpoint_toggled"), next_line); + breakpointed_lines[next_line] = true; + continue; + } + } + + lines_edited_from = -1; + lines_edited_to = -1; } CodeEdit::CodeEdit() { + /* Indent management */ + auto_indent_prefixes.insert(':'); + auto_indent_prefixes.insert('{'); + auto_indent_prefixes.insert('['); + auto_indent_prefixes.insert('('); + + /* Auto brace completion */ + add_auto_brace_completion_pair("(", ")"); + add_auto_brace_completion_pair("{", "}"); + add_auto_brace_completion_pair("[", "]"); + add_auto_brace_completion_pair("\"", "\""); + add_auto_brace_completion_pair("\'", "\'"); + + /* Delimiter traking */ + add_string_delimiter("\"", "\"", false); + add_string_delimiter("\'", "\'", false); + /* Text Direction */ set_layout_direction(LAYOUT_DIRECTION_LTR); set_text_direction(TEXT_DIRECTION_LTR); @@ -424,7 +3006,7 @@ CodeEdit::CodeEdit() { set_gutter_name(gutter_idx, "main_gutter"); set_gutter_draw(gutter_idx, false); set_gutter_overwritable(gutter_idx, true); - set_gutter_type(gutter_idx, GUTTER_TPYE_CUSTOM); + set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); set_gutter_custom_draw(gutter_idx, this, "_main_gutter_draw_callback"); gutter_idx++; @@ -432,7 +3014,7 @@ CodeEdit::CodeEdit() { add_gutter(); set_gutter_name(gutter_idx, "line_numbers"); set_gutter_draw(gutter_idx, false); - set_gutter_type(gutter_idx, GUTTER_TPYE_CUSTOM); + set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); set_gutter_custom_draw(gutter_idx, this, "_line_number_draw_callback"); gutter_idx++; @@ -440,13 +3022,15 @@ CodeEdit::CodeEdit() { add_gutter(); set_gutter_name(gutter_idx, "fold_gutter"); set_gutter_draw(gutter_idx, false); - set_gutter_type(gutter_idx, GUTTER_TPYE_CUSTOM); + set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); set_gutter_custom_draw(gutter_idx, this, "_fold_gutter_draw_callback"); gutter_idx++; connect("lines_edited_from", callable_mp(this, &CodeEdit::_lines_edited_from)); - connect("gutter_clicked", callable_mp(this, &CodeEdit::_gutter_clicked)); + connect("text_set", callable_mp(this, &CodeEdit::_text_set)); + connect("text_changed", callable_mp(this, &CodeEdit::_text_changed)); + connect("gutter_clicked", callable_mp(this, &CodeEdit::_gutter_clicked)); connect("gutter_added", callable_mp(this, &CodeEdit::_update_gutter_indexes)); connect("gutter_removed", callable_mp(this, &CodeEdit::_update_gutter_indexes)); _update_gutter_indexes(); diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index d0c39ec0f1..740548d559 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -36,7 +36,49 @@ class CodeEdit : public TextEdit { GDCLASS(CodeEdit, TextEdit) +public: + /* Keep enum in sync with: */ + /* /core/object/script_language.h - ScriptCodeCompletionOption::Kind */ + enum CodeCompletionKind { + KIND_CLASS, + KIND_FUNCTION, + KIND_SIGNAL, + KIND_VARIABLE, + KIND_MEMBER, + KIND_ENUM, + KIND_CONSTANT, + KIND_NODE_PATH, + KIND_FILE_PATH, + KIND_PLAIN_TEXT, + }; + private: + /* Indent management */ + int indent_size = 4; + String indent_text = "\t"; + + bool auto_indent = false; + Set<char32_t> auto_indent_prefixes; + + bool indent_using_spaces = false; + int _calculate_spaces_till_next_left_indent(int p_column) const; + int _calculate_spaces_till_next_right_indent(int p_column) const; + + void _new_line(bool p_split_current_line = true, bool p_above = false); + + /* Auto brace completion */ + bool auto_brace_completion_enabled = false; + + /* BracePair open_key must be uniquie and ordered by length. */ + struct BracePair { + String open_key = ""; + String close_key = ""; + }; + Vector<BracePair> auto_brace_completion_pairs; + + int _get_auto_brace_pair_open_at_pos(int p_line, int p_col); + int _get_auto_brace_pair_close_at_pos(int p_line, int p_col); + /* Main Gutter */ enum MainGutterType { MAIN_GUTTER_BREAKPOINT = 0x01, @@ -80,16 +122,186 @@ private: void _fold_gutter_draw_callback(int p_line, int p_gutter, Rect2 p_region); void _gutter_clicked(int p_line, int p_gutter); - void _lines_edited_from(int p_from_line, int p_to_line); - void _update_gutter_indexes(); + /* Line Folding */ + bool line_folding_enabled = false; + + /* Delimiters */ + enum DelimiterType { + TYPE_STRING, + TYPE_COMMENT, + }; + + struct Delimiter { + DelimiterType type; + String start_key = ""; + String end_key = ""; + bool line_only = true; + }; + bool setting_delimiters = false; + Vector<Delimiter> delimiters; + /* + * Vector entry per line, contains a Map of column numbers to delimiter index, -1 marks the end of a region. + * e.g the following text will be stored as so: + * + * 0: nothing here + * 1: + * 2: # test + * 3: "test" text "multiline + * 4: + * 5: test + * 6: string" + * + * Vector [ + * 0 = [] + * 1 = [] + * 2 = [ + * 1 = 1 + * 6 = -1 + * ] + * 3 = [ + * 1 = 0 + * 6 = -1 + * 13 = 0 + * ] + * 4 = [ + * 0 = 0 + * ] + * 5 = [ + * 5 = 0 + * ] + * 6 = [ + * 7 = -1 + * ] + * ] + */ + Vector<Map<int, int>> delimiter_cache; + + void _update_delimiter_cache(int p_from_line = 0, int p_to_line = -1); + int _is_in_delimiter(int p_line, int p_column, DelimiterType p_type) const; + + void _add_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only, DelimiterType p_type); + void _remove_delimiter(const String &p_start_key, DelimiterType p_type); + bool _has_delimiter(const String &p_start_key, DelimiterType p_type) const; + + void _set_delimiters(const TypedArray<String> &p_delimiters, DelimiterType p_type); + void _clear_delimiters(DelimiterType p_type); + TypedArray<String> _get_delimiters(DelimiterType p_type) const; + + /* Code Hint */ + String code_hint = ""; + + bool code_hint_draw_below = true; + int code_hint_xpos = -0xFFFF; + + /* Code Completion */ + bool code_completion_enabled = false; + bool code_completion_forced = false; + + int code_completion_max_width = 0; + int code_completion_max_lines = 7; + int code_completion_scroll_width = 0; + Color code_completion_scroll_color = Color(0, 0, 0, 0); + Color code_completion_background_color = Color(0, 0, 0, 0); + Color code_completion_selected_color = Color(0, 0, 0, 0); + Color code_completion_existing_color = Color(0, 0, 0, 0); + + bool code_completion_active = false; + Vector<ScriptCodeCompletionOption> code_completion_options; + int code_completion_line_ofs = 0; + int code_completion_current_selected = 0; + int code_completion_longest_line = 0; + Rect2i code_completion_rect; + + Set<String> code_completion_prefixes; + List<ScriptCodeCompletionOption> code_completion_option_submitted; + List<ScriptCodeCompletionOption> code_completion_option_sources; + String code_completion_base; + + void _filter_code_completion_candidates_impl(); + + /* Line length guidelines */ + TypedArray<int> line_length_guideline_columns; + Color line_length_guideline_color; + + /* Symbol lookup */ + bool symbol_lookup_on_click_enabled = false; + + String symbol_lookup_new_word = ""; + String symbol_lookup_word = ""; + + /* Visual */ + Ref<StyleBox> style_normal; + + Ref<Font> font; + int font_size = 16; + + int line_spacing = 1; + + /* Callbacks */ + int lines_edited_from = -1; + int lines_edited_to = -1; + + void _lines_edited_from(int p_from_line, int p_to_line); + void _text_set(); + void _text_changed(); + protected: void _notification(int p_what); static void _bind_methods(); + /* Text manipulation */ + + // Overridable actions + virtual void _handle_unicode_input_internal(const uint32_t p_unicode) override; + virtual void _backspace_internal() override; + + GDVIRTUAL1(_confirm_code_completion, bool) + GDVIRTUAL1(_request_code_completion, bool) + GDVIRTUAL1RC(Array, _filter_code_completion_candidates, TypedArray<Dictionary>) + public: + /* General overrides */ + virtual void gui_input(const Ref<InputEvent> &p_gui_input) override; + virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override; + + /* Indent management */ + void set_indent_size(const int p_size); + int get_indent_size() const; + + void set_indent_using_spaces(const bool p_use_spaces); + bool is_indent_using_spaces() const; + + void set_auto_indent_enabled(bool p_enabled); + bool is_auto_indent_enabled() const; + + void set_auto_indent_prefixes(const TypedArray<String> &p_prefixes); + TypedArray<String> get_auto_indent_prefixes() const; + + void do_indent(); + void do_unindent(); + + void indent_lines(); + void unindent_lines(); + + /* Auto brace completion */ + void set_auto_brace_completion_enabled(bool p_enabled); + bool is_auto_brace_completion_enabled() const; + + void set_highlight_matching_braces_enabled(bool p_enabled); + bool is_highlight_matching_braces_enabled() const; + + void add_auto_brace_completion_pair(const String &p_open_key, const String &p_close_key); + void set_auto_brace_completion_pairs(const Dictionary &p_auto_brace_completion_pairs); + Dictionary get_auto_brace_completion_pairs() const; + + bool has_auto_brace_completion_open_key(const String &p_open_key) const; + bool has_auto_brace_completion_close_key(const String &p_close_key) const; + + String get_auto_brace_completion_close_key(const String &p_open_key) const; + /* Main Gutter */ void set_draw_breakpoints_gutter(bool p_draw); bool is_drawing_breakpoints_gutter() const; @@ -128,8 +340,91 @@ public: void set_draw_fold_gutter(bool p_draw); bool is_drawing_fold_gutter() const; + /* Line Folding */ + void set_line_folding_enabled(bool p_enabled); + bool is_line_folding_enabled() const; + + bool can_fold_line(int p_line) const; + + void fold_line(int p_line); + void unfold_line(int p_line); + void fold_all_lines(); + void unfold_all_lines(); + void toggle_foldable_line(int p_line); + + bool is_line_folded(int p_line) const; + TypedArray<int> get_folded_lines() const; + + /* Delimiters */ + void add_string_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only = false); + void remove_string_delimiter(const String &p_start_key); + bool has_string_delimiter(const String &p_start_key) const; + + void set_string_delimiters(const TypedArray<String> &p_string_delimiters); + void clear_string_delimiters(); + TypedArray<String> get_string_delimiters() const; + + int is_in_string(int p_line, int p_column = -1) const; + + void add_comment_delimiter(const String &p_start_key, const String &p_end_key, bool p_line_only = false); + void remove_comment_delimiter(const String &p_start_key); + bool has_comment_delimiter(const String &p_start_key) const; + + void set_comment_delimiters(const TypedArray<String> &p_comment_delimiters); + void clear_comment_delimiters(); + TypedArray<String> get_comment_delimiters() const; + + int is_in_comment(int p_line, int p_column = -1) const; + + String get_delimiter_start_key(int p_delimiter_idx) const; + String get_delimiter_end_key(int p_delimiter_idx) const; + + Point2 get_delimiter_start_position(int p_line, int p_column) const; + Point2 get_delimiter_end_position(int p_line, int p_column) const; + + /* Code hint */ + void set_code_hint(const String &p_hint); + void set_code_hint_draw_below(bool p_below); + + /* Code Completion */ + void set_code_completion_enabled(bool p_enable); + bool is_code_completion_enabled() const; + + void set_code_completion_prefixes(const TypedArray<String> &p_prefixes); + TypedArray<String> get_code_completion_prefixes() const; + + String get_text_for_code_completion() const; + + void request_code_completion(bool p_force = false); + + void add_code_completion_option(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color = Color(1, 1, 1), const RES &p_icon = RES(), const Variant &p_value = Variant::NIL); + void update_code_completion_options(bool p_forced = false); + + TypedArray<Dictionary> get_code_completion_options() const; + Dictionary get_code_completion_option(int p_index) const; + + int get_code_completion_selected_index() const; + void set_code_completion_selected_index(int p_index); + + void confirm_code_completion(bool p_replace = false); + void cancel_code_completion(); + + /* Line length guidelines */ + void set_line_length_guidelines(TypedArray<int> p_guideline_columns); + TypedArray<int> get_line_length_guidelines() const; + + /* Symbol lookup */ + void set_symbol_lookup_on_click_enabled(bool p_enabled); + bool is_symbol_lookup_on_click_enabled() const; + + String get_text_for_symbol_lookup(); + + void set_symbol_lookup_word_as_valid(bool p_valid); + CodeEdit(); ~CodeEdit(); }; +VARIANT_ENUM_CAST(CodeEdit::CodeCompletionKind); + #endif // CODEEDIT_H diff --git a/scene/gui/color_picker.cpp b/scene/gui/color_picker.cpp index c0b4563615..1afb0b8e9d 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -40,40 +40,47 @@ #endif #include "scene/main/window.h" +List<Color> ColorPicker::preset_cache; + void ColorPicker::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { - btn_pick->set_icon(get_theme_icon("screen_picker", "ColorPicker")); - bt_add_preset->set_icon(get_theme_icon("add_preset")); - + btn_pick->set_icon(get_theme_icon(SNAME("screen_picker"), SNAME("ColorPicker"))); + btn_add_preset->set_icon(get_theme_icon(SNAME("add_preset"))); + _update_presets(); _update_controls(); } break; case NOTIFICATION_ENTER_TREE: { - btn_pick->set_icon(get_theme_icon("screen_picker", "ColorPicker")); - bt_add_preset->set_icon(get_theme_icon("add_preset")); + btn_pick->set_icon(get_theme_icon(SNAME("screen_picker"), SNAME("ColorPicker"))); + btn_add_preset->set_icon(get_theme_icon(SNAME("add_preset"))); _update_controls(); _update_color(); #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint()) { - PackedColorArray saved_presets = EditorSettings::get_singleton()->get_project_metadata("color_picker", "presets", PackedColorArray()); + if (preset_cache.is_empty()) { + PackedColorArray saved_presets = EditorSettings::get_singleton()->get_project_metadata("color_picker", "presets", PackedColorArray()); + for (int i = 0; i < saved_presets.size(); i++) { + preset_cache.push_back(saved_presets[i]); + } + } - for (int i = 0; i < saved_presets.size(); i++) { - add_preset(saved_presets[i]); + for (int i = 0; i < preset_cache.size(); i++) { + presets.push_back(preset_cache[i]); } } #endif } break; case NOTIFICATION_PARENTED: { for (int i = 0; i < 4; i++) { - set_offset((Side)i, get_offset((Side)i) + get_theme_constant("margin")); + set_offset((Side)i, get_offset((Side)i) + get_theme_constant(SNAME("margin"))); } } break; case NOTIFICATION_VISIBILITY_CHANGED: { Popup *p = Object::cast_to<Popup>(get_parent()); if (p) { - p->set_size(Size2(get_combined_minimum_size().width + get_theme_constant("margin") * 2, get_combined_minimum_size().height + get_theme_constant("margin") * 2)); + p->set_size(Size2(get_combined_minimum_size().width + get_theme_constant(SNAME("margin")) * 2, get_combined_minimum_size().height + get_theme_constant(SNAME("margin")) * 2)); } } break; case NOTIFICATION_WM_CLOSE_REQUEST: { @@ -88,44 +95,54 @@ Ref<Shader> ColorPicker::wheel_shader; Ref<Shader> ColorPicker::circle_shader; void ColorPicker::init_shaders() { - wheel_shader.instance(); - wheel_shader->set_code( - "shader_type canvas_item;" - "void fragment() {" - " float x = UV.x - 0.5;" - " float y = UV.y - 0.5;" - " float a = atan(y, x);" - " x += 0.001;" - " y += 0.001;" - " float b = float(sqrt(x * x + y * y) < 0.5) * float(sqrt(x * x + y * y) > 0.42);" - " x -= 0.002;" - " float b2 = float(sqrt(x * x + y * y) < 0.5) * float(sqrt(x * x + y * y) > 0.42);" - " y -= 0.002;" - " float b3 = float(sqrt(x * x + y * y) < 0.5) * float(sqrt(x * x + y * y) > 0.42);" - " x += 0.002;" - " float b4 = float(sqrt(x * x + y * y) < 0.5) * float(sqrt(x * x + y * y) > 0.42);" - " COLOR = vec4(clamp((abs(fract(((a - TAU) / TAU) + vec3(3.0, 2.0, 1.0) / 3.0) * 6.0 - 3.0) - 1.0), 0.0, 1.0), (b + b2 + b3 + b4) / 4.00);" - "}"); - - circle_shader.instance(); - circle_shader->set_code( - "shader_type canvas_item;" - "uniform float v = 1.0;" - "void fragment() {" - " float x = UV.x - 0.5;" - " float y = UV.y - 0.5;" - " float a = atan(y, x);" - " x += 0.001;" - " y += 0.001;" - " float b = float(sqrt(x * x + y * y) < 0.5);" - " x -= 0.002;" - " float b2 = float(sqrt(x * x + y * y) < 0.5);" - " y -= 0.002;" - " float b3 = float(sqrt(x * x + y * y) < 0.5);" - " x += 0.002;" - " float b4 = float(sqrt(x * x + y * y) < 0.5);" - " COLOR = vec4(mix(vec3(1.0), clamp(abs(fract(vec3((a - TAU) / TAU) + vec3(1.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - vec3(3.0)) - vec3(1.0), 0.0, 1.0), ((float(sqrt(x * x + y * y)) * 2.0)) / 1.0) * vec3(v), (b + b2 + b3 + b4) / 4.00);" - "}"); + wheel_shader.instantiate(); + wheel_shader->set_code(R"( +// ColorPicker wheel shader. + +shader_type canvas_item; + +void fragment() { + float x = UV.x - 0.5; + float y = UV.y - 0.5; + float a = atan(y, x); + x += 0.001; + y += 0.001; + float b = float(sqrt(x * x + y * y) < 0.5) * float(sqrt(x * x + y * y) > 0.42); + x -= 0.002; + float b2 = float(sqrt(x * x + y * y) < 0.5) * float(sqrt(x * x + y * y) > 0.42); + y -= 0.002; + float b3 = float(sqrt(x * x + y * y) < 0.5) * float(sqrt(x * x + y * y) > 0.42); + x += 0.002; + float b4 = float(sqrt(x * x + y * y) < 0.5) * float(sqrt(x * x + y * y) > 0.42); + + COLOR = vec4(clamp((abs(fract(((a - TAU) / TAU) + vec3(3.0, 2.0, 1.0) / 3.0) * 6.0 - 3.0) - 1.0), 0.0, 1.0), (b + b2 + b3 + b4) / 4.00); +} +)"); + + circle_shader.instantiate(); + circle_shader->set_code(R"( +// ColorPicker circle shader. + +shader_type canvas_item; + +uniform float v = 1.0; + +void fragment() { + float x = UV.x - 0.5; + float y = UV.y - 0.5; + float a = atan(y, x); + x += 0.001; + y += 0.001; + float b = float(sqrt(x * x + y * y) < 0.5); + x -= 0.002; + float b2 = float(sqrt(x * x + y * y) < 0.5); + y -= 0.002; + float b3 = float(sqrt(x * x + y * y) < 0.5); + x += 0.002; + float b4 = float(sqrt(x * x + y * y) < 0.5); + + COLOR = vec4(mix(vec3(1.0), clamp(abs(fract(vec3((a - TAU) / TAU) + vec3(1.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - vec3(3.0)) - vec3(1.0), 0.0, 1.0), ((float(sqrt(x * x + y * y)) * 2.0)) / 1.0) * vec3(v), (b + b2 + b3 + b4) / 4.00); +})"); } void ColorPicker::finish_shaders() { @@ -134,7 +151,7 @@ void ColorPicker::finish_shaders() { } void ColorPicker::set_focus_on_line_edit() { - c_text->call_deferred("grab_focus"); + c_text->call_deferred(SNAME("grab_focus")); } void ColorPicker::_update_controls() { @@ -172,7 +189,7 @@ void ColorPicker::_update_controls() { } } else { Ref<StyleBoxEmpty> style_box_empty(memnew(StyleBoxEmpty)); - Ref<Texture2D> bar_arrow = get_theme_icon("bar_arrow"); + Ref<Texture2D> bar_arrow = get_theme_icon(SNAME("bar_arrow")); for (int i = 0; i < 4; i++) { scroll[i]->add_theme_icon_override("grabber", bar_arrow); @@ -286,10 +303,10 @@ void ColorPicker::_value_changed(double) { } _set_pick_color(color, false); - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } -void ColorPicker::_html_entered(const String &p_html) { +void ColorPicker::_html_submitted(const String &p_html) { if (updating || text_is_constructor || !c_text->is_visible()) { return; } @@ -305,7 +322,7 @@ void ColorPicker::_html_entered(const String &p_html) { } set_pick_color(color); - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } void ColorPicker::_update_color(bool p_update_sliders) { @@ -358,29 +375,30 @@ void ColorPicker::_update_color(bool p_update_sliders) { } void ColorPicker::_update_presets() { - return; - //presets should be shown using buttons or something else, this method is not a good idea - - presets_per_row = 10; - Size2 size = bt_add_preset->get_size(); - Size2 preset_size = Size2(MIN(size.width * presets.size(), presets_per_row * size.width), size.height * (Math::ceil((float)presets.size() / presets_per_row))); - preset->set_custom_minimum_size(preset_size); - preset_container->set_custom_minimum_size(preset_size); - preset->draw_rect(Rect2(Point2(), preset_size), Color(1, 1, 1, 0)); - - for (int i = 0; i < presets.size(); i++) { - int x = (i % presets_per_row) * size.width; - int y = (Math::floor((float)i / presets_per_row)) * size.height; - preset->draw_rect(Rect2(Point2(x, y), size), presets[i]); + int preset_size = _get_preset_size(); + // Only update the preset button size if it has changed. + if (preset_size != prev_preset_size) { + prev_preset_size = preset_size; + btn_add_preset->set_custom_minimum_size(Size2(preset_size, preset_size)); + for (int i = 1; i < preset_container->get_child_count(); i++) { + ColorPresetButton *cpb = Object::cast_to<ColorPresetButton>(preset_container->get_child(i)); + cpb->set_custom_minimum_size(Size2(preset_size, preset_size)); + } + } + // Only load preset buttons when the only child is the add-preset button. + if (preset_container->get_child_count() == 1) { + for (int i = 0; i < preset_cache.size(); i++) { + _add_preset_button(preset_size, preset_cache[i]); + } + _notification(NOTIFICATION_VISIBILITY_CHANGED); } - _notification(NOTIFICATION_VISIBILITY_CHANGED); } void ColorPicker::_text_type_toggled() { text_is_constructor = !text_is_constructor; if (text_is_constructor) { text_type->set_text(""); - text_type->set_icon(get_theme_icon("Script", "EditorIcons")); + text_type->set_icon(get_theme_icon(SNAME("Script"), SNAME("EditorIcons"))); c_text->set_editable(false); } else { @@ -408,13 +426,37 @@ ColorPicker::PickerShapeType ColorPicker::get_picker_shape() const { return picker_type; } +inline int ColorPicker::_get_preset_size() { + return (int(get_minimum_size().width) - (preset_container->get_theme_constant(SNAME("hseparation")) * (preset_column_count - 1))) / preset_column_count; +} + +void ColorPicker::_add_preset_button(int p_size, const Color &p_color) { + ColorPresetButton *btn_preset = memnew(ColorPresetButton(p_color)); + btn_preset->set_preset_color(p_color); + btn_preset->set_custom_minimum_size(Size2(p_size, p_size)); + btn_preset->connect("gui_input", callable_mp(this, &ColorPicker::_preset_input), varray(p_color)); + btn_preset->set_tooltip(vformat(RTR("Color: #%s\nLMB: Apply color\nRMB: Remove preset"), p_color.to_html(p_color.a < 1))); + preset_container->add_child(btn_preset); +} + void ColorPicker::add_preset(const Color &p_color) { if (presets.find(p_color)) { presets.move_to_back(presets.find(p_color)); + + // Find button to move to the end. + for (int i = 1; i < preset_container->get_child_count(); i++) { + ColorPresetButton *current_btn = Object::cast_to<ColorPresetButton>(preset_container->get_child(i)); + if (current_btn && p_color == current_btn->get_preset_color()) { + preset_container->move_child(current_btn, preset_container->get_child_count() - 1); + break; + } + } } else { presets.push_back(p_color); + preset_cache.push_back(p_color); + + _add_preset_button(_get_preset_size(), p_color); } - preset->update(); #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint()) { @@ -427,7 +469,16 @@ void ColorPicker::add_preset(const Color &p_color) { void ColorPicker::erase_preset(const Color &p_color) { if (presets.find(p_color)) { presets.erase(presets.find(p_color)); - preset->update(); + preset_cache.erase(preset_cache.find(p_color)); + + // Find preset button to remove. + for (int i = 1; i < preset_container->get_child_count(); i++) { + ColorPresetButton *current_btn = Object::cast_to<ColorPresetButton>(preset_container->get_child(i)); + if (current_btn && p_color == current_btn->get_preset_color()) { + current_btn->queue_delete(); + break; + } + } #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint()) { @@ -527,7 +578,7 @@ void ColorPicker::_sample_input(const Ref<InputEvent> &p_event) { // Revert to the old color when left-clicking the old color sample. color = old_color; _update_color(); - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } } } @@ -544,28 +595,28 @@ void ColorPicker::_sample_draw() { const Rect2 rect_old = Rect2(Point2(), Size2(sample->get_size().width * 0.5, sample->get_size().height * 0.95)); if (display_old_color && old_color.a < 1.0) { - sample->draw_texture_rect(get_theme_icon("preset_bg", "ColorPicker"), rect_old, true); + sample->draw_texture_rect(get_theme_icon(SNAME("sample_bg"), SNAME("ColorPicker")), rect_old, true); } sample->draw_rect(rect_old, old_color); if (old_color.r > 1 || old_color.g > 1 || old_color.b > 1) { // Draw an indicator to denote that the old color is "overbright" and can't be displayed accurately in the preview. - sample->draw_texture(get_theme_icon("overbright_indicator", "ColorPicker"), Point2()); + sample->draw_texture(get_theme_icon(SNAME("overbright_indicator"), SNAME("ColorPicker")), Point2()); } } else { rect_new = Rect2(Point2(), Size2(sample->get_size().width, sample->get_size().height * 0.95)); } if (color.a < 1.0) { - sample->draw_texture_rect(get_theme_icon("preset_bg", "ColorPicker"), rect_new, true); + sample->draw_texture_rect(get_theme_icon(SNAME("sample_bg"), SNAME("ColorPicker")), rect_new, true); } sample->draw_rect(rect_new, color); if (color.r > 1 || color.g > 1 || color.b > 1) { // Draw an indicator to denote that the new color is "overbright" and can't be displayed accurately in the preview. - sample->draw_texture(get_theme_icon("overbright_indicator", "ColorPicker"), Point2(uv_edit->get_size().width * 0.5, 0)); + sample->draw_texture(get_theme_icon(SNAME("overbright_indicator"), SNAME("ColorPicker")), Point2(uv_edit->get_size().width * 0.5, 0)); } } @@ -639,7 +690,7 @@ void ColorPicker::_hsv_draw(int p_which, Control *c) { default: { } } - Ref<Texture2D> cursor = get_theme_icon("picker_cursor", "ColorPicker"); + Ref<Texture2D> cursor = get_theme_icon(SNAME("picker_cursor"), SNAME("ColorPicker")); int x; int y; if (picker_type == SHAPE_VHS_CIRCLE) { @@ -669,7 +720,7 @@ void ColorPicker::_hsv_draw(int p_which, Control *c) { } else if (p_which == 1) { if (picker_type == SHAPE_HSV_RECTANGLE) { - Ref<Texture2D> hue = get_theme_icon("color_hue", "ColorPicker"); + Ref<Texture2D> hue = get_theme_icon(SNAME("color_hue"), SNAME("ColorPicker")); c->draw_texture_rect(hue, Rect2(Point2(), c->get_size())); int y = c->get_size().y - c->get_size().y * (1.0 - h); Color col; @@ -718,7 +769,7 @@ void ColorPicker::_slider_draw(int p_which) { #endif if (p_which == 3) { - scroll[p_which]->draw_texture_rect(get_theme_icon("preset_bg", "ColorPicker"), Rect2(Point2(0, margin), Size2(size.x, margin)), true); + scroll[p_which]->draw_texture_rect(get_theme_icon(SNAME("sample_bg"), SNAME("ColorPicker")), Rect2(Point2(0, margin), Size2(size.x, margin)), true); left_color = color; left_color.a = 0; @@ -730,7 +781,7 @@ void ColorPicker::_slider_draw(int p_which) { } if (hsv_mode_enabled) { if (p_which == 0) { - Ref<Texture2D> hue = get_theme_icon("color_hue", "ColorPicker"); + Ref<Texture2D> hue = get_theme_icon(SNAME("color_hue"), SNAME("ColorPicker")); scroll[p_which]->draw_set_transform(Point2(), -Math_PI / 2, Size2(1.0, 1.0)); scroll[p_which]->draw_texture_rect(hue, Rect2(Vector2(margin * -2, 0), Vector2(scroll[p_which]->get_size().x, margin)), false, Color(1, 1, 1), true); return; @@ -816,10 +867,10 @@ void ColorPicker::_uv_input(const Ref<InputEvent> &p_event, Control *c) { set_pick_color(color); _update_color(); if (!deferred_mode_enabled) { - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } } else if (deferred_mode_enabled && !bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); changing_color = false; spinning = false; } else { @@ -863,7 +914,7 @@ void ColorPicker::_uv_input(const Ref<InputEvent> &p_event, Control *c) { set_pick_color(color); _update_color(); if (!deferred_mode_enabled) { - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } } } @@ -888,9 +939,9 @@ void ColorPicker::_w_input(const Ref<InputEvent> &p_event) { set_pick_color(color); _update_color(); if (!deferred_mode_enabled) { - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } else if (!bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } } @@ -911,47 +962,23 @@ void ColorPicker::_w_input(const Ref<InputEvent> &p_event) { set_pick_color(color); _update_color(); if (!deferred_mode_enabled) { - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } } } -void ColorPicker::_preset_input(const Ref<InputEvent> &p_event) { +void ColorPicker::_preset_input(const Ref<InputEvent> &p_event, const Color &p_color) { Ref<InputEventMouseButton> bev = p_event; if (bev.is_valid()) { - int index = 0; if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { - for (int i = 0; i < presets.size(); i++) { - int x = (i % presets_per_row) * bt_add_preset->get_size().x; - int y = (Math::floor((float)i / presets_per_row)) * bt_add_preset->get_size().y; - if (bev->get_position().x > x && bev->get_position().x < x + preset->get_size().x && bev->get_position().y > y && bev->get_position().y < y + preset->get_size().y) { - index = i; - } - } - set_pick_color(presets[index]); + set_pick_color(p_color); _update_color(); - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), p_color); } else if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_RIGHT && presets_enabled) { - index = bev->get_position().x / (preset->get_size().x / presets.size()); - Color clicked_preset = presets[index]; - erase_preset(clicked_preset); - emit_signal("preset_removed", clicked_preset); - bt_add_preset->show(); - } - } - - Ref<InputEventMouseMotion> mev = p_event; - - if (mev.is_valid()) { - int index = mev->get_position().x * presets.size(); - if (preset->get_size().x != 0) { - index /= preset->get_size().x; - } - if (index < 0 || index >= presets.size()) { - return; + erase_preset(p_color); + emit_signal(SNAME("preset_removed"), p_color); } - preset->set_tooltip(vformat(RTR("Color: #%s\nLMB: Set color\nRMB: Remove preset"), presets[index].to_html(presets[index].a < 1))); } } @@ -962,7 +989,7 @@ void ColorPicker::_screen_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> bev = p_event; if (bev.is_valid() && bev->get_button_index() == MOUSE_BUTTON_LEFT && !bev->is_pressed()) { - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); screen->hide(); } @@ -985,7 +1012,7 @@ void ColorPicker::_screen_input(const Ref<InputEvent> &p_event) { void ColorPicker::_add_preset_pressed() { add_preset(color); - emit_signal("preset_added", color); + emit_signal(SNAME("preset_added"), color); } void ColorPicker::_screen_pick_pressed() { @@ -1002,7 +1029,7 @@ void ColorPicker::_screen_pick_pressed() { screen->set_default_cursor_shape(CURSOR_POINTING_HAND); screen->connect("gui_input", callable_mp(this, &ColorPicker::_screen_input)); // It immediately toggles off in the first press otherwise. - screen->call_deferred("connect", "hidden", Callable(btn_pick, "set_pressed"), varray(false)); + screen->call_deferred(SNAME("connect"), "hidden", Callable(btn_pick, "set_pressed"), varray(false)); } screen->raise(); #ifndef _MSC_VER @@ -1038,21 +1065,21 @@ void ColorPicker::_focus_exit() { } void ColorPicker::_html_focus_exit() { - if (c_text->get_menu()->is_visible()) { + if (c_text->is_menu_visible()) { return; } - _html_entered(c_text->get_text()); + _html_submitted(c_text->get_text()); _focus_exit(); } void ColorPicker::set_presets_enabled(bool p_enabled) { presets_enabled = p_enabled; if (!p_enabled) { - bt_add_preset->set_disabled(true); - bt_add_preset->set_focus_mode(FOCUS_NONE); + btn_add_preset->set_disabled(true); + btn_add_preset->set_focus_mode(FOCUS_NONE); } else { - bt_add_preset->set_disabled(false); - bt_add_preset->set_focus_mode(FOCUS_ALL); + btn_add_preset->set_disabled(false); + btn_add_preset->set_focus_mode(FOCUS_ALL); } } @@ -1064,7 +1091,6 @@ void ColorPicker::set_presets_visible(bool p_visible) { presets_visible = p_visible; preset_separator->set_visible(p_visible); preset_container->set_visible(p_visible); - preset_container2->set_visible(p_visible); } bool ColorPicker::are_presets_visible() const { @@ -1113,7 +1139,7 @@ void ColorPicker::_bind_methods() { ColorPicker::ColorPicker() : BoxContainer(true) { HBoxContainer *hb_edit = memnew(HBoxContainer); - add_child(hb_edit); + add_child(hb_edit, false, INTERNAL_MODE_FRONT); hb_edit->set_v_size_flags(SIZE_EXPAND_FILL); hb_edit->add_child(uv_edit); @@ -1121,11 +1147,11 @@ ColorPicker::ColorPicker() : uv_edit->set_mouse_filter(MOUSE_FILTER_PASS); uv_edit->set_h_size_flags(SIZE_EXPAND_FILL); uv_edit->set_v_size_flags(SIZE_EXPAND_FILL); - uv_edit->set_custom_minimum_size(Size2(get_theme_constant("sv_width"), get_theme_constant("sv_height"))); + uv_edit->set_custom_minimum_size(Size2(get_theme_constant(SNAME("sv_width")), get_theme_constant(SNAME("sv_height")))); uv_edit->connect("draw", callable_mp(this, &ColorPicker::_hsv_draw), make_binds(0, uv_edit)); HBoxContainer *hb_smpl = memnew(HBoxContainer); - add_child(hb_smpl); + add_child(hb_smpl, false, INTERNAL_MODE_FRONT); hb_smpl->add_child(sample); sample->set_h_size_flags(SIZE_EXPAND_FILL); @@ -1139,19 +1165,19 @@ ColorPicker::ColorPicker() : btn_pick->connect("pressed", callable_mp(this, &ColorPicker::_screen_pick_pressed)); VBoxContainer *vbl = memnew(VBoxContainer); - add_child(vbl); + add_child(vbl, false, INTERNAL_MODE_FRONT); - add_child(memnew(HSeparator)); + add_child(memnew(HSeparator), false, INTERNAL_MODE_FRONT); VBoxContainer *vbr = memnew(VBoxContainer); - add_child(vbr); + add_child(vbr, false, INTERNAL_MODE_FRONT); vbr->set_h_size_flags(SIZE_EXPAND_FILL); for (int i = 0; i < 4; i++) { HBoxContainer *hbc = memnew(HBoxContainer); labels[i] = memnew(Label()); - labels[i]->set_custom_minimum_size(Size2(get_theme_constant("label_width"), 0)); + labels[i]->set_custom_minimum_size(Size2(get_theme_constant(SNAME("label_width")), 0)); labels[i]->set_v_size_flags(SIZE_SHRINK_CENTER); hbc->add_child(labels[i]); @@ -1204,18 +1230,18 @@ ColorPicker::ColorPicker() : hhb->add_child(c_text); c_text->set_h_size_flags(SIZE_EXPAND_FILL); - c_text->connect("text_entered", callable_mp(this, &ColorPicker::_html_entered)); + c_text->connect("text_submitted", callable_mp(this, &ColorPicker::_html_submitted)); c_text->connect("focus_entered", callable_mp(this, &ColorPicker::_focus_enter)); c_text->connect("focus_exited", callable_mp(this, &ColorPicker::_html_focus_exit)); wheel_edit->set_h_size_flags(SIZE_EXPAND_FILL); wheel_edit->set_v_size_flags(SIZE_EXPAND_FILL); - wheel_edit->set_custom_minimum_size(Size2(get_theme_constant("sv_width"), get_theme_constant("sv_height"))); + wheel_edit->set_custom_minimum_size(Size2(get_theme_constant(SNAME("sv_width")), get_theme_constant(SNAME("sv_height")))); hb_edit->add_child(wheel_edit); - wheel_mat.instance(); + wheel_mat.instantiate(); wheel_mat->set_shader(wheel_shader); - circle_mat.instance(); + circle_mat.instantiate(); circle_mat->set_shader(circle_shader); MarginContainer *wheel_margin(memnew(MarginContainer)); @@ -1235,7 +1261,7 @@ ColorPicker::ColorPicker() : wheel_uv->connect("draw", callable_mp(this, &ColorPicker::_hsv_draw), make_binds(0, wheel_uv)); hb_edit->add_child(w_edit); - w_edit->set_custom_minimum_size(Size2(get_theme_constant("h_width"), 0)); + w_edit->set_custom_minimum_size(Size2(get_theme_constant(SNAME("h_width")), 0)); w_edit->set_h_size_flags(SIZE_FILL); w_edit->set_v_size_flags(SIZE_EXPAND_FILL); w_edit->connect("gui_input", callable_mp(this, &ColorPicker::_w_input)); @@ -1247,20 +1273,16 @@ ColorPicker::ColorPicker() : set_pick_color(Color(1, 1, 1)); - add_child(preset_separator); + add_child(preset_separator, false, INTERNAL_MODE_FRONT); preset_container->set_h_size_flags(SIZE_EXPAND_FILL); - add_child(preset_container); - - preset_container->add_child(preset); - preset->connect("gui_input", callable_mp(this, &ColorPicker::_preset_input)); - preset->connect("draw", callable_mp(this, &ColorPicker::_update_presets)); + preset_container->set_columns(preset_column_count); + add_child(preset_container, false, INTERNAL_MODE_FRONT); - preset_container2->set_h_size_flags(SIZE_EXPAND_FILL); - add_child(preset_container2); - preset_container2->add_child(bt_add_preset); - bt_add_preset->set_tooltip(RTR("Add current color as a preset.")); - bt_add_preset->connect("pressed", callable_mp(this, &ColorPicker::_add_preset_pressed)); + btn_add_preset->set_icon_align(Button::ALIGN_CENTER); + btn_add_preset->set_tooltip(RTR("Add current color as a preset.")); + btn_add_preset->connect("pressed", callable_mp(this, &ColorPicker::_add_preset_pressed)); + preset_container->add_child(btn_add_preset); } ///////////////// @@ -1275,11 +1297,11 @@ void ColorPickerButton::_about_to_popup() { void ColorPickerButton::_color_changed(const Color &p_color) { color = p_color; update(); - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } void ColorPickerButton::_modal_closed() { - emit_signal("popup_closed"); + emit_signal(SNAME("popup_closed")); set_pressed(false); } @@ -1287,6 +1309,7 @@ void ColorPickerButton::pressed() { _update_picker(); popup->set_as_minsize(); + picker->_update_presets(); Rect2i usable_rect = popup->get_usable_parent_rect(); //let's try different positions to see which one we can use @@ -1317,14 +1340,14 @@ void ColorPickerButton::pressed() { void ColorPickerButton::_notification(int p_what) { switch (p_what) { case NOTIFICATION_DRAW: { - const Ref<StyleBox> normal = get_theme_stylebox("normal"); + const Ref<StyleBox> normal = get_theme_stylebox(SNAME("normal")); const Rect2 r = Rect2(normal->get_offset(), get_size() - normal->get_minimum_size()); - draw_texture_rect(Control::get_theme_icon("bg", "ColorPickerButton"), r, true); + draw_texture_rect(Control::get_theme_icon(SNAME("bg"), SNAME("ColorPickerButton")), r, true); draw_rect(r, color); if (color.r > 1 || color.g > 1 || color.b > 1) { // Draw an indicator to denote that the color is "overbright" and can't be displayed accurately in the preview - draw_texture(Control::get_theme_icon("overbright_indicator", "ColorPicker"), normal->get_offset()); + draw_texture(Control::get_theme_icon(SNAME("overbright_indicator"), SNAME("ColorPicker")), normal->get_offset()); } } break; case NOTIFICATION_WM_CLOSE_REQUEST: { @@ -1382,14 +1405,14 @@ void ColorPickerButton::_update_picker() { picker = memnew(ColorPicker); picker->set_anchors_and_offsets_preset(PRESET_WIDE); popup->add_child(picker); - add_child(popup); + add_child(popup, false, INTERNAL_MODE_FRONT); picker->connect("color_changed", callable_mp(this, &ColorPickerButton::_color_changed)); popup->connect("about_to_popup", callable_mp(this, &ColorPickerButton::_about_to_popup)); popup->connect("popup_hide", callable_mp(this, &ColorPickerButton::_modal_closed)); picker->set_pick_color(color); picker->set_edit_alpha(edit_alpha); picker->set_display_old_color(true); - emit_signal("picker_created"); + emit_signal(SNAME("picker_created")); } } @@ -1412,3 +1435,64 @@ void ColorPickerButton::_bind_methods() { ColorPickerButton::ColorPickerButton() { set_toggle_mode(true); } + +///////////////// + +void ColorPresetButton::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_DRAW: { + const Rect2 r = Rect2(Point2(0, 0), get_size()); + Ref<StyleBox> sb_raw = get_theme_stylebox(SNAME("preset_fg"), SNAME("ColorPresetButton"))->duplicate(); + Ref<StyleBoxFlat> sb_flat = sb_raw; + Ref<StyleBoxTexture> sb_texture = sb_raw; + + if (sb_flat.is_valid()) { + if (preset_color.a < 1) { + // Draw a background pattern when the color is transparent. + sb_flat->set_bg_color(Color(1, 1, 1)); + sb_flat->draw(get_canvas_item(), r); + + Rect2 bg_texture_rect = r.grow_side(SIDE_LEFT, -sb_flat->get_margin(SIDE_LEFT)); + bg_texture_rect = bg_texture_rect.grow_side(SIDE_RIGHT, -sb_flat->get_margin(SIDE_RIGHT)); + bg_texture_rect = bg_texture_rect.grow_side(SIDE_TOP, -sb_flat->get_margin(SIDE_TOP)); + bg_texture_rect = bg_texture_rect.grow_side(SIDE_BOTTOM, -sb_flat->get_margin(SIDE_BOTTOM)); + + draw_texture_rect(get_theme_icon(SNAME("preset_bg"), SNAME("ColorPresetButton")), bg_texture_rect, true); + sb_flat->set_bg_color(preset_color); + } + sb_flat->set_bg_color(preset_color); + sb_flat->draw(get_canvas_item(), r); + } else if (sb_texture.is_valid()) { + if (preset_color.a < 1) { + // Draw a background pattern when the color is transparent. + bool use_tile_texture = (sb_texture->get_h_axis_stretch_mode() == StyleBoxTexture::AxisStretchMode::AXIS_STRETCH_MODE_TILE) || (sb_texture->get_h_axis_stretch_mode() == StyleBoxTexture::AxisStretchMode::AXIS_STRETCH_MODE_TILE_FIT); + draw_texture_rect(get_theme_icon(SNAME("preset_bg"), SNAME("ColorPresetButton")), r, use_tile_texture); + } + sb_texture->set_modulate(preset_color); + sb_texture->draw(get_canvas_item(), r); + } else { + WARN_PRINT("Unsupported StyleBox used for ColorPresetButton. Use StyleBoxFlat or StyleBoxTexture instead."); + } + if (preset_color.r > 1 || preset_color.g > 1 || preset_color.b > 1) { + // Draw an indicator to denote that the color is "overbright" and can't be displayed accurately in the preview + draw_texture(Control::get_theme_icon(SNAME("overbright_indicator"), SNAME("ColorPresetButton")), Vector2(0, 0)); + } + + } break; + } +} + +void ColorPresetButton::set_preset_color(const Color &p_color) { + preset_color = p_color; +} + +Color ColorPresetButton::get_preset_color() const { + return preset_color; +} + +ColorPresetButton::ColorPresetButton(Color p_color) { + preset_color = p_color; +} + +ColorPresetButton::~ColorPresetButton() { +} diff --git a/scene/gui/color_picker.h b/scene/gui/color_picker.h index 14113467d0..67ca007eb5 100644 --- a/scene/gui/color_picker.h +++ b/scene/gui/color_picker.h @@ -35,6 +35,7 @@ #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/check_button.h" +#include "scene/gui/grid_container.h" #include "scene/gui/label.h" #include "scene/gui/line_edit.h" #include "scene/gui/popup.h" @@ -43,6 +44,22 @@ #include "scene/gui/spin_box.h" #include "scene/gui/texture_rect.h" +class ColorPresetButton : public BaseButton { + GDCLASS(ColorPresetButton, BaseButton); + + Color preset_color; + +protected: + void _notification(int); + +public: + void set_preset_color(const Color &p_color); + Color get_preset_color() const; + + ColorPresetButton(Color p_color); + ~ColorPresetButton(); +}; + class ColorPicker : public BoxContainer { GDCLASS(ColorPicker, BoxContainer); @@ -58,6 +75,7 @@ public: private: static Ref<Shader> wheel_shader; static Ref<Shader> circle_shader; + static List<Color> preset_cache; Control *screen = nullptr; Control *uv_edit = memnew(Control); @@ -68,12 +86,9 @@ private: Control *wheel = memnew(Control); Control *wheel_uv = memnew(Control); TextureRect *sample = memnew(TextureRect); - TextureRect *preset = memnew(TextureRect); - HBoxContainer *preset_container = memnew(HBoxContainer); - HBoxContainer *preset_container2 = memnew(HBoxContainer); + GridContainer *preset_container = memnew(GridContainer); HSeparator *preset_separator = memnew(HSeparator); - Button *bt_add_preset = memnew(Button); - List<Color> presets; + Button *btn_add_preset = memnew(Button); Button *btn_pick = memnew(Button); CheckButton *btn_hsv = memnew(CheckButton); CheckButton *btn_raw = memnew(CheckButton); @@ -82,14 +97,19 @@ private: Label *labels[4]; Button *text_type = memnew(Button); LineEdit *c_text = memnew(LineEdit); + bool edit_alpha = true; Size2i ms; bool text_is_constructor = false; - int presets_per_row = 0; PickerShapeType picker_type = SHAPE_HSV_WHEEL; + const int preset_column_count = 9; + int prev_preset_size = 0; + List<Color> presets; + Color color; Color old_color; + bool display_old_color = false; bool raw_mode_enabled = false; bool hsv_mode_enabled = false; @@ -99,16 +119,16 @@ private: bool spinning = false; bool presets_enabled = true; bool presets_visible = true; + float h = 0.0; float s = 0.0; float v = 0.0; Color last_hsv; - void _html_entered(const String &p_html); + void _html_submitted(const String &p_html); void _value_changed(double); void _update_controls(); void _update_color(bool p_update_sliders = true); - void _update_presets(); void _update_text_value(); void _text_type_toggled(); void _sample_input(const Ref<InputEvent> &p_event); @@ -118,7 +138,7 @@ private: void _uv_input(const Ref<InputEvent> &p_event, Control *c); void _w_input(const Ref<InputEvent> &p_event); - void _preset_input(const Ref<InputEvent> &p_event); + void _preset_input(const Ref<InputEvent> &p_event, const Color &p_color); void _screen_input(const Ref<InputEvent> &p_event); void _add_preset_pressed(); void _screen_pick_pressed(); @@ -126,6 +146,9 @@ private: void _focus_exit(); void _html_focus_exit(); + inline int _get_preset_size(); + void _add_preset_button(int p_size, const Color &p_color); + protected: void _notification(int); static void _bind_methods(); @@ -151,6 +174,7 @@ public: void add_preset(const Color &p_color); void erase_preset(const Color &p_color); PackedColorArray get_presets() const; + void _update_presets(); void set_hsv_mode(bool p_enabled); bool is_hsv_mode() const; diff --git a/scene/gui/container.cpp b/scene/gui/container.cpp index dea69aae6b..c97434f69b 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -47,9 +47,9 @@ void Container::add_child_notify(Node *p_child) { return; } - control->connect("size_flags_changed", callable_mp(this, &Container::queue_sort)); - control->connect("minimum_size_changed", callable_mp(this, &Container::_child_minsize_changed)); - control->connect("visibility_changed", callable_mp(this, &Container::_child_minsize_changed)); + control->connect(SNAME("size_flags_changed"), callable_mp(this, &Container::queue_sort)); + control->connect(SNAME("minimum_size_changed"), callable_mp(this, &Container::_child_minsize_changed)); + control->connect(SNAME("visibility_changed"), callable_mp(this, &Container::_child_minsize_changed)); minimum_size_changed(); queue_sort(); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 0ae1dec46d..81411d5844 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -30,6 +30,7 @@ #include "control.h" +#include "container.h" #include "core/config/project_settings.h" #include "core/math/geometry_2d.h" #include "core/object/message_queue.h" @@ -168,6 +169,20 @@ Size2 Control::_edit_get_minimum_size() const { } #endif +String Control::properties_managed_by_container[] = { + "offset_left", + "offset_top", + "offset_right", + "offset_bottom", + "anchor_left", + "anchor_top", + "anchor_right", + "anchor_bottom", + "rect_position", + "rect_scale", + "rect_size" +}; + void Control::accept_event() { if (is_inside_tree()) { get_viewport()->_gui_accept_event(); @@ -222,41 +237,42 @@ Transform2D Control::_get_internal_transform() const { bool Control::_set(const StringName &p_name, const Variant &p_value) { String name = p_name; - if (!name.begins_with("custom")) { + // Prefixes "custom_*" are supported for compatibility with 3.x. + if (!name.begins_with("theme_override") && !name.begins_with("custom")) { return false; } if (p_value.get_type() == Variant::NIL) { - if (name.begins_with("custom_icons/")) { + if (name.begins_with("theme_override_icons/") || name.begins_with("custom_icons/")) { String dname = name.get_slicec('/', 1); if (data.icon_override.has(dname)) { data.icon_override[dname]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } data.icon_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); - } else if (name.begins_with("custom_styles/")) { + } else if (name.begins_with("theme_override_styles/") || name.begins_with("custom_styles/")) { String dname = name.get_slicec('/', 1); if (data.style_override.has(dname)) { data.style_override[dname]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } data.style_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); - } else if (name.begins_with("custom_fonts/")) { + } else if (name.begins_with("theme_override_fonts/") || name.begins_with("custom_fonts/")) { String dname = name.get_slicec('/', 1); if (data.font_override.has(dname)) { data.font_override[dname]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } data.font_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); - } else if (name.begins_with("custom_font_sizes/")) { + } else if (name.begins_with("theme_override_font_sizes/") || name.begins_with("custom_font_sizes/")) { String dname = name.get_slicec('/', 1); data.font_size_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); - } else if (name.begins_with("custom_colors/")) { + } else if (name.begins_with("theme_override_colors/") || name.begins_with("custom_colors/")) { String dname = name.get_slicec('/', 1); data.color_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); - } else if (name.begins_with("custom_constants/")) { + } else if (name.begins_with("theme_override_constants/") || name.begins_with("custom_constants/")) { String dname = name.get_slicec('/', 1); data.constant_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); @@ -265,22 +281,22 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) { } } else { - if (name.begins_with("custom_icons/")) { + if (name.begins_with("theme_override_icons/") || name.begins_with("custom_icons/")) { String dname = name.get_slicec('/', 1); add_theme_icon_override(dname, p_value); - } else if (name.begins_with("custom_styles/")) { + } else if (name.begins_with("theme_override_styles/") || name.begins_with("custom_styles/")) { String dname = name.get_slicec('/', 1); add_theme_style_override(dname, p_value); - } else if (name.begins_with("custom_fonts/")) { + } else if (name.begins_with("theme_override_fonts/") || name.begins_with("custom_fonts/")) { String dname = name.get_slicec('/', 1); add_theme_font_override(dname, p_value); - } else if (name.begins_with("custom_font_sizes/")) { + } else if (name.begins_with("theme_override_font_sizes/") || name.begins_with("custom_font_sizes/")) { String dname = name.get_slicec('/', 1); add_theme_font_size_override(dname, p_value); - } else if (name.begins_with("custom_colors/")) { + } else if (name.begins_with("theme_override_colors/") || name.begins_with("custom_colors/")) { String dname = name.get_slicec('/', 1); add_theme_color_override(dname, p_value); - } else if (name.begins_with("custom_constants/")) { + } else if (name.begins_with("theme_override_constants/") || name.begins_with("custom_constants/")) { String dname = name.get_slicec('/', 1); add_theme_constant_override(dname, p_value); } else { @@ -307,27 +323,26 @@ void Control::_update_minimum_size() { bool Control::_get(const StringName &p_name, Variant &r_ret) const { String sname = p_name; - - if (!sname.begins_with("custom")) { + if (!sname.begins_with("theme_override")) { return false; } - if (sname.begins_with("custom_icons/")) { + if (sname.begins_with("theme_override_icons/")) { String name = sname.get_slicec('/', 1); r_ret = data.icon_override.has(name) ? Variant(data.icon_override[name]) : Variant(); - } else if (sname.begins_with("custom_styles/")) { + } else if (sname.begins_with("theme_override_styles/")) { String name = sname.get_slicec('/', 1); r_ret = data.style_override.has(name) ? Variant(data.style_override[name]) : Variant(); - } else if (sname.begins_with("custom_fonts/")) { + } else if (sname.begins_with("theme_override_fonts/")) { String name = sname.get_slicec('/', 1); r_ret = data.font_override.has(name) ? Variant(data.font_override[name]) : Variant(); - } else if (sname.begins_with("custom_font_sizes/")) { + } else if (sname.begins_with("theme_override_font_sizes/")) { String name = sname.get_slicec('/', 1); r_ret = data.font_size_override.has(name) ? Variant(data.font_size_override[name]) : Variant(); - } else if (sname.begins_with("custom_colors/")) { + } else if (sname.begins_with("theme_override_colors/")) { String name = sname.get_slicec('/', 1); r_ret = data.color_override.has(name) ? Variant(data.color_override[name]) : Variant(); - } else if (sname.begins_with("custom_constants/")) { + } else if (sname.begins_with("theme_override_constants/")) { String name = sname.get_slicec('/', 1); r_ret = data.constant_override.has(name) ? Variant(data.constant_override[name]) : Variant(); } else { @@ -339,96 +354,139 @@ bool Control::_get(const StringName &p_name, Variant &r_ret) const { void Control::_get_property_list(List<PropertyInfo> *p_list) const { Ref<Theme> theme = Theme::get_default(); - /* Using the default theme since the properties below are meant for editor only - if (data.theme.is_valid()) { - theme = data.theme; - } else { - theme = Theme::get_default(); - }*/ + p_list->push_back(PropertyInfo(Variant::NIL, "Theme Overrides", PROPERTY_HINT_NONE, "theme_override_", PROPERTY_USAGE_GROUP)); { List<StringName> names; - theme->get_icon_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { + theme->get_color_list(get_class_name(), &names); + for (const StringName &E : names) { uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.icon_override.has(E->get())) { + if (data.color_override.has(E)) { usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_icons/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", usage)); + p_list->push_back(PropertyInfo(Variant::COLOR, "theme_override_colors/" + E, PROPERTY_HINT_NONE, "", usage)); } } { List<StringName> names; - theme->get_stylebox_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { + theme->get_constant_list(get_class_name(), &names); + for (const StringName &E : names) { uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.style_override.has(E->get())) { + if (data.constant_override.has(E)) { usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_styles/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", usage)); + p_list->push_back(PropertyInfo(Variant::INT, "theme_override_constants/" + E, PROPERTY_HINT_RANGE, "-16384,16384", usage)); } } { List<StringName> names; theme->get_font_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { + for (const StringName &E : names) { uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.font_override.has(E->get())) { + if (data.font_override.has(E)) { usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_fonts/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "Font", usage)); + p_list->push_back(PropertyInfo(Variant::OBJECT, "theme_override_fonts/" + E, PROPERTY_HINT_RESOURCE_TYPE, "Font", usage)); } } { List<StringName> names; theme->get_font_size_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { + for (const StringName &E : names) { uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.font_size_override.has(E->get())) { + if (data.font_size_override.has(E)) { usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::INT, "custom_font_sizes/" + E->get(), PROPERTY_HINT_NONE, "", usage)); + p_list->push_back(PropertyInfo(Variant::INT, "theme_override_font_sizes/" + E, PROPERTY_HINT_NONE, "", usage)); } } { List<StringName> names; - theme->get_color_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { + theme->get_icon_list(get_class_name(), &names); + for (const StringName &E : names) { uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.color_override.has(E->get())) { + if (data.icon_override.has(E)) { usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::COLOR, "custom_colors/" + E->get(), PROPERTY_HINT_NONE, "", usage)); + p_list->push_back(PropertyInfo(Variant::OBJECT, "theme_override_icons/" + E, PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", usage)); } } { List<StringName> names; - theme->get_constant_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { + theme->get_stylebox_list(get_class_name(), &names); + for (const StringName &E : names) { uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.constant_override.has(E->get())) { + if (data.style_override.has(E)) { usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::INT, "custom_constants/" + E->get(), PROPERTY_HINT_RANGE, "-16384,16384", usage)); + p_list->push_back(PropertyInfo(Variant::OBJECT, "theme_override_styles/" + E, PROPERTY_HINT_RESOURCE_TYPE, "StyleBox", usage)); } } } +void Control::_validate_property(PropertyInfo &property) const { + if (property.name == "theme_type_variation") { + List<StringName> names; + + // Only the default theme and the project theme are used for the list of options. + // This is an imposed limitation to simplify the logic needed to leverage those options. + Theme::get_default()->get_type_variation_list(get_class_name(), &names); + if (Theme::get_project_default().is_valid()) { + Theme::get_project_default()->get_type_variation_list(get_class_name(), &names); + } + names.sort_custom<StringName::AlphCompare>(); + + Vector<StringName> unique_names; + String hint_string; + for (const StringName &E : names) { + // Skip duplicate values. + if (unique_names.has(E)) { + continue; + } + + hint_string += String(E) + ","; + unique_names.append(E); + } + + property.hint_string = hint_string; + } + if (!Object::cast_to<Container>(get_parent())) { + return; + } + // Disable the property if it's managed by the parent container. + bool property_is_managed_by_container = false; + for (unsigned i = 0; i < properties_managed_by_container_count; i++) { + property_is_managed_by_container = properties_managed_by_container[i] == property.name; + if (property_is_managed_by_container) { + break; + } + } + if (property_is_managed_by_container) { + property.usage |= PROPERTY_USAGE_READ_ONLY; + } +} + Control *Control::get_parent_control() const { return data.parent; } +Window *Control::get_parent_window() const { + return data.parent_window; +} + void Control::set_layout_direction(Control::LayoutDirection p_direction) { ERR_FAIL_INDEX((int)p_direction, 4); data.layout_dir = p_direction; + data.is_rtl_dirty = true; + propagate_notification(NOTIFICATION_LAYOUT_DIRECTION_CHANGED); } @@ -437,29 +495,49 @@ Control::LayoutDirection Control::get_layout_direction() const { } bool Control::is_layout_rtl() const { - if (data.layout_dir == LAYOUT_DIRECTION_INHERITED) { - Window *parent_window = Object::cast_to<Window>(get_parent()); - Control *parent_control = get_parent_control(); - if (parent_control) { - return parent_control->is_layout_rtl(); - } else if (parent_window) { - return parent_window->is_layout_rtl(); - } else { - if (GLOBAL_GET("internationalization/rendering/force_right_to_left_layout_direction")) { - return true; + if (data.is_rtl_dirty) { + const_cast<Control *>(this)->data.is_rtl_dirty = false; + if (data.layout_dir == LAYOUT_DIRECTION_INHERITED) { + Window *parent_window = get_parent_window(); + Control *parent_control = get_parent_control(); + if (parent_control) { + const_cast<Control *>(this)->data.is_rtl = parent_control->is_layout_rtl(); + } else if (parent_window) { + const_cast<Control *>(this)->data.is_rtl = parent_window->is_layout_rtl(); + } else { + if (GLOBAL_GET(SNAME("internationalization/rendering/force_right_to_left_layout_direction"))) { + const_cast<Control *>(this)->data.is_rtl = true; + } else { + String locale = TranslationServer::get_singleton()->get_tool_locale(); + const_cast<Control *>(this)->data.is_rtl = TS->is_locale_right_to_left(locale); + } } - String locale = TranslationServer::get_singleton()->get_tool_locale(); - return TS->is_locale_right_to_left(locale); - } - } else if (data.layout_dir == LAYOUT_DIRECTION_LOCALE) { - if (GLOBAL_GET("internationalization/rendering/force_right_to_left_layout_direction")) { - return true; + } else if (data.layout_dir == LAYOUT_DIRECTION_LOCALE) { + if (GLOBAL_GET(SNAME("internationalization/rendering/force_right_to_left_layout_direction"))) { + const_cast<Control *>(this)->data.is_rtl = true; + } else { + String locale = TranslationServer::get_singleton()->get_tool_locale(); + const_cast<Control *>(this)->data.is_rtl = TS->is_locale_right_to_left(locale); + } + } else { + const_cast<Control *>(this)->data.is_rtl = (data.layout_dir == LAYOUT_DIRECTION_RTL); } - String locale = TranslationServer::get_singleton()->get_tool_locale(); - return TS->is_locale_right_to_left(locale); - } else { - return (data.layout_dir == LAYOUT_DIRECTION_RTL); } + return data.is_rtl; +} + +void Control::set_auto_translate(bool p_enable) { + if (p_enable == data.auto_translate) { + return; + } + + data.auto_translate = p_enable; + + notification(MainLoop::NOTIFICATION_TRANSLATION_CHANGED); +} + +bool Control::is_auto_translating() const { + return data.auto_translate; } void Control::_clear_size_warning() { @@ -509,17 +587,22 @@ void Control::_notification(int p_notification) { } break; case NOTIFICATION_POST_ENTER_TREE: { data.minimum_size_valid = false; + data.is_rtl_dirty = true; _size_changed(); } break; case NOTIFICATION_EXIT_TREE: { get_viewport()->_gui_remove_control(this); } break; case NOTIFICATION_READY: { +#ifdef DEBUG_ENABLED connect("ready", callable_mp(this, &Control::_clear_size_warning), varray(), CONNECT_DEFERRED | CONNECT_ONESHOT); +#endif } break; case NOTIFICATION_ENTER_CANVAS: { data.parent = Object::cast_to<Control>(get_parent()); + data.parent_window = Object::cast_to<Window>(get_parent()); + data.is_rtl_dirty = true; Node *parent = this; //meh Control *parent_control = nullptr; @@ -584,6 +667,8 @@ void Control::_notification(int p_notification) { data.parent = nullptr; data.parent_canvas_item = nullptr; + data.parent_window = nullptr; + data.is_rtl_dirty = true; } break; case NOTIFICATION_MOVED_IN_PARENT: { @@ -643,32 +728,17 @@ void Control::_notification(int p_notification) { } break; case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + data.is_rtl_dirty = true; _size_changed(); } break; } } -bool Control::clips_input() const { - if (get_script_instance()) { - return get_script_instance()->call(SceneStringNames::get_singleton()->_clips_input); - } - return false; -} - bool Control::has_point(const Point2 &p_point) const { - if (get_script_instance()) { - Variant v = p_point; - const Variant *p = &v; - Callable::CallError ce; - Variant ret = get_script_instance()->call(SceneStringNames::get_singleton()->has_point, &p, 1, ce); - if (ce.error == Callable::CallError::CALL_OK) { - return ret; - } - } - /*if (has_stylebox("mask")) { - Ref<StyleBox> mask = get_stylebox("mask"); - return mask->test_mask(p_point,Rect2(Point2(),get_size())); - }*/ + bool ret; + if (GDVIRTUAL_CALL(_has_point, p_point, ret)) { + return ret; + } return Rect2(Point2(), get_size()).has_point(p_point); } @@ -685,18 +755,13 @@ Variant Control::get_drag_data(const Point2 &p_point) { Object *obj = ObjectDB::get_instance(data.drag_owner); if (obj) { Control *c = Object::cast_to<Control>(obj); - return c->call("get_drag_data_fw", p_point, this); + return c->call("_get_drag_data_fw", p_point, this); } } - if (get_script_instance()) { - Variant v = p_point; - const Variant *p = &v; - Callable::CallError ce; - Variant ret = get_script_instance()->call(SceneStringNames::get_singleton()->get_drag_data, &p, 1, ce); - if (ce.error == Callable::CallError::CALL_OK) { - return ret; - } + Variant dd; + if (GDVIRTUAL_CALL(_get_drag_data, p_point, dd)) { + return dd; } return Variant(); @@ -707,20 +772,14 @@ bool Control::can_drop_data(const Point2 &p_point, const Variant &p_data) const Object *obj = ObjectDB::get_instance(data.drag_owner); if (obj) { Control *c = Object::cast_to<Control>(obj); - return c->call("can_drop_data_fw", p_point, p_data, this); + return c->call("_can_drop_data_fw", p_point, p_data, this); } } - if (get_script_instance()) { - Variant v = p_point; - const Variant *p[2] = { &v, &p_data }; - Callable::CallError ce; - Variant ret = get_script_instance()->call(SceneStringNames::get_singleton()->can_drop_data, p, 2, ce); - if (ce.error == Callable::CallError::CALL_OK) { - return ret; - } + bool ret; + if (GDVIRTUAL_CALL(_can_drop_data, p_point, p_data, ret)) { + return ret; } - return false; } @@ -729,20 +788,12 @@ void Control::drop_data(const Point2 &p_point, const Variant &p_data) { Object *obj = ObjectDB::get_instance(data.drag_owner); if (obj) { Control *c = Object::cast_to<Control>(obj); - c->call("drop_data_fw", p_point, p_data, this); + c->call("_drop_data_fw", p_point, p_data, this); return; } } - if (get_script_instance()) { - Variant v = p_point; - const Variant *p[2] = { &v, &p_data }; - Callable::CallError ce; - Variant ret = get_script_instance()->call(SceneStringNames::get_singleton()->drop_data, p, 2, ce); - if (ce.error == Callable::CallError::CALL_OK) { - return; - } - } + GDVIRTUAL_CALL(_drop_data, p_point, p_data); } void Control::force_drag(const Variant &p_data, Control *p_control) { @@ -758,16 +809,26 @@ void Control::set_drag_preview(Control *p_control) { get_viewport()->_gui_set_drag_preview(this, p_control); } +void Control::_call_gui_input(const Ref<InputEvent> &p_event) { + emit_signal(SceneStringNames::get_singleton()->gui_input, p_event); //signal should be first, so it's possible to override an event (and then accept it) + if (!is_inside_tree() || get_viewport()->is_input_handled()) { + return; //input was handled, abort + } + GDVIRTUAL_CALL(_gui_input, p_event); + if (!is_inside_tree() || get_viewport()->is_input_handled()) { + return; //input was handled, abort + } + gui_input(p_event); +} +void Control::gui_input(const Ref<InputEvent> &p_event) { +} + Size2 Control::get_minimum_size() const { - ScriptInstance *si = const_cast<Control *>(this)->get_script_instance(); - if (si) { - Callable::CallError ce; - Variant s = si->call(SceneStringNames::get_singleton()->_get_minimum_size, nullptr, 0, ce); - if (ce.error == Callable::CallError::CALL_OK) { - return s; - } + Vector2 ms; + if (GDVIRTUAL_CALL(_get_minimum_size, ms)) { + return ms; } - return Size2(); + return Vector2(); } template <class T> @@ -780,13 +841,13 @@ T Control::get_theme_item_in_types(Control *p_theme_owner, Window *p_theme_owner Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - for (List<StringName>::Element *E = p_theme_types.front(); E; E = E->next()) { - if (theme_owner && theme_owner->data.theme->has_theme_item(p_data_type, p_name, E->get())) { - return theme_owner->data.theme->get_theme_item(p_data_type, p_name, E->get()); + for (const StringName &E : p_theme_types) { + if (theme_owner && theme_owner->data.theme->has_theme_item(p_data_type, p_name, E)) { + return theme_owner->data.theme->get_theme_item(p_data_type, p_name, E); } - if (theme_owner_window && theme_owner_window->theme->has_theme_item(p_data_type, p_name, E->get())) { - return theme_owner_window->theme->get_theme_item(p_data_type, p_name, E->get()); + if (theme_owner_window && theme_owner_window->theme->has_theme_item(p_data_type, p_name, E)) { + return theme_owner_window->theme->get_theme_item(p_data_type, p_name, E); } } @@ -809,17 +870,17 @@ T Control::get_theme_item_in_types(Control *p_theme_owner, Window *p_theme_owner // Secondly, check the project-defined Theme resource. if (Theme::get_project_default().is_valid()) { - for (List<StringName>::Element *E = p_theme_types.front(); E; E = E->next()) { - if (Theme::get_project_default()->has_theme_item(p_data_type, p_name, E->get())) { - return Theme::get_project_default()->get_theme_item(p_data_type, p_name, E->get()); + for (const StringName &E : p_theme_types) { + if (Theme::get_project_default()->has_theme_item(p_data_type, p_name, E)) { + return Theme::get_project_default()->get_theme_item(p_data_type, p_name, E); } } } // Lastly, fall back on the items defined in the default Theme, if they exist. - for (List<StringName>::Element *E = p_theme_types.front(); E; E = E->next()) { - if (Theme::get_default()->has_theme_item(p_data_type, p_name, E->get())) { - return Theme::get_default()->get_theme_item(p_data_type, p_name, E->get()); + for (const StringName &E : p_theme_types) { + if (Theme::get_default()->has_theme_item(p_data_type, p_name, E)) { + return Theme::get_default()->get_theme_item(p_data_type, p_name, E); } } // If they don't exist, use any type to return the default/empty value. @@ -835,12 +896,12 @@ bool Control::has_theme_item_in_types(Control *p_theme_owner, Window *p_theme_ow Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - for (List<StringName>::Element *E = p_theme_types.front(); E; E = E->next()) { - if (theme_owner && theme_owner->data.theme->has_theme_item(p_data_type, p_name, E->get())) { + for (const StringName &E : p_theme_types) { + if (theme_owner && theme_owner->data.theme->has_theme_item(p_data_type, p_name, E)) { return true; } - if (theme_owner_window && theme_owner_window->theme->has_theme_item(p_data_type, p_name, E->get())) { + if (theme_owner_window && theme_owner_window->theme->has_theme_item(p_data_type, p_name, E)) { return true; } } @@ -864,16 +925,16 @@ bool Control::has_theme_item_in_types(Control *p_theme_owner, Window *p_theme_ow // Secondly, check the project-defined Theme resource. if (Theme::get_project_default().is_valid()) { - for (List<StringName>::Element *E = p_theme_types.front(); E; E = E->next()) { - if (Theme::get_project_default()->has_theme_item(p_data_type, p_name, E->get())) { + for (const StringName &E : p_theme_types) { + if (Theme::get_project_default()->has_theme_item(p_data_type, p_name, E)) { return true; } } } // Lastly, fall back on the items defined in the default Theme, if they exist. - for (List<StringName>::Element *E = p_theme_types.front(); E; E = E->next()) { - if (Theme::get_default()->has_theme_item(p_data_type, p_name, E->get())) { + for (const StringName &E : p_theme_types) { + if (Theme::get_default()->has_theme_item(p_data_type, p_name, E)) { return true; } } @@ -881,18 +942,19 @@ bool Control::has_theme_item_in_types(Control *p_theme_owner, Window *p_theme_ow } void Control::_get_theme_type_dependencies(const StringName &p_theme_type, List<StringName> *p_list) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { - if (data.theme_custom_type != StringName()) { - p_list->push_back(data.theme_custom_type); + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { + if (Theme::get_project_default().is_valid() && Theme::get_project_default()->get_type_variation_base(data.theme_type_variation) != StringName()) { + Theme::get_project_default()->get_type_dependencies(get_class_name(), data.theme_type_variation, p_list); + } else { + Theme::get_default()->get_type_dependencies(get_class_name(), data.theme_type_variation, p_list); } - Theme::get_type_dependencies(get_class_name(), p_list); } else { - Theme::get_type_dependencies(p_theme_type, p_list); + Theme::get_default()->get_type_dependencies(p_theme_type, StringName(), p_list); } } Ref<Texture2D> Control::get_theme_icon(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { const Ref<Texture2D> *tex = data.icon_override.getptr(p_name); if (tex) { return *tex; @@ -905,7 +967,7 @@ Ref<Texture2D> Control::get_theme_icon(const StringName &p_name, const StringNam } Ref<StyleBox> Control::get_theme_stylebox(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { const Ref<StyleBox> *style = data.style_override.getptr(p_name); if (style) { return *style; @@ -918,7 +980,7 @@ Ref<StyleBox> Control::get_theme_stylebox(const StringName &p_name, const String } Ref<Font> Control::get_theme_font(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { const Ref<Font> *font = data.font_override.getptr(p_name); if (font) { return *font; @@ -931,7 +993,7 @@ Ref<Font> Control::get_theme_font(const StringName &p_name, const StringName &p_ } int Control::get_theme_font_size(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { const int *font_size = data.font_size_override.getptr(p_name); if (font_size) { return *font_size; @@ -944,7 +1006,7 @@ int Control::get_theme_font_size(const StringName &p_name, const StringName &p_t } Color Control::get_theme_color(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { const Color *color = data.color_override.getptr(p_name); if (color) { return *color; @@ -957,7 +1019,7 @@ Color Control::get_theme_color(const StringName &p_name, const StringName &p_the } int Control::get_theme_constant(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { const int *constant = data.constant_override.getptr(p_name); if (constant) { return *constant; @@ -1000,7 +1062,7 @@ bool Control::has_theme_constant_override(const StringName &p_name) const { } bool Control::has_theme_icon(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { if (has_theme_icon_override(p_name)) { return true; } @@ -1012,7 +1074,7 @@ bool Control::has_theme_icon(const StringName &p_name, const StringName &p_theme } bool Control::has_theme_stylebox(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { if (has_theme_stylebox_override(p_name)) { return true; } @@ -1024,7 +1086,7 @@ bool Control::has_theme_stylebox(const StringName &p_name, const StringName &p_t } bool Control::has_theme_font(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { if (has_theme_font_override(p_name)) { return true; } @@ -1036,7 +1098,7 @@ bool Control::has_theme_font(const StringName &p_name, const StringName &p_theme } bool Control::has_theme_font_size(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { if (has_theme_font_size_override(p_name)) { return true; } @@ -1048,7 +1110,7 @@ bool Control::has_theme_font_size(const StringName &p_name, const StringName &p_ } bool Control::has_theme_color(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { if (has_theme_color_override(p_name)) { return true; } @@ -1060,7 +1122,7 @@ bool Control::has_theme_color(const StringName &p_name, const StringName &p_them } bool Control::has_theme_constant(const StringName &p_name, const StringName &p_theme_type) const { - if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_custom_type) { + if (p_theme_type == StringName() || p_theme_type == get_class_name() || p_theme_type == data.theme_type_variation) { if (has_theme_constant_override(p_name)) { return true; } @@ -1082,7 +1144,7 @@ Rect2 Control::get_parent_anchorable_rect() const { } else { #ifdef TOOLS_ENABLED Node *edited_root = get_tree()->get_edited_scene_root(); - if (edited_root && (this == edited_root || edited_root->is_a_parent_of(this))) { + if (edited_root && (this == edited_root || edited_root->is_ancestor_of(this))) { parent_rect.size = Size2(ProjectSettings::get_singleton()->get("display/window/size/width"), ProjectSettings::get_singleton()->get("display/window/size/height")); } else { parent_rect = get_viewport()->get_visible_rect(); @@ -1502,7 +1564,7 @@ Point2 Control::get_global_position() const { Point2 Control::get_screen_position() const { ERR_FAIL_COND_V(!is_inside_tree(), Point2()); - Point2 global_pos = get_global_position(); + Point2 global_pos = get_viewport()->get_canvas_transform().xform(get_global_position()); Window *w = Object::cast_to<Window>(get_viewport()); if (w && !w->is_embedding_subwindows()) { global_pos += w->get_position(); @@ -1579,8 +1641,8 @@ void Control::set_rect(const Rect2 &p_rect) { void Control::_set_size(const Size2 &p_size) { #ifdef DEBUG_ENABLED - if (data.size_warning) { - WARN_PRINT("Adjusting the size of Control nodes before they are fully initialized is unreliable. Consider deferring it with set_deferred()."); + if (data.size_warning && (data.anchor[SIDE_LEFT] != data.anchor[SIDE_RIGHT] || data.anchor[SIDE_TOP] != data.anchor[SIDE_BOTTOM])) { + WARN_PRINT("Nodes with non-equal opposite anchors will have their size overridden after _ready(). \nIf you want to set size, change the anchors or consider using set_deferred()."); } #endif set_size(p_size); @@ -1644,6 +1706,17 @@ Rect2 Control::get_anchorable_rect() const { return Rect2(Point2(), get_size()); } +void Control::begin_bulk_theme_override() { + data.bulk_theme_override = true; +} + +void Control::end_bulk_theme_override() { + ERR_FAIL_COND(!data.bulk_theme_override); + + data.bulk_theme_override = false; + _notify_theme_changed(); +} + void Control::add_theme_icon_override(const StringName &p_name, const Ref<Texture2D> &p_icon) { ERR_FAIL_COND(!p_icon.is_valid()); @@ -1653,7 +1726,7 @@ void Control::add_theme_icon_override(const StringName &p_name, const Ref<Textur data.icon_override[p_name] = p_icon; data.icon_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::add_theme_style_override(const StringName &p_name, const Ref<StyleBox> &p_style) { @@ -1665,7 +1738,7 @@ void Control::add_theme_style_override(const StringName &p_name, const Ref<Style data.style_override[p_name] = p_style; data.style_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::add_theme_font_override(const StringName &p_name, const Ref<Font> &p_font) { @@ -1677,22 +1750,22 @@ void Control::add_theme_font_override(const StringName &p_name, const Ref<Font> data.font_override[p_name] = p_font; data.font_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::add_theme_font_size_override(const StringName &p_name, int p_font_size) { data.font_size_override[p_name] = p_font_size; - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::add_theme_color_override(const StringName &p_name, const Color &p_color) { data.color_override[p_name] = p_color; - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::add_theme_constant_override(const StringName &p_name, int p_constant) { data.constant_override[p_name] = p_constant; - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::remove_theme_icon_override(const StringName &p_name) { @@ -1701,7 +1774,7 @@ void Control::remove_theme_icon_override(const StringName &p_name) { } data.icon_override.erase(p_name); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::remove_theme_style_override(const StringName &p_name) { @@ -1710,7 +1783,7 @@ void Control::remove_theme_style_override(const StringName &p_name) { } data.style_override.erase(p_name); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::remove_theme_font_override(const StringName &p_name) { @@ -1719,22 +1792,22 @@ void Control::remove_theme_font_override(const StringName &p_name) { } data.font_override.erase(p_name); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::remove_theme_font_size_override(const StringName &p_name) { data.font_size_override.erase(p_name); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::remove_theme_color_override(const StringName &p_name) { data.color_override.erase(p_name); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::remove_theme_constant_override(const StringName &p_name) { data.constant_override.erase(p_name); - notification(NOTIFICATION_THEME_CHANGED); + _notify_theme_changed(); } void Control::set_focus_mode(FocusMode p_focus_mode) { @@ -2007,6 +2080,12 @@ void Control::_theme_changed() { _propagate_theme_changed(this, this, nullptr, false); } +void Control::_notify_theme_changed() { + if (!data.bulk_theme_override) { + notification(NOTIFICATION_THEME_CHANGED); + } +} + void Control::set_theme(const Ref<Theme> &p_theme) { if (data.theme == p_theme) { return; @@ -2045,13 +2124,13 @@ Ref<Theme> Control::get_theme() const { return data.theme; } -void Control::set_theme_custom_type(const StringName &p_theme_type) { - data.theme_custom_type = p_theme_type; +void Control::set_theme_type_variation(const StringName &p_theme_type) { + data.theme_type_variation = p_theme_type; _propagate_theme_changed(this, data.theme_owner, data.theme_owner_window); } -StringName Control::get_theme_custom_type() const { - return data.theme_custom_type; +StringName Control::get_theme_type_variation() const { + return data.theme_type_variation; } void Control::set_tooltip(const String &p_tooltip) { @@ -2064,8 +2143,9 @@ String Control::get_tooltip(const Point2 &p_pos) const { } Control *Control::make_custom_tooltip(const String &p_text) const { - if (get_script_instance()) { - return const_cast<Control *>(this)->call("_make_custom_tooltip", p_text); + Object *ret = nullptr; + if (GDVIRTUAL_CALL(_make_custom_tooltip, p_text, ret)) { + return Object::cast_to<Control>(ret); } return nullptr; } @@ -2440,14 +2520,11 @@ Vector<Vector2i> Control::structured_text_parser(StructuredTextParser p_theme_ty } } break; case STRUCTURED_TEXT_CUSTOM: { - if (get_script_instance()) { - Variant data = get_script_instance()->call(SceneStringNames::get_singleton()->_structured_text_parser, p_args, p_text); - if (data.get_type() == Variant::ARRAY) { - Array _data = data; - for (int i = 0; i < _data.size(); i++) { - if (_data[i].get_type() == Variant::VECTOR2I) { - ret.push_back(Vector2i(_data[i])); - } + Array r; + if (GDVIRTUAL_CALL(_structured_text_parser, p_args, p_text, r)) { + for (int i = 0; i < r.size(); i++) { + if (r[i].get_type() == Variant::VECTOR2I) { + ret.push_back(Vector2i(r[i])); } } } @@ -2471,14 +2548,6 @@ real_t Control::get_rotation() const { return data.rotation; } -void Control::set_rotation_degrees(real_t p_degrees) { - set_rotation(Math::deg2rad(p_degrees)); -} - -real_t Control::get_rotation_degrees() const { - return Math::rad2deg(get_rotation()); -} - void Control::_override_changed() { notification(NOTIFICATION_THEME_CHANGED); emit_signal(SceneStringNames::get_singleton()->theme_changed); @@ -2551,7 +2620,7 @@ bool Control::is_visibility_clip_disabled() const { void Control::get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const { #ifdef TOOLS_ENABLED - const String quote_style = EDITOR_DEF("text_editor/completion/use_single_quotes", 0) ? "'" : "\""; + const String quote_style = EDITOR_GET("text_editor/completion/use_single_quotes") ? "'" : "\""; #else const String quote_style = "\""; #endif @@ -2574,8 +2643,8 @@ void Control::get_argument_options(const StringName &p_function, int p_idx, List } sn.sort_custom<StringName::AlphCompare>(); - for (List<StringName>::Element *E = sn.front(); E; E = E->next()) { - r_options->push_back(quote_style + E->get() + quote_style); + for (const StringName &name : sn) { + r_options->push_back(String(name).quote(quote_style)); } } } @@ -2646,7 +2715,6 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("set_global_position", "position", "keep_offsets"), &Control::set_global_position, DEFVAL(false)); ClassDB::bind_method(D_METHOD("_set_global_position", "position"), &Control::_set_global_position); ClassDB::bind_method(D_METHOD("set_rotation", "radians"), &Control::set_rotation); - ClassDB::bind_method(D_METHOD("set_rotation_degrees", "degrees"), &Control::set_rotation_degrees); ClassDB::bind_method(D_METHOD("set_scale", "scale"), &Control::set_scale); ClassDB::bind_method(D_METHOD("set_pivot_offset", "pivot_offset"), &Control::set_pivot_offset); ClassDB::bind_method(D_METHOD("get_offset", "offset"), &Control::get_offset); @@ -2655,7 +2723,6 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_position"), &Control::get_position); ClassDB::bind_method(D_METHOD("get_size"), &Control::get_size); ClassDB::bind_method(D_METHOD("get_rotation"), &Control::get_rotation); - ClassDB::bind_method(D_METHOD("get_rotation_degrees"), &Control::get_rotation_degrees); ClassDB::bind_method(D_METHOD("get_scale"), &Control::get_scale); ClassDB::bind_method(D_METHOD("get_pivot_offset"), &Control::get_pivot_offset); ClassDB::bind_method(D_METHOD("get_custom_minimum_size"), &Control::get_custom_minimum_size); @@ -2684,8 +2751,11 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("set_theme", "theme"), &Control::set_theme); ClassDB::bind_method(D_METHOD("get_theme"), &Control::get_theme); - ClassDB::bind_method(D_METHOD("set_theme_custom_type", "theme_type"), &Control::set_theme_custom_type); - ClassDB::bind_method(D_METHOD("get_theme_custom_type"), &Control::get_theme_custom_type); + ClassDB::bind_method(D_METHOD("set_theme_type_variation", "theme_type"), &Control::set_theme_type_variation); + ClassDB::bind_method(D_METHOD("get_theme_type_variation"), &Control::get_theme_type_variation); + + ClassDB::bind_method(D_METHOD("begin_bulk_theme_override"), &Control::begin_bulk_theme_override); + ClassDB::bind_method(D_METHOD("end_bulk_theme_override"), &Control::end_bulk_theme_override); ClassDB::bind_method(D_METHOD("add_theme_icon_override", "name", "texture"), &Control::add_theme_icon_override); ClassDB::bind_method(D_METHOD("add_theme_stylebox_override", "name", "stylebox"), &Control::add_theme_style_override); @@ -2768,21 +2838,8 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_layout_direction"), &Control::get_layout_direction); ClassDB::bind_method(D_METHOD("is_layout_rtl"), &Control::is_layout_rtl); - BIND_VMETHOD(MethodInfo("_structured_text_parser", PropertyInfo(Variant::ARRAY, "args"), PropertyInfo(Variant::STRING, "text"))); - - BIND_VMETHOD(MethodInfo("_gui_input", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); - BIND_VMETHOD(MethodInfo(Variant::VECTOR2, "_get_minimum_size")); - - MethodInfo get_drag_data = MethodInfo("get_drag_data", PropertyInfo(Variant::VECTOR2, "position")); - get_drag_data.return_val.usage |= PROPERTY_USAGE_NIL_IS_VARIANT; - BIND_VMETHOD(get_drag_data); - - BIND_VMETHOD(MethodInfo(Variant::BOOL, "can_drop_data", PropertyInfo(Variant::VECTOR2, "position"), PropertyInfo(Variant::NIL, "data"))); - BIND_VMETHOD(MethodInfo("drop_data", PropertyInfo(Variant::VECTOR2, "position"), PropertyInfo(Variant::NIL, "data"))); - BIND_VMETHOD(MethodInfo( - PropertyInfo(Variant::OBJECT, "control", PROPERTY_HINT_RESOURCE_TYPE, "Control"), - "_make_custom_tooltip", PropertyInfo(Variant::STRING, "for_text"))); - BIND_VMETHOD(MethodInfo(Variant::BOOL, "_clips_input")); + ClassDB::bind_method(D_METHOD("set_auto_translate", "enable"), &Control::set_auto_translate); + ClassDB::bind_method(D_METHOD("is_auto_translating"), &Control::is_auto_translating); ADD_GROUP("Anchor", "anchor_"); ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_left", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_LEFT); @@ -2803,13 +2860,15 @@ void Control::_bind_methods() { ADD_GROUP("Layout Direction", "layout_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "layout_direction", PROPERTY_HINT_ENUM, "Inherited,Locale,Left-to-Right,Right-to-Left"), "set_layout_direction", "get_layout_direction"); + ADD_GROUP("Auto Translate", ""); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_translate"), "set_auto_translate", "is_auto_translating"); + ADD_GROUP("Rect", "rect_"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "_set_position", "get_position"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_global_position", PROPERTY_HINT_NONE, "", 0), "_set_global_position", "get_global_position"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_global_position", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "_set_global_position", "get_global_position"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_size", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR), "_set_size", "get_size"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_min_size"), "set_custom_minimum_size", "get_custom_minimum_size"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rect_rotation", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR), "set_rotation", "get_rotation"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rect_rotation_degrees", PROPERTY_HINT_RANGE, "-360,360,0.1,or_lesser,or_greater"), "set_rotation_degrees", "get_rotation_degrees"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rect_rotation", PROPERTY_HINT_RANGE, "-360,360,0.1,or_lesser,or_greater,radians"), "set_rotation", "get_rotation"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_scale"), "set_scale", "get_scale"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "rect_pivot_offset"), "set_pivot_offset", "get_pivot_offset"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "rect_clip_content"), "set_clip_contents", "is_clipping_contents"); @@ -2834,10 +2893,10 @@ void Control::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "size_flags_horizontal", PROPERTY_HINT_FLAGS, "Fill,Expand,Shrink Center,Shrink End"), "set_h_size_flags", "get_h_size_flags"); ADD_PROPERTY(PropertyInfo(Variant::INT, "size_flags_vertical", PROPERTY_HINT_FLAGS, "Fill,Expand,Shrink Center,Shrink End"), "set_v_size_flags", "get_v_size_flags"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "size_flags_stretch_ratio", PROPERTY_HINT_RANGE, "0,20,0.01,or_greater"), "set_stretch_ratio", "get_stretch_ratio"); + ADD_GROUP("Theme", "theme_"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "theme", PROPERTY_HINT_RESOURCE_TYPE, "Theme"), "set_theme", "get_theme"); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "theme_custom_type"), "set_theme_custom_type", "get_theme_custom_type"); - ADD_GROUP("", ""); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "theme_type_variation", PROPERTY_HINT_ENUM_SUGGESTION), "set_theme_type_variation", "get_theme_type_variation"); BIND_ENUM_CONSTANT(FOCUS_NONE); BIND_ENUM_CONSTANT(FOCUS_CLICK); @@ -2938,5 +2997,14 @@ void Control::_bind_methods() { ADD_SIGNAL(MethodInfo("minimum_size_changed")); ADD_SIGNAL(MethodInfo("theme_changed")); - BIND_VMETHOD(MethodInfo(Variant::BOOL, "has_point", PropertyInfo(Variant::VECTOR2, "point"))); + GDVIRTUAL_BIND(_has_point, "position"); + GDVIRTUAL_BIND(_structured_text_parser, "args", "text"); + GDVIRTUAL_BIND(_get_minimum_size); + + GDVIRTUAL_BIND(_get_drag_data, "at_position"); + GDVIRTUAL_BIND(_can_drop_data, "at_position", "data"); + GDVIRTUAL_BIND(_drop_data, "at_position", "data"); + GDVIRTUAL_BIND(_make_custom_tooltip, "for_text"); + + GDVIRTUAL_BIND(_gui_input, "event"); } diff --git a/scene/gui/control.h b/scene/gui/control.h index a05025c32d..9cec5d6e8d 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -31,9 +31,10 @@ #ifndef CONTROL_H #define CONTROL_H +#include "core/input/shortcut.h" #include "core/math/transform_2d.h" +#include "core/object/gdvirtual.gen.inc" #include "core/templates/rid.h" -#include "scene/gui/shortcut.h" #include "scene/main/canvas_item.h" #include "scene/main/node.h" #include "scene/main/timer.h" @@ -178,6 +179,10 @@ private: GrowDirection v_grow = GROW_DIRECTION_END; LayoutDirection layout_dir = LAYOUT_DIRECTION_INHERITED; + bool is_rtl_dirty = true; + bool is_rtl = false; + + bool auto_translate = true; real_t rotation = 0.0; Vector2 scale = Vector2(1, 1); @@ -201,7 +206,8 @@ private: Ref<Theme> theme; Control *theme_owner = nullptr; Window *theme_owner_window = nullptr; - StringName theme_custom_type; + Window *parent_window = nullptr; + StringName theme_type_variation; String tooltip; CursorShape default_cursor = CURSOR_ARROW; @@ -214,6 +220,7 @@ private: NodePath focus_next; NodePath focus_prev; + bool bulk_theme_override = false; HashMap<StringName, Ref<Texture2D>> icon_override; HashMap<StringName, Ref<StyleBox>> style_override; HashMap<StringName, Ref<Font>> font_override; @@ -223,6 +230,9 @@ private: } data; + static constexpr unsigned properties_managed_by_container_count = 11; + static String properties_managed_by_container[properties_managed_by_container_count]; + // used internally Control *_find_control_at_pos(CanvasItem *p_node, const Point2 &p_pos, const Transform2D &p_xform, Transform2D &r_inv_xform); @@ -235,6 +245,7 @@ private: void _set_size(const Size2 &p_size); void _theme_changed(); + void _notify_theme_changed(); void _update_minimum_size(); @@ -255,6 +266,8 @@ private: friend class Viewport; + void _call_gui_input(const Ref<InputEvent> &p_event); + void _update_minimum_size_cache(); friend class Window; static void _propagate_theme_changed(Node *p_at, Control *p_owner, Window *p_owner_window, bool p_assign = true); @@ -277,11 +290,22 @@ protected: void _get_property_list(List<PropertyInfo> *p_list) const; void _notification(int p_notification); - static void _bind_methods(); + virtual void _validate_property(PropertyInfo &property) const override; //bind helpers + GDVIRTUAL1RC(bool, _has_point, Vector2) + GDVIRTUAL2RC(Array, _structured_text_parser, Array, String) + GDVIRTUAL0RC(Vector2, _get_minimum_size) + + GDVIRTUAL1RC(Variant, _get_drag_data, Vector2) + GDVIRTUAL2RC(bool, _can_drop_data, Vector2, Variant) + GDVIRTUAL2(_drop_data, Vector2, Variant) + GDVIRTUAL1RC(Object *, _make_custom_tooltip, String) + + GDVIRTUAL1(_gui_input, Ref<InputEvent>) + public: enum { /* NOTIFICATION_DRAW=30, @@ -324,12 +348,13 @@ public: virtual Size2 _edit_get_minimum_size() const override; #endif + virtual void gui_input(const Ref<InputEvent> &p_event); + void accept_event(); virtual Size2 get_minimum_size() const; virtual Size2 get_combined_minimum_size() const; virtual bool has_point(const Point2 &p_point) const; - virtual bool clips_input() const; virtual void set_drag_forwarding(Control *p_target); virtual Variant get_drag_data(const Point2 &p_point); virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const; @@ -341,11 +366,16 @@ public: Size2 get_custom_minimum_size() const; Control *get_parent_control() const; + Window *get_parent_window() const; void set_layout_direction(LayoutDirection p_direction); LayoutDirection get_layout_direction() const; virtual bool is_layout_rtl() const; + void set_auto_translate(bool p_enable); + bool is_auto_translating() const; + _FORCE_INLINE_ String atr(const String p_string) const { return is_auto_translating() ? tr(p_string) : p_string; }; + /* POSITIONING */ void set_anchors_preset(LayoutPreset p_preset, bool p_keep_offsets = true); @@ -384,9 +414,7 @@ public: void set_rect(const Rect2 &p_rect); // Reset anchors to begin and set rect, for faster container children sorting. void set_rotation(real_t p_radians); - void set_rotation_degrees(real_t p_degrees); real_t get_rotation() const; - real_t get_rotation_degrees() const; void set_h_grow_direction(GrowDirection p_direction); GrowDirection get_h_grow_direction() const; @@ -403,8 +431,8 @@ public: void set_theme(const Ref<Theme> &p_theme); Ref<Theme> get_theme() const; - void set_theme_custom_type(const StringName &p_theme_type); - StringName get_theme_custom_type() const; + void set_theme_type_variation(const StringName &p_theme_type); + StringName get_theme_type_variation() const; void set_h_size_flags(int p_flags); int get_h_size_flags() const; @@ -443,6 +471,9 @@ public: /* SKINNING */ + void begin_bulk_theme_override(); + void end_bulk_theme_override(); + void add_theme_icon_override(const StringName &p_name, const Ref<Texture2D> &p_icon); void add_theme_style_override(const StringName &p_name, const Ref<StyleBox> &p_style); void add_theme_font_override(const StringName &p_name, const Ref<Font> &p_font); diff --git a/scene/gui/dialogs.cpp b/scene/gui/dialogs.cpp index b6884bd37d..5d98aaa698 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -72,13 +72,10 @@ void AcceptDialog::_notification(int p_what) { parent_visible = nullptr; } } - } break; - case NOTIFICATION_THEME_CHANGED: { - bg->add_theme_style_override("panel", bg->get_theme_stylebox("panel", "AcceptDialog")); + bg->add_theme_style_override("panel", bg->get_theme_stylebox(SNAME("panel"), SNAME("AcceptDialog"))); } break; - case NOTIFICATION_EXIT_TREE: { if (parent_visible) { parent_visible->disconnect("focus_entered", callable_mp(this, &AcceptDialog::_parent_focused)); @@ -97,7 +94,7 @@ void AcceptDialog::_notification(int p_what) { } } -void AcceptDialog::_text_entered(const String &p_text) { +void AcceptDialog::_text_submitted(const String &p_text) { _ok_pressed(); } @@ -106,7 +103,7 @@ void AcceptDialog::_ok_pressed() { set_visible(false); } ok_pressed(); - emit_signal("confirmed"); + emit_signal(SNAME("confirmed")); } void AcceptDialog::_cancel_pressed() { @@ -116,9 +113,9 @@ void AcceptDialog::_cancel_pressed() { parent_visible = nullptr; } - call_deferred("hide"); + call_deferred(SNAME("hide")); - emit_signal("cancelled"); + emit_signal(SNAME("cancelled")); cancel_pressed(); @@ -148,18 +145,18 @@ bool AcceptDialog::get_hide_on_ok() const { } void AcceptDialog::set_autowrap(bool p_autowrap) { - label->set_autowrap(p_autowrap); + label->set_autowrap_mode(p_autowrap ? Label::AUTOWRAP_WORD : Label::AUTOWRAP_OFF); } bool AcceptDialog::has_autowrap() { - return label->has_autowrap(); + return label->get_autowrap_mode() != Label::AUTOWRAP_OFF; } -void AcceptDialog::register_text_enter(Node *p_line_edit) { +void AcceptDialog::register_text_enter(Control *p_line_edit) { ERR_FAIL_NULL(p_line_edit); LineEdit *line_edit = Object::cast_to<LineEdit>(p_line_edit); if (line_edit) { - line_edit->connect("text_entered", callable_mp(this, &AcceptDialog::_text_entered)); + line_edit->connect("text_submitted", callable_mp(this, &AcceptDialog::_text_submitted)); } } @@ -168,7 +165,7 @@ void AcceptDialog::_update_child_rects() { if (label->get_text().is_empty()) { label_size.height = 0; } - int margin = hbc->get_theme_constant("margin", "Dialogs"); + int margin = hbc->get_theme_constant(SNAME("margin"), SNAME("Dialogs")); Size2 size = get_size(); Size2 hminsize = hbc->get_combined_minimum_size(); @@ -200,7 +197,7 @@ void AcceptDialog::_update_child_rects() { } Size2 AcceptDialog::_get_contents_minimum_size() const { - int margin = hbc->get_theme_constant("margin", "Dialogs"); + int margin = hbc->get_theme_constant(SNAME("margin"), SNAME("Dialogs")); Size2 minsize = label->get_combined_minimum_size(); for (int i = 0; i < get_child_count(); i++) { @@ -230,7 +227,7 @@ Size2 AcceptDialog::_get_contents_minimum_size() const { } void AcceptDialog::_custom_action(const String &p_action) { - emit_signal("custom_action", p_action); + emit_signal(SNAME("custom_action"), p_action); custom_action(p_action); } @@ -263,6 +260,28 @@ Button *AcceptDialog::add_cancel_button(const String &p_cancel) { return b; } +void AcceptDialog::remove_button(Control *p_button) { + Button *button = Object::cast_to<Button>(p_button); + ERR_FAIL_NULL(button); + ERR_FAIL_COND_MSG(button->get_parent() != hbc, vformat("Cannot remove button %s as it does not belong to this dialog.", button->get_name())); + ERR_FAIL_COND_MSG(button == ok, "Cannot remove dialog's OK button."); + + Node *right_spacer = hbc->get_child(button->get_index() + 1); + // Should always be valid but let's avoid crashing + if (right_spacer) { + hbc->remove_child(right_spacer); + memdelete(right_spacer); + } + hbc->remove_child(button); + + if (button->is_connected("pressed", callable_mp(this, &AcceptDialog::_custom_action))) { + button->disconnect("pressed", callable_mp(this, &AcceptDialog::_custom_action)); + } + if (button->is_connected("pressed", callable_mp(this, &AcceptDialog::_cancel_pressed))) { + button->disconnect("pressed", callable_mp(this, &AcceptDialog::_cancel_pressed)); + } +} + void AcceptDialog::_bind_methods() { ClassDB::bind_method(D_METHOD("get_ok_button"), &AcceptDialog::get_ok_button); ClassDB::bind_method(D_METHOD("get_label"), &AcceptDialog::get_label); @@ -270,6 +289,7 @@ void AcceptDialog::_bind_methods() { ClassDB::bind_method(D_METHOD("get_hide_on_ok"), &AcceptDialog::get_hide_on_ok); ClassDB::bind_method(D_METHOD("add_button", "text", "right", "action"), &AcceptDialog::add_button, DEFVAL(false), DEFVAL("")); ClassDB::bind_method(D_METHOD("add_cancel_button", "name"), &AcceptDialog::add_cancel_button); + ClassDB::bind_method(D_METHOD("remove_button", "button"), &AcceptDialog::remove_button); ClassDB::bind_method(D_METHOD("register_text_enter", "line_edit"), &AcceptDialog::register_text_enter); ClassDB::bind_method(D_METHOD("set_text", "text"), &AcceptDialog::set_text); ClassDB::bind_method(D_METHOD("get_text"), &AcceptDialog::get_text); @@ -299,21 +319,21 @@ AcceptDialog::AcceptDialog() { set_clamp_to_embedder(true); bg = memnew(Panel); - add_child(bg); + add_child(bg, false, INTERNAL_MODE_FRONT); hbc = memnew(HBoxContainer); - int margin = hbc->get_theme_constant("margin", "Dialogs"); - int button_margin = hbc->get_theme_constant("button_margin", "Dialogs"); + int margin = hbc->get_theme_constant(SNAME("margin"), SNAME("Dialogs")); + int button_margin = hbc->get_theme_constant(SNAME("button_margin"), SNAME("Dialogs")); label = memnew(Label); label->set_anchor(SIDE_RIGHT, Control::ANCHOR_END); label->set_anchor(SIDE_BOTTOM, Control::ANCHOR_END); label->set_begin(Point2(margin, margin)); label->set_end(Point2(-margin, -button_margin - 10)); - add_child(label); + add_child(label, false, INTERNAL_MODE_FRONT); - add_child(hbc); + add_child(hbc, false, INTERNAL_MODE_FRONT); hbc->add_spacer(); ok = memnew(Button); diff --git a/scene/gui/dialogs.h b/scene/gui/dialogs.h index b072055d49..8e803a2a7c 100644 --- a/scene/gui/dialogs.h +++ b/scene/gui/dialogs.h @@ -69,7 +69,7 @@ protected: virtual void custom_action(const String &) {} // Not private since used by derived classes signal. - void _text_entered(const String &p_text); + void _text_submitted(const String &p_text); void _ok_pressed(); void _cancel_pressed(); @@ -77,11 +77,12 @@ public: Label *get_label() { return label; } static void set_swap_cancel_ok(bool p_swap); - void register_text_enter(Node *p_line_edit); + void register_text_enter(Control *p_line_edit); Button *get_ok_button() { return ok; } Button *add_button(const String &p_text, bool p_right = false, const String &p_action = ""); Button *add_cancel_button(const String &p_cancel = ""); + void remove_button(Control *p_button); void set_hide_on_ok(bool p_hide); bool get_hide_on_ok() const; diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 806039d7ac..973b72973d 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -49,9 +49,9 @@ VBoxContainer *FileDialog::get_vbox() { } void FileDialog::_theme_changed() { - Color font_color = vbox->get_theme_color("font_color", "Button"); - Color font_hover_color = vbox->get_theme_color("font_hover_color", "Button"); - Color font_pressed_color = vbox->get_theme_color("font_pressed_color", "Button"); + Color font_color = vbox->get_theme_color(SNAME("font_color"), SNAME("Button")); + Color font_hover_color = vbox->get_theme_color(SNAME("font_hover_color"), SNAME("Button")); + Color font_pressed_color = vbox->get_theme_color(SNAME("font_pressed_color"), SNAME("Button")); dir_up->add_theme_color_override("icon_normal_color", font_color); dir_up->add_theme_color_override("icon_hover_color", font_hover_color); @@ -81,21 +81,21 @@ void FileDialog::_notification(int p_what) { } } if (p_what == NOTIFICATION_ENTER_TREE) { - dir_up->set_icon(vbox->get_theme_icon("parent_folder", "FileDialog")); + dir_up->set_icon(vbox->get_theme_icon(SNAME("parent_folder"), SNAME("FileDialog"))); if (vbox->is_layout_rtl()) { - dir_prev->set_icon(vbox->get_theme_icon("forward_folder", "FileDialog")); - dir_next->set_icon(vbox->get_theme_icon("back_folder", "FileDialog")); + dir_prev->set_icon(vbox->get_theme_icon(SNAME("forward_folder"), SNAME("FileDialog"))); + dir_next->set_icon(vbox->get_theme_icon(SNAME("back_folder"), SNAME("FileDialog"))); } else { - dir_prev->set_icon(vbox->get_theme_icon("back_folder", "FileDialog")); - dir_next->set_icon(vbox->get_theme_icon("forward_folder", "FileDialog")); + dir_prev->set_icon(vbox->get_theme_icon(SNAME("back_folder"), SNAME("FileDialog"))); + dir_next->set_icon(vbox->get_theme_icon(SNAME("forward_folder"), SNAME("FileDialog"))); } - refresh->set_icon(vbox->get_theme_icon("reload", "FileDialog")); - show_hidden->set_icon(vbox->get_theme_icon("toggle_hidden", "FileDialog")); + refresh->set_icon(vbox->get_theme_icon(SNAME("reload"), SNAME("FileDialog"))); + show_hidden->set_icon(vbox->get_theme_icon(SNAME("toggle_hidden"), SNAME("FileDialog"))); _theme_changed(); } } -void FileDialog::_unhandled_input(const Ref<InputEvent> &p_event) { +void FileDialog::unhandled_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventKey> k = p_event; @@ -116,7 +116,7 @@ void FileDialog::_unhandled_input(const Ref<InputEvent> &p_event) { invalidate(); } break; case KEY_BACKSPACE: { - _dir_entered(".."); + _dir_submitted(".."); } break; default: { handled = false; @@ -156,7 +156,7 @@ void FileDialog::update_dir() { deselect_all(); } -void FileDialog::_dir_entered(String p_dir) { +void FileDialog::_dir_submitted(String p_dir) { dir_access->change_dir(p_dir); file->set_text(""); invalidate(); @@ -164,13 +164,13 @@ void FileDialog::_dir_entered(String p_dir) { _push_history(); } -void FileDialog::_file_entered(const String &p_file) { +void FileDialog::_file_submitted(const String &p_file) { _action_pressed(); } void FileDialog::_save_confirm_pressed() { String f = dir_access->get_current_dir().plus_file(file->get_text()); - emit_signal("file_selected", f); + emit_signal(SNAME("file_selected"), f); hide(); } @@ -224,7 +224,7 @@ void FileDialog::_action_pressed() { } if (files.size()) { - emit_signal("files_selected", files); + emit_signal(SNAME("files_selected"), files); hide(); } @@ -234,7 +234,7 @@ void FileDialog::_action_pressed() { String f = dir_access->get_current_dir().plus_file(file->get_text()); if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && dir_access->file_exists(f)) { - emit_signal("file_selected", f); + emit_signal(SNAME("file_selected"), f); hide(); } else if (mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_DIR) { String path = dir_access->get_current_dir(); @@ -248,7 +248,7 @@ void FileDialog::_action_pressed() { } } - emit_signal("dir_selected", path); + emit_signal(SNAME("dir_selected"), path); hide(); } @@ -308,7 +308,7 @@ void FileDialog::_action_pressed() { confirm_save->set_text(TTRC("File exists, overwrite?")); confirm_save->popup_centered(Size2(200, 80)); } else { - emit_signal("file_selected", f); + emit_signal(SNAME("file_selected"), f); hide(); } } @@ -437,8 +437,8 @@ void FileDialog::_tree_item_activated() { if (mode == FILE_MODE_OPEN_FILE || mode == FILE_MODE_OPEN_FILES || mode == FILE_MODE_OPEN_DIR || mode == FILE_MODE_OPEN_ANY) { file->set_text(""); } - call_deferred("_update_file_list"); - call_deferred("_update_dir"); + call_deferred(SNAME("_update_file_list")); + call_deferred(SNAME("_update_dir")); _push_history(); } else { _action_pressed(); @@ -468,10 +468,10 @@ void FileDialog::update_file_list() { dir_access->list_dir_begin(); TreeItem *root = tree->create_item(); - Ref<Texture2D> folder = vbox->get_theme_icon("folder", "FileDialog"); - Ref<Texture2D> file_icon = vbox->get_theme_icon("file", "FileDialog"); - const Color folder_color = vbox->get_theme_color("folder_icon_modulate", "FileDialog"); - const Color file_color = vbox->get_theme_color("file_icon_modulate", "FileDialog"); + Ref<Texture2D> folder = vbox->get_theme_icon(SNAME("folder"), SNAME("FileDialog")); + Ref<Texture2D> file_icon = vbox->get_theme_icon(SNAME("file"), SNAME("FileDialog")); + const Color folder_color = vbox->get_theme_color(SNAME("folder_icon_modulate"), SNAME("FileDialog")); + const Color file_color = vbox->get_theme_color(SNAME("file_icon_modulate"), SNAME("FileDialog")); List<String> files; List<String> dirs; @@ -552,9 +552,9 @@ void FileDialog::update_file_list() { bool match = patterns.is_empty(); String match_str; - for (List<String>::Element *E = patterns.front(); E; E = E->next()) { - if (files.front()->get().matchn(E->get())) { - match_str = E->get(); + for (const String &E : patterns) { + if (files.front()->get().matchn(E)) { + match_str = E; match = true; break; } @@ -573,7 +573,7 @@ void FileDialog::update_file_list() { ti->set_icon_modulate(0, file_color); if (mode == FILE_MODE_OPEN_DIR) { - ti->set_custom_color(0, vbox->get_theme_color("files_disabled", "FileDialog")); + ti->set_custom_color(0, vbox->get_theme_color(SNAME("files_disabled"), SNAME("FileDialog"))); ti->set_selectable(0, false); } Dictionary d; @@ -854,8 +854,6 @@ void FileDialog::_update_drives() { bool FileDialog::default_show_hidden_files = false; void FileDialog::_bind_methods() { - ClassDB::bind_method(D_METHOD("_unhandled_input"), &FileDialog::_unhandled_input); - ClassDB::bind_method(D_METHOD("_cancel_pressed"), &FileDialog::_cancel_pressed); ClassDB::bind_method(D_METHOD("clear_filters"), &FileDialog::clear_filters); @@ -926,7 +924,7 @@ FileDialog::FileDialog() { show_hidden_files = default_show_hidden_files; vbox = memnew(VBoxContainer); - add_child(vbox); + add_child(vbox, false, INTERNAL_MODE_FRONT); vbox->connect("theme_changed", callable_mp(this, &FileDialog::_theme_changed)); mode = FILE_MODE_SAVE_FILE; @@ -1020,13 +1018,12 @@ FileDialog::FileDialog() { tree->connect("cell_selected", callable_mp(this, &FileDialog::_tree_selected), varray(), CONNECT_DEFERRED); tree->connect("item_activated", callable_mp(this, &FileDialog::_tree_item_activated), varray()); tree->connect("nothing_selected", callable_mp(this, &FileDialog::deselect_all)); - dir->connect("text_entered", callable_mp(this, &FileDialog::_dir_entered)); - file->connect("text_entered", callable_mp(this, &FileDialog::_file_entered)); + dir->connect("text_submitted", callable_mp(this, &FileDialog::_dir_submitted)); + file->connect("text_submitted", callable_mp(this, &FileDialog::_file_submitted)); filter->connect("item_selected", callable_mp(this, &FileDialog::_filter_selected)); confirm_save = memnew(ConfirmationDialog); - // confirm_save->set_as_top_level(true); - add_child(confirm_save); + add_child(confirm_save, false, INTERNAL_MODE_FRONT); confirm_save->connect("confirmed", callable_mp(this, &FileDialog::_save_confirm_pressed)); @@ -1038,16 +1035,16 @@ FileDialog::FileDialog() { makedirname = memnew(LineEdit); makedirname->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); makevb->add_margin_child(TTRC("Name:"), makedirname); - add_child(makedialog); + add_child(makedialog, false, INTERNAL_MODE_FRONT); makedialog->register_text_enter(makedirname); makedialog->connect("confirmed", callable_mp(this, &FileDialog::_make_dir_confirm)); mkdirerr = memnew(AcceptDialog); mkdirerr->set_text(TTRC("Could not create folder.")); - add_child(mkdirerr); + add_child(mkdirerr, false, INTERNAL_MODE_FRONT); exterr = memnew(AcceptDialog); exterr->set_text(TTRC("Must use a valid extension.")); - add_child(exterr); + add_child(exterr, false, INTERNAL_MODE_FRONT); update_filters(); update_dir(); diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index 4996f00cb3..b5190bdab1 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -32,7 +32,7 @@ #define FILE_DIALOG_H #include "box_container.h" -#include "core/os/dir_access.h" +#include "core/io/dir_access.h" #include "scene/gui/dialogs.h" #include "scene/gui/line_edit.h" #include "scene/gui/option_button.h" @@ -118,8 +118,8 @@ private: void _select_drive(int p_idx); void _tree_item_activated(); - void _dir_entered(String p_dir); - void _file_entered(const String &p_file); + void _dir_submitted(String p_dir); + void _file_submitted(const String &p_file); void _action_pressed(); void _save_confirm_pressed(); void _cancel_pressed(); @@ -132,7 +132,7 @@ private: void _update_drives(); - void _unhandled_input(const Ref<InputEvent> &p_event); + virtual void unhandled_input(const Ref<InputEvent> &p_event) override; bool _is_open_should_be_disabled(); diff --git a/scene/gui/gradient_edit.cpp b/scene/gui/gradient_edit.cpp index 7278ca6e94..56b8a936e1 100644 --- a/scene/gui/gradient_edit.cpp +++ b/scene/gui/gradient_edit.cpp @@ -48,10 +48,7 @@ GradientEdit::GradientEdit() { picker = memnew(ColorPicker); popup->add_child(picker); - add_child(popup); - - checker = Ref<ImageTexture>(memnew(ImageTexture)); - Ref<Image> img = memnew(Image(checker_bg_png)); + add_child(popup, false, INTERNAL_MODE_FRONT); } int GradientEdit::_get_point_from_pos(int x) { @@ -91,7 +88,7 @@ void GradientEdit::_show_color_picker() { GradientEdit::~GradientEdit() { } -void GradientEdit::_gui_input(const Ref<InputEvent> &p_event) { +void GradientEdit::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventKey> k = p_event; @@ -101,7 +98,7 @@ void GradientEdit::_gui_input(const Ref<InputEvent> &p_event) { grabbed = -1; grabbing = false; update(); - emit_signal("ramp_changed"); + emit_signal(SNAME("ramp_changed")); accept_event(); } @@ -121,7 +118,7 @@ void GradientEdit::_gui_input(const Ref<InputEvent> &p_event) { grabbed = -1; grabbing = false; update(); - emit_signal("ramp_changed"); + emit_signal(SNAME("ramp_changed")); accept_event(); } } @@ -145,7 +142,7 @@ void GradientEdit::_gui_input(const Ref<InputEvent> &p_event) { } } - emit_signal("ramp_changed"); + emit_signal(SNAME("ramp_changed")); update(); } } @@ -214,13 +211,13 @@ void GradientEdit::_gui_input(const Ref<InputEvent> &p_event) { } } - emit_signal("ramp_changed"); + emit_signal(SNAME("ramp_changed")); } if (mb.is_valid() && mb->get_button_index() == 1 && !mb->is_pressed()) { if (grabbing) { grabbing = false; - emit_signal("ramp_changed"); + emit_signal(SNAME("ramp_changed")); } update(); } @@ -288,7 +285,7 @@ void GradientEdit::_gui_input(const Ref<InputEvent> &p_event) { } } - emit_signal("ramp_changed"); + emit_signal(SNAME("ramp_changed")); update(); } @@ -311,7 +308,7 @@ void GradientEdit::_notification(int p_what) { int total_w = get_size().width - get_size().height - SPACING; //Draw checker pattern for ramp - _draw_checker(0, 0, total_w, h); + draw_texture_rect(get_theme_icon(SNAME("GuiMiniCheckerboard"), SNAME("EditorIcons")), Rect2(0, 0, total_w, h), true); //Draw color ramp Gradient::Point prev; @@ -378,7 +375,7 @@ void GradientEdit::_notification(int p_what) { } //Draw "button" for color selector - _draw_checker(total_w + SPACING, 0, h, h); + draw_texture_rect(get_theme_icon(SNAME("GuiMiniCheckerboard"), SNAME("EditorIcons")), Rect2(total_w + SPACING, 0, h, h), true); if (grabbed != -1) { //Draw with selection color draw_rect(Rect2(total_w + SPACING, 0, h, h), points[grabbed].color); @@ -405,27 +402,6 @@ void GradientEdit::_notification(int p_what) { } } -void GradientEdit::_draw_checker(int x, int y, int w, int h) { - //Draw it with polygon to insert UVs for scale - Vector<Vector2> backPoints; - backPoints.push_back(Vector2(x, y)); - backPoints.push_back(Vector2(x, y + h)); - backPoints.push_back(Vector2(x + w, y + h)); - backPoints.push_back(Vector2(x + w, y)); - Vector<Color> colorPoints; - colorPoints.push_back(Color(1, 1, 1, 1)); - colorPoints.push_back(Color(1, 1, 1, 1)); - colorPoints.push_back(Color(1, 1, 1, 1)); - colorPoints.push_back(Color(1, 1, 1, 1)); - Vector<Vector2> uvPoints; - //Draw checker pattern pixel-perfect and scale it by 2. - uvPoints.push_back(Vector2(x, y)); - uvPoints.push_back(Vector2(x, y + h * .5f / checker->get_height())); - uvPoints.push_back(Vector2(x + w * .5f / checker->get_width(), y + h * .5f / checker->get_height())); - uvPoints.push_back(Vector2(x + w * .5f / checker->get_width(), y)); - draw_polygon(backPoints, colorPoints, uvPoints, checker); -} - Size2 GradientEdit::get_minimum_size() const { return Vector2(0, 16); } @@ -436,10 +412,10 @@ void GradientEdit::_color_changed(const Color &p_color) { } points.write[grabbed].color = p_color; update(); - emit_signal("ramp_changed"); + emit_signal(SNAME("ramp_changed")); } -void GradientEdit::set_ramp(const Vector<float> &p_offsets, const Vector<Color> &p_colors) { +void GradientEdit::set_ramp(const Vector<real_t> &p_offsets, const Vector<Color> &p_colors) { ERR_FAIL_COND(p_offsets.size() != p_colors.size()); points.clear(); for (int i = 0; i < p_offsets.size(); i++) { @@ -453,8 +429,8 @@ void GradientEdit::set_ramp(const Vector<float> &p_offsets, const Vector<Color> update(); } -Vector<float> GradientEdit::get_offsets() const { - Vector<float> ret; +Vector<real_t> GradientEdit::get_offsets() const { + Vector<real_t> ret; for (int i = 0; i < points.size(); i++) { ret.push_back(points[i].offset); } @@ -482,6 +458,5 @@ Vector<Gradient::Point> &GradientEdit::get_points() { } void GradientEdit::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &GradientEdit::_gui_input); ADD_SIGNAL(MethodInfo("ramp_changed")); } diff --git a/scene/gui/gradient_edit.h b/scene/gui/gradient_edit.h index eb7367d598..a173631963 100644 --- a/scene/gui/gradient_edit.h +++ b/scene/gui/gradient_edit.h @@ -42,8 +42,6 @@ class GradientEdit : public Control { PopupPanel *popup; ColorPicker *picker; - Ref<ImageTexture> checker; - bool grabbing = false; int grabbed = -1; Vector<Gradient::Point> points; @@ -54,13 +52,13 @@ class GradientEdit : public Control { void _show_color_picker(); protected: - void _gui_input(const Ref<InputEvent> &p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; void _notification(int p_what); static void _bind_methods(); public: - void set_ramp(const Vector<float> &p_offsets, const Vector<Color> &p_colors); - Vector<float> get_offsets() const; + void set_ramp(const Vector<real_t> &p_offsets, const Vector<Color> &p_colors); + Vector<real_t> get_offsets() const; Vector<Color> get_colors() const; void set_points(Vector<Gradient::Point> &p_points); Vector<Gradient::Point> &get_points(); diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index 5a4dacd897..b9b02b1427 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -40,13 +40,8 @@ #include "editor/editor_scale.h" #endif -#define ZOOM_SCALE 1.2 - -#define MIN_ZOOM (((1 / ZOOM_SCALE) / ZOOM_SCALE) / ZOOM_SCALE) -#define MAX_ZOOM (1 * ZOOM_SCALE * ZOOM_SCALE * ZOOM_SCALE) - -#define MINIMAP_OFFSET 12 -#define MINIMAP_PADDING 5 +constexpr int MINIMAP_OFFSET = 12; +constexpr int MINIMAP_PADDING = 5; bool GraphEditFilter::has_point(const Point2 &p_point) const { return ge->_filter_input(p_point); @@ -56,10 +51,6 @@ GraphEditFilter::GraphEditFilter(GraphEdit *p_edit) { ge = p_edit; } -void GraphEditMinimap::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &GraphEditMinimap::_gui_input); -} - GraphEditMinimap::GraphEditMinimap(GraphEdit *p_edit) { ge = p_edit; @@ -153,7 +144,7 @@ Vector2 GraphEditMinimap::_convert_to_graph_position(const Vector2 &p_position) return graph_position; } -void GraphEditMinimap::_gui_input(const Ref<InputEvent> &p_ev) { +void GraphEditMinimap::gui_input(const Ref<InputEvent> &p_ev) { ERR_FAIL_COND(p_ev.is_null()); if (!ge->is_minimap_enabled()) { @@ -167,7 +158,7 @@ void GraphEditMinimap::_gui_input(const Ref<InputEvent> &p_ev) { if (mb->is_pressed()) { is_pressing = true; - Ref<Texture2D> resizer = get_theme_icon("resizer"); + Ref<Texture2D> resizer = get_theme_icon(SNAME("resizer")); Rect2 resizer_hitbox = Rect2(Point2(), resizer->get_size()); if (resizer_hitbox.has_point(mb->get_position())) { is_resizing = true; @@ -222,8 +213,8 @@ Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const S } bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { - for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { - if (E->get().from == p_from && E->get().from_port == p_from_port && E->get().to == p_to && E->get().to_port == p_to_port) { + for (const Connection &E : connections) { + if (E.from == p_from && E.from_port == p_from_port && E.to == p_to && E.to_port == p_to_port) { return true; } } @@ -232,7 +223,7 @@ bool GraphEdit::is_node_connected(const StringName &p_from, int p_from_port, con } void GraphEdit::disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { - for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { + for (const List<Connection>::Element *E = connections.front(); E; E = E->next()) { if (E->get().from == p_from && E->get().from_port == p_from_port && E->get().to == p_to && E->get().to_port == p_to_port) { connections.erase(E); top_layer->update(); @@ -244,10 +235,6 @@ void GraphEdit::disconnect_node(const StringName &p_from, int p_from_port, const } } -bool GraphEdit::clips_input() const { - return true; -} - void GraphEdit::get_connection_list(List<Connection> *r_connections) const { *r_connections = connections; } @@ -266,7 +253,7 @@ Vector2 GraphEdit::get_scroll_ofs() const { void GraphEdit::_scroll_moved(double) { if (!awaiting_scroll_offset_update) { - call_deferred("_update_scroll_offset"); + call_deferred(SNAME("_update_scroll_offset")); awaiting_scroll_offset_update = true; } top_layer->update(); @@ -274,7 +261,7 @@ void GraphEdit::_scroll_moved(double) { update(); if (!setting_scroll_ofs) { //in godot, signals on change value are avoided as a convention - emit_signal("scroll_offset_changed", get_scroll_ofs()); + emit_signal(SNAME("scroll_offset_changed"), get_scroll_ofs()); } } @@ -354,7 +341,7 @@ void GraphEdit::_update_scroll() { set_block_minimum_size_adjust(false); if (!awaiting_scroll_offset_update) { - call_deferred("_update_scroll_offset"); + call_deferred(SNAME("_update_scroll_offset")); awaiting_scroll_offset_update = true; } @@ -369,18 +356,7 @@ void GraphEdit::_graph_node_raised(Node *p_gn) { } else { gn->raise(); } - int first_not_comment = 0; - for (int i = 0; i < get_child_count(); i++) { - GraphNode *gn2 = Object::cast_to<GraphNode>(get_child(i)); - if (gn2 && !gn2->is_comment()) { - first_not_comment = i; - break; - } - } - - move_child(connections_layer, first_not_comment); - top_layer->raise(); - emit_signal("node_selected", p_gn); + emit_signal(SNAME("node_selected"), p_gn); } void GraphEdit::_graph_node_moved(Node *p_gn) { @@ -404,7 +380,7 @@ void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_gn) { void GraphEdit::add_child_notify(Node *p_child) { Control::add_child_notify(p_child); - top_layer->call_deferred("raise"); // Top layer always on top! + top_layer->call_deferred(SNAME("raise")); // Top layer always on top! GraphNode *gn = Object::cast_to<GraphNode>(p_child); if (gn) { @@ -430,7 +406,7 @@ void GraphEdit::remove_child_notify(Node *p_child) { } if (top_layer != nullptr && is_inside_tree()) { - top_layer->call_deferred("raise"); // Top layer always on top! + top_layer->call_deferred(SNAME("raise")); // Top layer always on top! } GraphNode *gn = Object::cast_to<GraphNode>(p_child); @@ -451,14 +427,15 @@ void GraphEdit::remove_child_notify(Node *p_child) { void GraphEdit::_notification(int p_what) { if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED) { - port_grab_distance_horizontal = get_theme_constant("port_grab_distance_horizontal"); - port_grab_distance_vertical = get_theme_constant("port_grab_distance_vertical"); - - zoom_minus->set_icon(get_theme_icon("minus")); - zoom_reset->set_icon(get_theme_icon("reset")); - zoom_plus->set_icon(get_theme_icon("more")); - snap_button->set_icon(get_theme_icon("snap")); - minimap_button->set_icon(get_theme_icon("minimap")); + port_grab_distance_horizontal = get_theme_constant(SNAME("port_grab_distance_horizontal")); + port_grab_distance_vertical = get_theme_constant(SNAME("port_grab_distance_vertical")); + + zoom_minus->set_icon(get_theme_icon(SNAME("minus"))); + zoom_reset->set_icon(get_theme_icon(SNAME("reset"))); + zoom_plus->set_icon(get_theme_icon(SNAME("more"))); + snap_button->set_icon(get_theme_icon(SNAME("snap"))); + minimap_button->set_icon(get_theme_icon(SNAME("minimap"))); + layout_button->set_icon(get_theme_icon(SNAME("layout"))); } if (p_what == NOTIFICATION_READY) { Size2 hmin = h_scroll->get_combined_minimum_size(); @@ -475,7 +452,7 @@ void GraphEdit::_notification(int p_what) { v_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); } if (p_what == NOTIFICATION_DRAW) { - draw_style_box(get_theme_stylebox("bg"), Rect2(Point2(), get_size())); + draw_style_box(get_theme_stylebox(SNAME("bg")), Rect2(Point2(), get_size())); if (is_using_snap()) { //draw grid @@ -488,8 +465,8 @@ void GraphEdit::_notification(int p_what) { Point2i from = (offset / float(snap)).floor(); Point2i len = (size / float(snap)).floor() + Vector2(1, 1); - Color grid_minor = get_theme_color("grid_minor"); - Color grid_major = get_theme_color("grid_major"); + Color grid_minor = get_theme_color(SNAME("grid_minor")); + Color grid_major = get_theme_color(SNAME("grid_major")); for (int i = from.x; i < from.x + len.x; i++) { Color color; @@ -527,7 +504,8 @@ void GraphEdit::_notification(int p_what) { } bool GraphEdit::_filter_input(const Point2 &p_point) { - Ref<Texture2D> port = get_theme_icon("port", "GraphNode"); + Ref<Texture2D> port = get_theme_icon(SNAME("port"), SNAME("GraphNode")); + Vector2i port_size = Vector2i(port->get_width(), port->get_height()); for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); @@ -537,14 +515,14 @@ bool GraphEdit::_filter_input(const Point2 &p_point) { for (int j = 0; j < gn->get_connection_output_count(); j++) { Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); - if (is_in_hot_zone(pos / zoom, p_point / zoom)) { + if (is_in_hot_zone(pos / zoom, p_point / zoom, port_size, false)) { return true; } } for (int j = 0; j < gn->get_connection_input_count(); j++) { Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); - if (is_in_hot_zone(pos / zoom, p_point / zoom)) { + if (is_in_hot_zone(pos / zoom, p_point / zoom, port_size, true)) { return true; } } @@ -556,8 +534,10 @@ bool GraphEdit::_filter_input(const Point2 &p_point) { void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { Ref<InputEventMouseButton> mb = p_ev; if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_LEFT && mb->is_pressed()) { + Ref<Texture2D> port = get_theme_icon(SNAME("port"), SNAME("GraphNode")); + Vector2i port_size = Vector2i(port->get_width(), port->get_height()); + connecting_valid = false; - Ref<Texture2D> port = get_theme_icon("port", "GraphNode"); click_pos = mb->get_position() / zoom; for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); @@ -567,23 +547,23 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_output_count(); j++) { Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); - if (is_in_hot_zone(pos / zoom, click_pos)) { + if (is_in_hot_zone(pos / zoom, click_pos, port_size, false)) { if (valid_left_disconnect_types.has(gn->get_connection_output_type(j))) { //check disconnect - for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { - if (E->get().from == gn->get_name() && E->get().from_port == j) { - Node *to = get_node(String(E->get().to)); + for (const Connection &E : connections) { + if (E.from == gn->get_name() && E.from_port == j) { + Node *to = get_node(String(E.to)); if (Object::cast_to<GraphNode>(to)) { - connecting_from = E->get().to; - connecting_index = E->get().to_port; + connecting_from = E.to; + connecting_index = E.to_port; connecting_out = false; - connecting_type = Object::cast_to<GraphNode>(to)->get_connection_input_type(E->get().to_port); - connecting_color = Object::cast_to<GraphNode>(to)->get_connection_input_color(E->get().to_port); + connecting_type = Object::cast_to<GraphNode>(to)->get_connection_input_type(E.to_port); + connecting_color = Object::cast_to<GraphNode>(to)->get_connection_input_color(E.to_port); connecting_target = false; connecting_to = pos; just_disconnected = true; - emit_signal("disconnection_request", E->get().from, E->get().from_port, E->get().to, E->get().to_port); + emit_signal(SNAME("disconnection_request"), E.from, E.from_port, E.to, E.to_port); to = get_node(String(connecting_from)); //maybe it was erased if (Object::cast_to<GraphNode>(to)) { connecting = true; @@ -609,23 +589,23 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_input_count(); j++) { Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); - if (is_in_hot_zone(pos / zoom, click_pos)) { + if (is_in_hot_zone(pos / zoom, click_pos, port_size, true)) { if (right_disconnects || valid_right_disconnect_types.has(gn->get_connection_input_type(j))) { //check disconnect - for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { - if (E->get().to == gn->get_name() && E->get().to_port == j) { - Node *fr = get_node(String(E->get().from)); + for (const Connection &E : connections) { + if (E.to == gn->get_name() && E.to_port == j) { + Node *fr = get_node(String(E.from)); if (Object::cast_to<GraphNode>(fr)) { - connecting_from = E->get().from; - connecting_index = E->get().from_port; + connecting_from = E.from; + connecting_index = E.from_port; connecting_out = true; - connecting_type = Object::cast_to<GraphNode>(fr)->get_connection_output_type(E->get().from_port); - connecting_color = Object::cast_to<GraphNode>(fr)->get_connection_output_color(E->get().from_port); + connecting_type = Object::cast_to<GraphNode>(fr)->get_connection_output_type(E.from_port); + connecting_color = Object::cast_to<GraphNode>(fr)->get_connection_output_color(E.from_port); connecting_target = false; connecting_to = pos; just_disconnected = true; - emit_signal("disconnection_request", E->get().from, E->get().from_port, E->get().to, E->get().to_port); + emit_signal(SNAME("disconnection_request"), E.from, E.from_port, E.to, E.to_port); fr = get_node(String(connecting_from)); //maybe it was erased if (Object::cast_to<GraphNode>(fr)) { connecting = true; @@ -658,10 +638,12 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting_target = false; top_layer->update(); minimap->update(); - connecting_valid = just_disconnected || click_pos.distance_to(connecting_to / zoom) > 20.0 * zoom; + connecting_valid = just_disconnected || click_pos.distance_to(connecting_to / zoom) > 20.0; if (connecting_valid) { - Ref<Texture2D> port = get_theme_icon("port", "GraphNode"); + Ref<Texture2D> port = get_theme_icon(SNAME("port"), SNAME("GraphNode")); + Vector2i port_size = Vector2i(port->get_width(), port->get_height()); + Vector2 mpos = mm->get_position() / zoom; for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); @@ -673,7 +655,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_output_count(); j++) { Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); int type = gn->get_connection_output_type(j); - if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos / zoom, mpos)) { + if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos / zoom, mpos, port_size, false)) { connecting_target = true; connecting_to = pos; connecting_target_to = gn->get_name(); @@ -685,7 +667,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_input_count(); j++) { Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); int type = gn->get_connection_input_type(j); - if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos / zoom, mpos)) { + if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos / zoom, mpos, port_size, true)) { connecting_target = true; connecting_to = pos; connecting_target_to = gn->get_name(); @@ -710,7 +692,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { SWAP(from, to); SWAP(from_slot, to_slot); } - emit_signal("connection_request", from, from_slot, to, to_slot); + emit_signal(SNAME("connection_request"), from, from_slot, to, to_slot); } else if (!just_disconnected) { String from = connecting_from; @@ -718,9 +700,9 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { Vector2 ofs = Vector2(mb->get_position().x, mb->get_position().y); if (!connecting_out) { - emit_signal("connection_from_empty", from, from_slot, ofs); + emit_signal(SNAME("connection_from_empty"), from, from_slot, ofs); } else { - emit_signal("connection_to_empty", from, from_slot, ofs); + emit_signal(SNAME("connection_to_empty"), from, from_slot, ofs); } } } @@ -756,9 +738,25 @@ bool GraphEdit::_check_clickable_control(Control *p_control, const Vector2 &pos) } } -bool GraphEdit::is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos) { - if (!Rect2(pos.x - port_grab_distance_horizontal, pos.y - port_grab_distance_vertical, port_grab_distance_horizontal * 2, port_grab_distance_vertical * 2).has_point(p_mouse_pos)) { - return false; +bool GraphEdit::is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left) { + if (p_left) { + if (!Rect2( + pos.x - p_port_size.x / 2 - port_grab_distance_horizontal, + pos.y - p_port_size.y / 2 - port_grab_distance_vertical / 2, + p_port_size.x + port_grab_distance_horizontal, + p_port_size.y + port_grab_distance_vertical) + .has_point(p_mouse_pos)) { + return false; + } + } else { + if (!Rect2( + pos.x - p_port_size.x / 2, + pos.y - p_port_size.y / 2 - port_grab_distance_vertical / 2, + p_port_size.x + port_grab_distance_horizontal, + p_port_size.y + port_grab_distance_vertical) + .has_point(p_mouse_pos)) { + return false; + } } for (int i = 0; i < get_child_count(); i++) { @@ -792,73 +790,41 @@ bool GraphEdit::is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos) { return true; } -template <class Vector2> -static _FORCE_INLINE_ Vector2 _bezier_interp(real_t t, Vector2 start, Vector2 control_1, Vector2 control_2, Vector2 end) { - /* Formula from Wikipedia article on Bezier curves. */ - real_t omt = (1.0 - t); - real_t omt2 = omt * omt; - real_t omt3 = omt2 * omt; - real_t t2 = t * t; - real_t t3 = t2 * t; - - return start * omt3 + control_1 * omt2 * t * 3.0 + control_2 * omt * t2 * 3.0 + end * t3; -} - -void GraphEdit::_bake_segment2d(Vector<Vector2> &points, Vector<Color> &colors, float p_begin, float p_end, const Vector2 &p_a, const Vector2 &p_out, const Vector2 &p_b, const Vector2 &p_in, int p_depth, int p_min_depth, int p_max_depth, float p_tol, const Color &p_color, const Color &p_to_color, int &lines) const { - float mp = p_begin + (p_end - p_begin) * 0.5; - Vector2 beg = _bezier_interp(p_begin, p_a, p_a + p_out, p_b + p_in, p_b); - Vector2 mid = _bezier_interp(mp, p_a, p_a + p_out, p_b + p_in, p_b); - Vector2 end = _bezier_interp(p_end, p_a, p_a + p_out, p_b + p_in, p_b); - - Vector2 na = (mid - beg).normalized(); - Vector2 nb = (end - mid).normalized(); - float dp = Math::rad2deg(Math::acos(na.dot(nb))); - - if (p_depth >= p_min_depth && (dp < p_tol || p_depth >= p_max_depth)) { - points.push_back((beg + end) * 0.5); - colors.push_back(p_color.lerp(p_to_color, mp)); - lines++; - } else { - _bake_segment2d(points, colors, p_begin, mp, p_a, p_out, p_b, p_in, p_depth + 1, p_min_depth, p_max_depth, p_tol, p_color, p_to_color, lines); - _bake_segment2d(points, colors, mp, p_end, p_a, p_out, p_b, p_in, p_depth + 1, p_min_depth, p_max_depth, p_tol, p_color, p_to_color, lines); +PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const Vector2 &p_to) { + Vector<Vector2> ret; + if (GDVIRTUAL_CALL(_get_connection_line, p_from, p_to, ret)) { + return ret; } -} - -void GraphEdit::_draw_cos_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_bezier_ratio = 1.0) { - //cubic bezier code - float diff = p_to.x - p_from.x; - float cp_offset; - int cp_len = get_theme_constant("bezier_len_pos") * p_bezier_ratio; - int cp_neg_len = get_theme_constant("bezier_len_neg") * p_bezier_ratio; - if (diff > 0) { - cp_offset = MIN(cp_len, diff * 0.5); - } else { - cp_offset = MAX(MIN(cp_len - diff, cp_neg_len), -diff * 0.5); - } - - Vector2 c1 = Vector2(cp_offset * zoom, 0); - Vector2 c2 = Vector2(-cp_offset * zoom, 0); - - int lines = 0; + Curve2D curve; + Vector<Color> colors; + curve.add_point(p_from); + curve.set_point_out(0, Vector2(60, 0)); + curve.add_point(p_to); + curve.set_point_in(1, Vector2(-60, 0)); + return curve.tessellate(); +} - Vector<Point2> points; +void GraphEdit::_draw_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_zoom) { + Vector<Vector2> points = get_connection_line(p_from / p_zoom, p_to / p_zoom); + Vector<Vector2> scaled_points; Vector<Color> colors; - points.push_back(p_from); - colors.push_back(p_color); - _bake_segment2d(points, colors, 0, 1, p_from, c1, p_to, c2, 0, 3, 9, 3, p_color, p_to_color, lines); - points.push_back(p_to); - colors.push_back(p_to_color); + float length = (p_from / p_zoom).distance_to(p_to / p_zoom); + for (int i = 0; i < points.size(); i++) { + float d = (p_from / p_zoom).distance_to(points[i]) / length; + colors.push_back(p_color.lerp(p_to_color, d)); + scaled_points.push_back(points[i] * p_zoom); + } #ifdef TOOLS_ENABLED - p_where->draw_polyline_colors(points, colors, Math::floor(p_width * EDSCALE), lines_antialiased); + p_where->draw_polyline_colors(scaled_points, colors, Math::floor(p_width * EDSCALE), lines_antialiased); #else - p_where->draw_polyline_colors(points, colors, p_width, lines_antialiased); + p_where->draw_polyline_colors(scaled_points, colors, p_width, lines_antialiased); #endif } void GraphEdit::_connections_layer_draw() { - Color activity_color = get_theme_color("activity"); + Color activity_color = get_theme_color(SNAME("activity")); //draw connections List<List<Connection>::Element *> to_erase; for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { @@ -900,7 +866,7 @@ void GraphEdit::_connections_layer_draw() { color = color.lerp(activity_color, E->get().activity); tocolor = tocolor.lerp(activity_color, E->get().activity); } - _draw_cos_line(connections_layer, frompos, topos, color, tocolor, lines_thickness); + _draw_connection_line(connections_layer, frompos, topos, color, tocolor, lines_thickness, zoom); } while (to_erase.size()) { @@ -939,12 +905,12 @@ void GraphEdit::_top_layer_draw() { if (!connecting_out) { SWAP(pos, topos); } - _draw_cos_line(top_layer, pos, topos, col, col, lines_thickness); + _draw_connection_line(top_layer, pos, topos, col, col, lines_thickness, zoom); } if (box_selecting) { - top_layer->draw_rect(box_selecting_rect, get_theme_color("selection_fill")); - top_layer->draw_rect(box_selecting_rect, get_theme_color("selection_stroke"), false); + top_layer->draw_rect(box_selecting_rect, get_theme_color(SNAME("selection_fill"))); + top_layer->draw_rect(box_selecting_rect, get_theme_color(SNAME("selection_stroke")), false); } } @@ -957,7 +923,7 @@ void GraphEdit::_minimap_draw() { // Draw the minimap background. Rect2 minimap_rect = Rect2(Point2(), minimap->get_size()); - minimap->draw_style_box(minimap->get_theme_stylebox("bg"), minimap_rect); + minimap->draw_style_box(minimap->get_theme_stylebox(SNAME("bg")), minimap_rect); Vector2 graph_offset = minimap->_get_graph_offset(); Vector2 minimap_offset = minimap->minimap_offset; @@ -973,7 +939,7 @@ void GraphEdit::_minimap_draw() { Vector2 node_size = minimap->_convert_from_graph_position(gn->get_size() * zoom); Rect2 node_rect = Rect2(node_position, node_size); - Ref<StyleBoxFlat> sb_minimap = minimap->get_theme_stylebox("node")->duplicate(); + Ref<StyleBoxFlat> sb_minimap = minimap->get_theme_stylebox(SNAME("node"))->duplicate(); // Override default values with colors provided by the GraphNode's stylebox, if possible. Ref<StyleBoxFlat> sbf = gn->get_theme_stylebox(gn->is_selected() ? "commentfocus" : "comment"); @@ -996,7 +962,7 @@ void GraphEdit::_minimap_draw() { Vector2 node_size = minimap->_convert_from_graph_position(gn->get_size() * zoom); Rect2 node_rect = Rect2(node_position, node_size); - Ref<StyleBoxFlat> sb_minimap = minimap->get_theme_stylebox("node")->duplicate(); + Ref<StyleBoxFlat> sb_minimap = minimap->get_theme_stylebox(SNAME("node"))->duplicate(); // Override default values with colors provided by the GraphNode's stylebox, if possible. Ref<StyleBoxFlat> sbf = gn->get_theme_stylebox(gn->is_selected() ? "selectedframe" : "frame"); @@ -1009,9 +975,9 @@ void GraphEdit::_minimap_draw() { } // Draw node connections. - Color activity_color = get_theme_color("activity"); - for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { - NodePath fromnp(E->get().from); + Color activity_color = get_theme_color(SNAME("activity")); + for (const Connection &E : connections) { + NodePath fromnp(E.from); Node *from = get_node(fromnp); if (!from) { @@ -1022,7 +988,7 @@ void GraphEdit::_minimap_draw() { continue; } - NodePath tonp(E->get().to); + NodePath tonp(E.to); Node *to = get_node(tonp); if (!to) { continue; @@ -1032,27 +998,27 @@ void GraphEdit::_minimap_draw() { continue; } - Vector2 from_slot_position = gfrom->get_position_offset() * zoom + gfrom->get_connection_output_position(E->get().from_port); + Vector2 from_slot_position = gfrom->get_position_offset() * zoom + gfrom->get_connection_output_position(E.from_port); Vector2 from_position = minimap->_convert_from_graph_position(from_slot_position - graph_offset) + minimap_offset; - Color from_color = gfrom->get_connection_output_color(E->get().from_port); - Vector2 to_slot_position = gto->get_position_offset() * zoom + gto->get_connection_input_position(E->get().to_port); + Color from_color = gfrom->get_connection_output_color(E.from_port); + Vector2 to_slot_position = gto->get_position_offset() * zoom + gto->get_connection_input_position(E.to_port); Vector2 to_position = minimap->_convert_from_graph_position(to_slot_position - graph_offset) + minimap_offset; - Color to_color = gto->get_connection_input_color(E->get().to_port); + Color to_color = gto->get_connection_input_color(E.to_port); - if (E->get().activity > 0) { - from_color = from_color.lerp(activity_color, E->get().activity); - to_color = to_color.lerp(activity_color, E->get().activity); + if (E.activity > 0) { + from_color = from_color.lerp(activity_color, E.activity); + to_color = to_color.lerp(activity_color, E.activity); } - _draw_cos_line(minimap, from_position, to_position, from_color, to_color, 1.0, 0.5); + _draw_connection_line(minimap, from_position, to_position, from_color, to_color, 0.1, minimap->_convert_from_graph_position(Vector2(zoom, zoom)).length()); } // Draw the "camera" viewport. Rect2 camera_rect = minimap->get_camera_rect(); - minimap->draw_style_box(minimap->get_theme_stylebox("camera"), camera_rect); + minimap->draw_style_box(minimap->get_theme_stylebox(SNAME("camera")), camera_rect); // Draw the resizer control. - Ref<Texture2D> resizer = minimap->get_theme_icon("resizer"); - Color resizer_color = minimap->get_theme_color("resizer_color"); + Ref<Texture2D> resizer = minimap->get_theme_icon(SNAME("resizer")); + Color resizer_color = minimap->get_theme_color(SNAME("resizer_color")); minimap->draw_texture(resizer, Point2(), resizer_color); } @@ -1067,18 +1033,19 @@ void GraphEdit::set_selected(Node *p_child) { } } -void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { +void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { ERR_FAIL_COND(p_ev.is_null()); Ref<InputEventMouseMotion> mm = p_ev; if (mm.is_valid() && (mm->get_button_mask() & MOUSE_BUTTON_MASK_MIDDLE || (mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT && Input::get_singleton()->is_key_pressed(KEY_SPACE)))) { - h_scroll->set_value(h_scroll->get_value() - mm->get_relative().x); - v_scroll->set_value(v_scroll->get_value() - mm->get_relative().y); + Vector2i relative = Input::get_singleton()->warp_mouse_motion(mm, get_global_rect()); + h_scroll->set_value(h_scroll->get_value() - relative.x); + v_scroll->set_value(v_scroll->get_value() - relative.y); } if (mm.is_valid() && dragging) { if (!moving_selection) { - emit_signal("begin_node_move"); + emit_signal(SNAME("begin_node_move")); moving_selection = true; } @@ -1121,17 +1088,17 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { if (in_box) { if (!gn->is_selected() && box_selection_mode_additive) { - emit_signal("node_selected", gn); + emit_signal(SNAME("node_selected"), gn); } else if (gn->is_selected() && !box_selection_mode_additive) { - emit_signal("node_deselected", gn); + emit_signal(SNAME("node_deselected"), gn); } gn->set_selected(box_selection_mode_additive); } else { bool select = (previous_selected.find(gn) != nullptr); if (gn->is_selected() && !select) { - emit_signal("node_deselected", gn); + emit_signal(SNAME("node_deselected"), gn); } else if (!gn->is_selected() && select) { - emit_signal("node_selected", gn); + emit_signal(SNAME("node_selected"), gn); } gn->set_selected(select); } @@ -1154,9 +1121,9 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { bool select = (previous_selected.find(gn) != nullptr); if (gn->is_selected() && !select) { - emit_signal("node_deselected", gn); + emit_signal(SNAME("node_deselected"), gn); } else if (!gn->is_selected() && select) { - emit_signal("node_selected", gn); + emit_signal(SNAME("node_selected"), gn); } gn->set_selected(select); } @@ -1168,7 +1135,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { top_layer->update(); minimap->update(); } else { - emit_signal("popup_request", b->get_global_position()); + emit_signal(SNAME("popup_request"), b->get_global_position()); } } } @@ -1183,7 +1150,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { Rect2 r = gn->get_rect(); r.size *= zoom; if (r.has_point(b->get_position())) { - emit_signal("node_deselected", gn); + emit_signal(SNAME("node_deselected"), gn); gn->set_selected(false); } } @@ -1200,7 +1167,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { } if (moving_selection) { - emit_signal("end_node_move"); + emit_signal(SNAME("end_node_move")); moving_selection = false; } @@ -1246,7 +1213,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { o_gn->set_selected(true); } else { if (o_gn->is_selected()) { - emit_signal("node_deselected", o_gn); + emit_signal(SNAME("node_deselected"), o_gn); } o_gn->set_selected(false); } @@ -1306,7 +1273,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { continue; } if (gn2->is_selected()) { - emit_signal("node_deselected", gn2); + emit_signal(SNAME("node_deselected"), gn2); } gn2->set_selected(false); } @@ -1322,33 +1289,35 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { minimap->update(); } - if (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP && Input::get_singleton()->is_key_pressed(KEY_CTRL)) { - set_zoom_custom(zoom * ZOOM_SCALE, b->get_position()); - } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && Input::get_singleton()->is_key_pressed(KEY_CTRL)) { - set_zoom_custom(zoom / ZOOM_SCALE, b->get_position()); - } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP && !Input::get_singleton()->is_key_pressed(KEY_SHIFT)) { - v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() * b->get_factor() / 8); - } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && !Input::get_singleton()->is_key_pressed(KEY_SHIFT)) { - v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() * b->get_factor() / 8); - } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_RIGHT || (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && Input::get_singleton()->is_key_pressed(KEY_SHIFT))) { - h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * b->get_factor() / 8); - } else if (b->get_button_index() == MOUSE_BUTTON_WHEEL_LEFT || (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP && Input::get_singleton()->is_key_pressed(KEY_SHIFT))) { - h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() * b->get_factor() / 8); + int scroll_direction = (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN) - (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP); + if (scroll_direction != 0) { + if (b->is_ctrl_pressed()) { + if (b->is_shift_pressed()) { + // Horizontal scrolling. + h_scroll->set_value(h_scroll->get_value() + (h_scroll->get_page() * b->get_factor() / 8) * scroll_direction); + } else { + // Vertical scrolling. + v_scroll->set_value(v_scroll->get_value() + (v_scroll->get_page() * b->get_factor() / 8) * scroll_direction); + } + } else { + // Zooming. + set_zoom_custom(scroll_direction < 0 ? zoom * zoom_step : zoom / zoom_step, b->get_position()); + } } } if (p_ev->is_pressed()) { if (p_ev->is_action("ui_graph_duplicate")) { - emit_signal("duplicate_nodes_request"); + emit_signal(SNAME("duplicate_nodes_request")); accept_event(); } else if (p_ev->is_action("ui_copy")) { - emit_signal("copy_nodes_request"); + emit_signal(SNAME("copy_nodes_request")); accept_event(); } else if (p_ev->is_action("ui_paste")) { - emit_signal("paste_nodes_request"); + emit_signal(SNAME("paste_nodes_request")); accept_event(); } else if (p_ev->is_action("ui_graph_delete")) { - emit_signal("delete_nodes_request"); + emit_signal(SNAME("delete_nodes_request")); accept_event(); } } @@ -1366,15 +1335,15 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { } void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity) { - for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { - if (E->get().from == p_from && E->get().from_port == p_from_port && E->get().to == p_to && E->get().to_port == p_to_port) { - if (Math::is_equal_approx(E->get().activity, p_activity)) { + for (Connection &E : connections) { + if (E.from == p_from && E.from_port == p_from_port && E.to == p_to && E.to_port == p_to_port) { + if (Math::is_equal_approx(E.activity, p_activity)) { //update only if changed top_layer->update(); minimap->update(); connections_layer->update(); } - E->get().activity = p_activity; + E.activity = p_activity; return; } } @@ -1392,19 +1361,19 @@ void GraphEdit::set_zoom(float p_zoom) { } void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) { - p_zoom = CLAMP(p_zoom, MIN_ZOOM, MAX_ZOOM); + p_zoom = CLAMP(p_zoom, zoom_min, zoom_max); if (zoom == p_zoom) { return; } - zoom_minus->set_disabled(zoom == MIN_ZOOM); - zoom_plus->set_disabled(zoom == MAX_ZOOM); - Vector2 sbofs = (Vector2(h_scroll->get_value(), v_scroll->get_value()) + p_center) / zoom; zoom = p_zoom; top_layer->update(); + zoom_minus->set_disabled(zoom == zoom_min); + zoom_plus->set_disabled(zoom == zoom_max); + _update_scroll(); minimap->update(); connections_layer->update(); @@ -1415,6 +1384,7 @@ void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) { v_scroll->set_value(ofs.y); } + _update_zoom_label(); update(); } @@ -1422,6 +1392,61 @@ float GraphEdit::get_zoom() const { return zoom; } +void GraphEdit::set_zoom_step(float p_zoom_step) { + p_zoom_step = abs(p_zoom_step); + if (zoom_step == p_zoom_step) { + return; + } + + zoom_step = p_zoom_step; +} + +float GraphEdit::get_zoom_step() const { + return zoom_step; +} + +void GraphEdit::set_zoom_min(float p_zoom_min) { + ERR_FAIL_COND_MSG(p_zoom_min > zoom_max, "Cannot set min zoom level greater than max zoom level."); + + if (zoom_min == p_zoom_min) { + return; + } + + zoom_min = p_zoom_min; + set_zoom(zoom); +} + +float GraphEdit::get_zoom_min() const { + return zoom_min; +} + +void GraphEdit::set_zoom_max(float p_zoom_max) { + ERR_FAIL_COND_MSG(p_zoom_max < zoom_min, "Cannot set max zoom level lesser than min zoom level."); + + if (zoom_max == p_zoom_max) { + return; + } + + zoom_max = p_zoom_max; + set_zoom(zoom); +} + +float GraphEdit::get_zoom_max() const { + return zoom_max; +} + +void GraphEdit::set_show_zoom_label(bool p_enable) { + if (zoom_label->is_visible() == p_enable) { + return; + } + + zoom_label->set_visible(p_enable); +} + +bool GraphEdit::is_showing_zoom_label() const { + return zoom_label->is_visible(); +} + void GraphEdit::set_right_disconnects(bool p_enable) { right_disconnects = p_enable; } @@ -1450,19 +1475,19 @@ Array GraphEdit::_get_connection_list() const { List<Connection> conns; get_connection_list(&conns); Array arr; - for (List<Connection>::Element *E = conns.front(); E; E = E->next()) { + for (const Connection &E : conns) { Dictionary d; - d["from"] = E->get().from; - d["from_port"] = E->get().from_port; - d["to"] = E->get().to; - d["to_port"] = E->get().to_port; + d["from"] = E.from; + d["from_port"] = E.from_port; + d["to"] = E.to; + d["to_port"] = E.to_port; arr.push_back(d); } return arr; } void GraphEdit::_zoom_minus() { - set_zoom(zoom / ZOOM_SCALE); + set_zoom(zoom / zoom_step); } void GraphEdit::_zoom_reset() { @@ -1470,7 +1495,13 @@ void GraphEdit::_zoom_reset() { } void GraphEdit::_zoom_plus() { - set_zoom(zoom * ZOOM_SCALE); + set_zoom(zoom * zoom_step); +} + +void GraphEdit::_update_zoom_label() { + int zoom_percent = static_cast<int>(Math::round(zoom * 100)); + String zoom_text = itos(zoom_percent) + "%"; + zoom_label->set_text(zoom_text); } void GraphEdit::add_valid_connection_type(int p_type, int p_with_type) { @@ -1590,6 +1621,505 @@ HBoxContainer *GraphEdit::get_zoom_hbox() { return zoom_hb; } +int GraphEdit::_set_operations(SET_OPERATIONS p_operation, Set<StringName> &r_u, const Set<StringName> &r_v) { + switch (p_operation) { + case GraphEdit::IS_EQUAL: { + for (Set<StringName>::Element *E = r_u.front(); E; E = E->next()) { + if (!r_v.has(E->get())) + return 0; + } + return r_u.size() == r_v.size(); + } break; + case GraphEdit::IS_SUBSET: { + if (r_u.size() == r_v.size() && !r_u.size()) { + return 1; + } + for (Set<StringName>::Element *E = r_u.front(); E; E = E->next()) { + if (!r_v.has(E->get())) + return 0; + } + return 1; + } break; + case GraphEdit::DIFFERENCE: { + for (Set<StringName>::Element *E = r_u.front(); E; E = E->next()) { + if (r_v.has(E->get())) { + r_u.erase(E->get()); + } + } + return r_u.size(); + } break; + case GraphEdit::UNION: { + for (Set<StringName>::Element *E = r_v.front(); E; E = E->next()) { + if (!r_u.has(E->get())) { + r_u.insert(E->get()); + } + } + return r_v.size(); + } break; + default: + break; + } + return -1; +} + +HashMap<int, Vector<StringName>> GraphEdit::_layering(const Set<StringName> &r_selected_nodes, const HashMap<StringName, Set<StringName>> &r_upper_neighbours) { + HashMap<int, Vector<StringName>> l; + + Set<StringName> p = r_selected_nodes, q = r_selected_nodes, u, z; + int current_layer = 0; + bool selected = false; + + while (!_set_operations(GraphEdit::IS_EQUAL, q, u)) { + _set_operations(GraphEdit::DIFFERENCE, p, u); + for (const Set<StringName>::Element *E = p.front(); E; E = E->next()) { + Set<StringName> n = r_upper_neighbours[E->get()]; + if (_set_operations(GraphEdit::IS_SUBSET, n, z)) { + Vector<StringName> t; + t.push_back(E->get()); + if (!l.has(current_layer)) { + l.set(current_layer, Vector<StringName>{}); + } + selected = true; + t.append_array(l[current_layer]); + l.set(current_layer, t); + Set<StringName> V; + V.insert(E->get()); + _set_operations(GraphEdit::UNION, u, V); + } + } + if (!selected) { + current_layer++; + _set_operations(GraphEdit::UNION, z, u); + } + selected = false; + } + + return l; +} + +Vector<StringName> GraphEdit::_split(const Vector<StringName> &r_layer, const HashMap<StringName, Dictionary> &r_crossings) { + if (!r_layer.size()) { + return Vector<StringName>(); + } + + StringName p = r_layer[Math::random(0, r_layer.size() - 1)]; + Vector<StringName> left; + Vector<StringName> right; + + for (int i = 0; i < r_layer.size(); i++) { + if (p != r_layer[i]) { + StringName q = r_layer[i]; + int cross_pq = r_crossings[p][q]; + int cross_qp = r_crossings[q][p]; + if (cross_pq > cross_qp) { + left.push_back(q); + } else { + right.push_back(q); + } + } + } + + left.push_back(p); + left.append_array(right); + return left; +} + +void GraphEdit::_horizontal_alignment(Dictionary &r_root, Dictionary &r_align, const HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours, const Set<StringName> &r_selected_nodes) { + for (const Set<StringName>::Element *E = r_selected_nodes.front(); E; E = E->next()) { + r_root[E->get()] = E->get(); + r_align[E->get()] = E->get(); + } + + if (r_layers.size() == 1) { + return; + } + + for (unsigned int i = 1; i < r_layers.size(); i++) { + Vector<StringName> lower_layer = r_layers[i]; + Vector<StringName> upper_layer = r_layers[i - 1]; + int r = -1; + + for (int j = 0; j < lower_layer.size(); j++) { + Vector<Pair<int, StringName>> up; + StringName current_node = lower_layer[j]; + for (int k = 0; k < upper_layer.size(); k++) { + StringName adjacent_neighbour = upper_layer[k]; + if (r_upper_neighbours[current_node].has(adjacent_neighbour)) { + up.push_back(Pair<int, StringName>(k, adjacent_neighbour)); + } + } + + int start = up.size() / 2; + int end = up.size() % 2 ? start : start + 1; + for (int p = start; p <= end; p++) { + StringName Align = r_align[current_node]; + if (Align == current_node && r < up[p].first) { + r_align[up[p].second] = lower_layer[j]; + r_root[current_node] = r_root[up[p].second]; + r_align[current_node] = r_root[up[p].second]; + r = up[p].first; + } + } + } + } +} + +void GraphEdit::_crossing_minimisation(HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours) { + if (r_layers.size() == 1) { + return; + } + + for (unsigned int i = 1; i < r_layers.size(); i++) { + Vector<StringName> upper_layer = r_layers[i - 1]; + Vector<StringName> lower_layer = r_layers[i]; + HashMap<StringName, Dictionary> c; + + for (int j = 0; j < lower_layer.size(); j++) { + StringName p = lower_layer[j]; + Dictionary d; + + for (int k = 0; k < lower_layer.size(); k++) { + unsigned int crossings = 0; + StringName q = lower_layer[k]; + + if (j != k) { + for (int h = 1; h < upper_layer.size(); h++) { + if (r_upper_neighbours[p].has(upper_layer[h])) { + for (int g = 0; g < h; g++) { + if (r_upper_neighbours[q].has(upper_layer[g])) { + crossings++; + } + } + } + } + } + d[q] = crossings; + } + c.set(p, d); + } + + r_layers.set(i, _split(lower_layer, c)); + } +} + +void GraphEdit::_calculate_inner_shifts(Dictionary &r_inner_shifts, const Dictionary &r_root, const Dictionary &r_node_names, const Dictionary &r_align, const Set<StringName> &r_block_heads, const HashMap<StringName, Pair<int, int>> &r_port_info) { + for (const Set<StringName>::Element *E = r_block_heads.front(); E; E = E->next()) { + real_t left = 0; + StringName u = E->get(); + StringName v = r_align[u]; + while (u != v && (StringName)r_root[u] != v) { + String _connection = String(u) + " " + String(v); + GraphNode *gfrom = Object::cast_to<GraphNode>(r_node_names[u]); + GraphNode *gto = Object::cast_to<GraphNode>(r_node_names[v]); + + Pair<int, int> ports = r_port_info[_connection]; + int pfrom = ports.first; + int pto = ports.second; + Vector2 frompos = gfrom->get_connection_output_position(pfrom); + Vector2 topos = gto->get_connection_input_position(pto); + + real_t s = (real_t)r_inner_shifts[u] + (frompos.y - topos.y) / zoom; + r_inner_shifts[v] = s; + left = MIN(left, s); + + u = v; + v = (StringName)r_align[v]; + } + + u = E->get(); + do { + r_inner_shifts[u] = (real_t)r_inner_shifts[u] - left; + u = (StringName)r_align[u]; + } while (u != E->get()); + } +} + +float GraphEdit::_calculate_threshold(StringName p_v, StringName p_w, const Dictionary &r_node_names, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_inner_shift, real_t p_current_threshold, const HashMap<StringName, Vector2> &r_node_positions) { +#define MAX_ORDER 2147483647 +#define ORDER(node, layers) \ + for (unsigned int i = 0; i < layers.size(); i++) { \ + int index = layers[i].find(node); \ + if (index > 0) { \ + order = index; \ + break; \ + } \ + order = MAX_ORDER; \ + } + + int order = MAX_ORDER; + float threshold = p_current_threshold; + if (p_v == p_w) { + int min_order = MAX_ORDER; + Connection incoming; + for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { + if (E->get().to == p_w) { + ORDER(E->get().from, r_layers); + if (min_order > order) { + min_order = order; + incoming = E->get(); + } + } + } + + if (incoming.from != StringName()) { + GraphNode *gfrom = Object::cast_to<GraphNode>(r_node_names[incoming.from]); + GraphNode *gto = Object::cast_to<GraphNode>(r_node_names[p_w]); + Vector2 frompos = gfrom->get_connection_output_position(incoming.from_port); + Vector2 topos = gto->get_connection_input_position(incoming.to_port); + + //If connected block node is selected, calculate thershold or add current block to list + if (gfrom->is_selected()) { + Vector2 connected_block_pos = r_node_positions[r_root[incoming.from]]; + if (connected_block_pos.y != FLT_MAX) { + //Connected block is placed. Calculate threshold + threshold = connected_block_pos.y + (real_t)r_inner_shift[incoming.from] - (real_t)r_inner_shift[p_w] + frompos.y - topos.y; + } + } + } + } + if (threshold == FLT_MIN && (StringName)r_align[p_w] == p_v) { + //This time, pick an outgoing edge and repeat as above! + int min_order = MAX_ORDER; + Connection outgoing; + for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { + if (E->get().from == p_w) { + ORDER(E->get().to, r_layers); + if (min_order > order) { + min_order = order; + outgoing = E->get(); + } + } + } + + if (outgoing.to != StringName()) { + GraphNode *gfrom = Object::cast_to<GraphNode>(r_node_names[p_w]); + GraphNode *gto = Object::cast_to<GraphNode>(r_node_names[outgoing.to]); + Vector2 frompos = gfrom->get_connection_output_position(outgoing.from_port); + Vector2 topos = gto->get_connection_input_position(outgoing.to_port); + + //If connected block node is selected, calculate thershold or add current block to list + if (gto->is_selected()) { + Vector2 connected_block_pos = r_node_positions[r_root[outgoing.to]]; + if (connected_block_pos.y != FLT_MAX) { + //Connected block is placed. Calculate threshold + threshold = connected_block_pos.y + (real_t)r_inner_shift[outgoing.to] - (real_t)r_inner_shift[p_w] + frompos.y - topos.y; + } + } + } + } +#undef MAX_ORDER +#undef ORDER + return threshold; +} + +void GraphEdit::_place_block(StringName p_v, float p_delta, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_node_name, const Dictionary &r_inner_shift, Dictionary &r_sink, Dictionary &r_shift, HashMap<StringName, Vector2> &r_node_positions) { +#define PRED(node, layers) \ + for (unsigned int i = 0; i < layers.size(); i++) { \ + int index = layers[i].find(node); \ + if (index > 0) { \ + predecessor = layers[i][index - 1]; \ + break; \ + } \ + predecessor = StringName(); \ + } + + StringName predecessor; + StringName successor; + Vector2 pos = r_node_positions[p_v]; + + if (pos.y == FLT_MAX) { + pos.y = 0; + bool initial = false; + StringName w = p_v; + real_t threshold = FLT_MIN; + do { + PRED(w, r_layers); + if (predecessor != StringName()) { + StringName u = r_root[predecessor]; + _place_block(u, p_delta, r_layers, r_root, r_align, r_node_name, r_inner_shift, r_sink, r_shift, r_node_positions); + threshold = _calculate_threshold(p_v, w, r_node_name, r_layers, r_root, r_align, r_inner_shift, threshold, r_node_positions); + if ((StringName)r_sink[p_v] == p_v) { + r_sink[p_v] = r_sink[u]; + } + + Vector2 predecessor_root_pos = r_node_positions[u]; + Vector2 predecessor_node_size = Object::cast_to<GraphNode>(r_node_name[predecessor])->get_size(); + if (r_sink[p_v] != r_sink[u]) { + real_t sc = pos.y + (real_t)r_inner_shift[w] - predecessor_root_pos.y - (real_t)r_inner_shift[predecessor] - predecessor_node_size.y - p_delta; + r_shift[r_sink[u]] = MIN(sc, (real_t)r_shift[r_sink[u]]); + } else { + real_t sb = predecessor_root_pos.y + (real_t)r_inner_shift[predecessor] + predecessor_node_size.y - (real_t)r_inner_shift[w] + p_delta; + sb = MAX(sb, threshold); + if (initial) { + pos.y = sb; + } else { + pos.y = MAX(pos.y, sb); + } + initial = false; + } + } + threshold = _calculate_threshold(p_v, w, r_node_name, r_layers, r_root, r_align, r_inner_shift, threshold, r_node_positions); + w = r_align[w]; + } while (w != p_v); + r_node_positions.set(p_v, pos); + } + +#undef PRED +} + +void GraphEdit::arrange_nodes() { + if (!arranging_graph) { + arranging_graph = true; + } else { + return; + } + + Dictionary node_names; + Set<StringName> selected_nodes; + + for (int i = get_child_count() - 1; i >= 0; i--) { + GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); + if (!gn) { + continue; + } + + node_names[gn->get_name()] = gn; + } + + HashMap<StringName, Set<StringName>> upper_neighbours; + HashMap<StringName, Pair<int, int>> port_info; + Vector2 origin(FLT_MAX, FLT_MAX); + + float gap_v = 100.0f; + float gap_h = 100.0f; + + for (int i = get_child_count() - 1; i >= 0; i--) { + GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); + if (!gn) { + continue; + } + + if (gn->is_selected()) { + selected_nodes.insert(gn->get_name()); + Set<StringName> s; + for (List<Connection>::Element *E = connections.front(); E; E = E->next()) { + GraphNode *p_from = Object::cast_to<GraphNode>(node_names[E->get().from]); + if (E->get().to == gn->get_name() && p_from->is_selected()) { + if (!s.has(p_from->get_name())) { + s.insert(p_from->get_name()); + } + String s_connection = String(p_from->get_name()) + " " + String(E->get().to); + StringName _connection(s_connection); + Pair<int, int> ports(E->get().from_port, E->get().to_port); + if (port_info.has(_connection)) { + Pair<int, int> p_ports = port_info[_connection]; + if (p_ports.first < ports.first) { + ports = p_ports; + } + } + port_info.set(_connection, ports); + } + } + upper_neighbours.set(gn->get_name(), s); + } + } + + if (!selected_nodes.size()) { + arranging_graph = false; + return; + } + + HashMap<int, Vector<StringName>> layers = _layering(selected_nodes, upper_neighbours); + _crossing_minimisation(layers, upper_neighbours); + + Dictionary root, align, sink, shift; + _horizontal_alignment(root, align, layers, upper_neighbours, selected_nodes); + + HashMap<StringName, Vector2> new_positions; + Vector2 default_position(FLT_MAX, FLT_MAX); + Dictionary inner_shift; + Set<StringName> block_heads; + + for (const Set<StringName>::Element *E = selected_nodes.front(); E; E = E->next()) { + inner_shift[E->get()] = 0.0f; + sink[E->get()] = E->get(); + shift[E->get()] = FLT_MAX; + new_positions.set(E->get(), default_position); + if ((StringName)root[E->get()] == E->get()) { + block_heads.insert(E->get()); + } + } + + _calculate_inner_shifts(inner_shift, root, node_names, align, block_heads, port_info); + + for (const Set<StringName>::Element *E = block_heads.front(); E; E = E->next()) { + _place_block(E->get(), gap_v, layers, root, align, node_names, inner_shift, sink, shift, new_positions); + } + origin.y = Object::cast_to<GraphNode>(node_names[layers[0][0]])->get_position_offset().y - (new_positions[layers[0][0]].y + (float)inner_shift[layers[0][0]]); + origin.x = Object::cast_to<GraphNode>(node_names[layers[0][0]])->get_position_offset().x; + + for (const Set<StringName>::Element *E = block_heads.front(); E; E = E->next()) { + StringName u = E->get(); + float start_from = origin.y + new_positions[E->get()].y; + do { + Vector2 cal_pos; + cal_pos.y = start_from + (real_t)inner_shift[u]; + new_positions.set(u, cal_pos); + u = align[u]; + } while (u != E->get()); + } + + //Compute horizontal co-ordinates individually for layers to get uniform gap + float start_from = origin.x; + float largest_node_size = 0.0f; + + for (unsigned int i = 0; i < layers.size(); i++) { + Vector<StringName> layer = layers[i]; + for (int j = 0; j < layer.size(); j++) { + float current_node_size = Object::cast_to<GraphNode>(node_names[layer[j]])->get_size().x; + largest_node_size = MAX(largest_node_size, current_node_size); + } + + for (int j = 0; j < layer.size(); j++) { + float current_node_size = Object::cast_to<GraphNode>(node_names[layer[j]])->get_size().x; + Vector2 cal_pos = new_positions[layer[j]]; + + if (current_node_size == largest_node_size) { + cal_pos.x = start_from; + } else { + float current_node_start_pos = start_from; + if (current_node_size < largest_node_size / 2) { + if (!(i || j)) { + start_from -= (largest_node_size - current_node_size); + } + current_node_start_pos = start_from + largest_node_size - current_node_size; + } + cal_pos.x = current_node_start_pos; + } + new_positions.set(layer[j], cal_pos); + } + + start_from += largest_node_size + gap_h; + largest_node_size = 0.0f; + } + + emit_signal("begin_node_move"); + for (const Set<StringName>::Element *E = selected_nodes.front(); E; E = E->next()) { + GraphNode *gn = Object::cast_to<GraphNode>(node_names[E->get()]); + gn->set_drag(true); + Vector2 pos = (new_positions[E->get()]); + + if (is_using_snap()) { + const int snap = get_snap(); + pos = pos.snapped(Vector2(snap, snap)); + } + gn->set_position_offset(pos); + gn->set_drag(false); + } + emit_signal("end_node_move"); + arranging_graph = false; +} + void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("connect_node", "from", "from_port", "to", "to_port"), &GraphEdit::connect_node); ClassDB::bind_method(D_METHOD("is_node_connected", "from", "from_port", "to", "to_port"), &GraphEdit::is_node_connected); @@ -1607,10 +2137,23 @@ void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("add_valid_connection_type", "from_type", "to_type"), &GraphEdit::add_valid_connection_type); ClassDB::bind_method(D_METHOD("remove_valid_connection_type", "from_type", "to_type"), &GraphEdit::remove_valid_connection_type); ClassDB::bind_method(D_METHOD("is_valid_connection_type", "from_type", "to_type"), &GraphEdit::is_valid_connection_type); + ClassDB::bind_method(D_METHOD("get_connection_line", "from", "to"), &GraphEdit::get_connection_line); ClassDB::bind_method(D_METHOD("set_zoom", "zoom"), &GraphEdit::set_zoom); ClassDB::bind_method(D_METHOD("get_zoom"), &GraphEdit::get_zoom); + ClassDB::bind_method(D_METHOD("set_zoom_min", "zoom_min"), &GraphEdit::set_zoom_min); + ClassDB::bind_method(D_METHOD("get_zoom_min"), &GraphEdit::get_zoom_min); + + ClassDB::bind_method(D_METHOD("set_zoom_max", "zoom_max"), &GraphEdit::set_zoom_max); + ClassDB::bind_method(D_METHOD("get_zoom_max"), &GraphEdit::get_zoom_max); + + ClassDB::bind_method(D_METHOD("set_zoom_step", "zoom_step"), &GraphEdit::set_zoom_step); + ClassDB::bind_method(D_METHOD("get_zoom_step"), &GraphEdit::get_zoom_step); + + ClassDB::bind_method(D_METHOD("set_show_zoom_label", "enable"), &GraphEdit::set_show_zoom_label); + ClassDB::bind_method(D_METHOD("is_showing_zoom_label"), &GraphEdit::is_showing_zoom_label); + ClassDB::bind_method(D_METHOD("set_snap", "pixels"), &GraphEdit::set_snap); ClassDB::bind_method(D_METHOD("get_snap"), &GraphEdit::get_snap); @@ -1634,20 +2177,32 @@ void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_right_disconnects", "enable"), &GraphEdit::set_right_disconnects); ClassDB::bind_method(D_METHOD("is_right_disconnects_enabled"), &GraphEdit::is_right_disconnects_enabled); - ClassDB::bind_method(D_METHOD("_gui_input"), &GraphEdit::_gui_input); ClassDB::bind_method(D_METHOD("_update_scroll_offset"), &GraphEdit::_update_scroll_offset); ClassDB::bind_method(D_METHOD("get_zoom_hbox"), &GraphEdit::get_zoom_hbox); + ClassDB::bind_method(D_METHOD("arrange_nodes"), &GraphEdit::arrange_nodes); + ClassDB::bind_method(D_METHOD("set_selected", "node"), &GraphEdit::set_selected); + GDVIRTUAL_BIND(_get_connection_line, "from", "to") + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "right_disconnects"), "set_right_disconnects", "is_right_disconnects_enabled"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "scroll_offset"), "set_scroll_ofs", "get_scroll_ofs"); ADD_PROPERTY(PropertyInfo(Variant::INT, "snap_distance"), "set_snap", "get_snap"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_snap"), "set_use_snap", "is_using_snap"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom"), "set_zoom", "get_zoom"); + + ADD_GROUP("Connection Lines", "connection_lines"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_thickness"), "set_connection_lines_thickness", "get_connection_lines_thickness"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "connection_lines_antialiased"), "set_connection_lines_antialiased", "is_connection_lines_antialiased"); + + ADD_GROUP("Zoom", ""); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom"), "set_zoom", "get_zoom"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom_min"), "set_zoom_min", "get_zoom_min"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom_max"), "set_zoom_max", "get_zoom_max"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "zoom_step"), "set_zoom_step", "get_zoom_step"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_zoom_label"), "set_show_zoom_label", "is_showing_zoom_label"); + ADD_GROUP("Minimap", "minimap"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "minimap_enabled"), "set_minimap_enabled", "is_minimap_enabled"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "minimap_size"), "set_minimap_size", "get_minimap_size"); @@ -1672,15 +2227,22 @@ void GraphEdit::_bind_methods() { GraphEdit::GraphEdit() { set_focus_mode(FOCUS_ALL); + // Allow dezooming 8 times from the default zoom level. + // At low zoom levels, text is unreadable due to its small size and poor filtering, + // but this is still useful for previewing and navigation. + zoom_min = (1 / Math::pow(zoom_step, 8)); + // Allow zooming 4 times from the default zoom level. + zoom_max = (1 * Math::pow(zoom_step, 4)); + top_layer = memnew(GraphEditFilter(this)); - add_child(top_layer); + add_child(top_layer, false, INTERNAL_MODE_BACK); top_layer->set_mouse_filter(MOUSE_FILTER_PASS); top_layer->set_anchors_and_offsets_preset(Control::PRESET_WIDE); top_layer->connect("draw", callable_mp(this, &GraphEdit::_top_layer_draw)); top_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_layer_input)); connections_layer = memnew(Control); - add_child(connections_layer); + add_child(connections_layer, false, INTERNAL_MODE_FRONT); connections_layer->connect("draw", callable_mp(this, &GraphEdit::_connections_layer_draw)); connections_layer->set_name("CLAYER"); connections_layer->set_disable_visibility_clip(true); // so it can draw freely and be offset @@ -1708,6 +2270,18 @@ GraphEdit::GraphEdit() { top_layer->add_child(zoom_hb); zoom_hb->set_position(Vector2(10, 10)); + zoom_label = memnew(Label); + zoom_hb->add_child(zoom_label); + zoom_label->set_visible(false); + zoom_label->set_v_size_flags(Control::SIZE_SHRINK_CENTER); + zoom_label->set_align(Label::ALIGN_CENTER); +#ifdef TOOLS_ENABLED + zoom_label->set_custom_minimum_size(Size2(48, 0) * EDSCALE); +#else + zoom_label->set_custom_minimum_size(Size2(48, 0)); +#endif + _update_zoom_label(); + zoom_minus = memnew(Button); zoom_minus->set_flat(true); zoom_hb->add_child(zoom_minus); @@ -1755,6 +2329,13 @@ GraphEdit::GraphEdit() { minimap_button->set_focus_mode(FOCUS_NONE); zoom_hb->add_child(minimap_button); + layout_button = memnew(Button); + layout_button->set_flat(true); + zoom_hb->add_child(layout_button); + layout_button->set_tooltip(RTR("Arrange nodes.")); + layout_button->connect("pressed", callable_mp(this, &GraphEdit::arrange_nodes)); + layout_button->set_focus_mode(FOCUS_NONE); + Vector2 minimap_size = Vector2(240, 160); float minimap_opacity = 0.65; diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h index fa3b113705..44e50aa3c2 100644 --- a/scene/gui/graph_edit.h +++ b/scene/gui/graph_edit.h @@ -34,6 +34,7 @@ #include "scene/gui/box_container.h" #include "scene/gui/button.h" #include "scene/gui/graph_node.h" +#include "scene/gui/label.h" #include "scene/gui/scroll_bar.h" #include "scene/gui/slider.h" #include "scene/gui/spin_box.h" @@ -61,8 +62,6 @@ class GraphEditMinimap : public Control { GraphEdit *ge; protected: - static void _bind_methods(); - public: GraphEditMinimap(GraphEdit *p_edit); @@ -87,7 +86,7 @@ private: Vector2 _convert_from_graph_position(const Vector2 &p_position); Vector2 _convert_to_graph_position(const Vector2 &p_position); - void _gui_input(const Ref<InputEvent> &p_ev); + virtual void gui_input(const Ref<InputEvent> &p_ev) override; void _adjust_graph_scroll(const Vector2 &p_offset); }; @@ -105,6 +104,7 @@ public: }; private: + Label *zoom_label; Button *zoom_minus; Button *zoom_reset; Button *zoom_plus; @@ -114,9 +114,7 @@ private: Button *minimap_button; - void _zoom_minus(); - void _zoom_reset(); - void _zoom_plus(); + Button *layout_button; HScrollBar *h_scroll; VScrollBar *v_scroll; @@ -144,6 +142,14 @@ private: Vector2 drag_accum; float zoom = 1.0; + float zoom_step = 1.2; + float zoom_min; + float zoom_max; + + void _zoom_minus(); + void _zoom_reset(); + void _zoom_plus(); + void _update_zoom_label(); bool box_selecting = false; bool box_selection_mode_additive = false; @@ -161,9 +167,8 @@ private: float lines_thickness = 2.0f; bool lines_antialiased = true; - void _bake_segment2d(Vector<Vector2> &points, Vector<Color> &colors, float p_begin, float p_end, const Vector2 &p_a, const Vector2 &p_out, const Vector2 &p_b, const Vector2 &p_in, int p_depth, int p_min_depth, int p_max_depth, float p_tol, const Color &p_color, const Color &p_to_color, int &lines) const; - - void _draw_cos_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_bezier_ratio); + PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to); + void _draw_connection_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color, float p_width, float p_zoom); void _graph_node_raised(Node *p_gn); void _graph_node_moved(Node *p_gn); @@ -171,14 +176,14 @@ private: void _update_scroll(); void _scroll_moved(double); - void _gui_input(const Ref<InputEvent> &p_ev); + virtual void gui_input(const Ref<InputEvent> &p_ev) override; Control *connections_layer; GraphEditFilter *top_layer; GraphEditMinimap *minimap; void _top_layer_input(const Ref<InputEvent> &p_ev); - bool is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos); + bool is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left); void _top_layer_draw(); void _connections_layer_draw(); @@ -224,12 +229,31 @@ private: bool _check_clickable_control(Control *p_control, const Vector2 &pos); + bool arranging_graph = false; + + enum SET_OPERATIONS { + IS_EQUAL, + IS_SUBSET, + DIFFERENCE, + UNION, + }; + + int _set_operations(SET_OPERATIONS p_operation, Set<StringName> &r_u, const Set<StringName> &r_v); + HashMap<int, Vector<StringName>> _layering(const Set<StringName> &r_selected_nodes, const HashMap<StringName, Set<StringName>> &r_upper_neighbours); + Vector<StringName> _split(const Vector<StringName> &r_layer, const HashMap<StringName, Dictionary> &r_crossings); + void _horizontal_alignment(Dictionary &r_root, Dictionary &r_align, const HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours, const Set<StringName> &r_selected_nodes); + void _crossing_minimisation(HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours); + void _calculate_inner_shifts(Dictionary &r_inner_shifts, const Dictionary &r_root, const Dictionary &r_node_names, const Dictionary &r_align, const Set<StringName> &r_block_heads, const HashMap<StringName, Pair<int, int>> &r_port_info); + float _calculate_threshold(StringName p_v, StringName p_w, const Dictionary &r_node_names, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_inner_shift, real_t p_current_threshold, const HashMap<StringName, Vector2> &r_node_positions); + void _place_block(StringName p_v, float p_delta, const HashMap<int, Vector<StringName>> &r_layers, const Dictionary &r_root, const Dictionary &r_align, const Dictionary &r_node_name, const Dictionary &r_inner_shift, Dictionary &r_sink, Dictionary &r_shift, HashMap<StringName, Vector2> &r_node_positions); + protected: static void _bind_methods(); virtual void add_child_notify(Node *p_child) override; virtual void remove_child_notify(Node *p_child) override; void _notification(int p_what); - virtual bool clips_input() const override; + + GDVIRTUAL2RC(Vector<Vector2>, _get_connection_line, Vector2, Vector2) public: Error connect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); @@ -247,6 +271,18 @@ public: void set_zoom_custom(float p_zoom, const Vector2 &p_center); float get_zoom() const; + void set_zoom_min(float p_zoom_min); + float get_zoom_min() const; + + void set_zoom_max(float p_zoom_max); + float get_zoom_max() const; + + void set_zoom_step(float p_zoom_step); + float get_zoom_step() const; + + void set_show_zoom_label(bool p_enable); + bool is_showing_zoom_label() const; + void set_minimap_size(Vector2 p_size); Vector2 get_minimap_size() const; void set_minimap_opacity(float p_opacity); @@ -287,6 +323,8 @@ public: HBoxContainer *get_zoom_hbox(); + void arrange_nodes(); + GraphEdit(); }; diff --git a/scene/gui/graph_node.cpp b/scene/gui/graph_node.cpp index 77c502cf8d..08c8c60d7a 100644 --- a/scene/gui/graph_node.cpp +++ b/scene/gui/graph_node.cpp @@ -180,9 +180,9 @@ void GraphNode::_resort() { /** First pass, determine minimum size AND amount of stretchable elements */ Size2i new_size = get_size(); - Ref<StyleBox> sb = get_theme_stylebox("frame"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("frame")); - int sep = get_theme_constant("separation"); + int sep = get_theme_constant(SNAME("separation")); bool first = true; int children_count = 0; @@ -323,8 +323,8 @@ void GraphNode::_resort() { bool GraphNode::has_point(const Point2 &p_point) const { if (comment) { - Ref<StyleBox> comment = get_theme_stylebox("comment"); - Ref<Texture2D> resizer = get_theme_icon("resizer"); + Ref<StyleBox> comment = get_theme_stylebox(SNAME("comment")); + Ref<Texture2D> resizer = get_theme_icon(SNAME("resizer")); if (Rect2(get_size() - resizer->get_size(), resizer->get_size()).has_point(p_point)) { return true; @@ -355,18 +355,18 @@ void GraphNode::_notification(int p_what) { //sb=sb->duplicate(); //sb->call("set_modulate",modulate); - Ref<Texture2D> port = get_theme_icon("port"); - Ref<Texture2D> close = get_theme_icon("close"); - Ref<Texture2D> resizer = get_theme_icon("resizer"); - int close_offset = get_theme_constant("close_offset"); - int close_h_offset = get_theme_constant("close_h_offset"); - Color close_color = get_theme_color("close_color"); - Color resizer_color = get_theme_color("resizer_color"); - int title_offset = get_theme_constant("title_offset"); - int title_h_offset = get_theme_constant("title_h_offset"); - Color title_color = get_theme_color("title_color"); + Ref<Texture2D> port = get_theme_icon(SNAME("port")); + Ref<Texture2D> close = get_theme_icon(SNAME("close")); + Ref<Texture2D> resizer = get_theme_icon(SNAME("resizer")); + int close_offset = get_theme_constant(SNAME("close_offset")); + int close_h_offset = get_theme_constant(SNAME("close_h_offset")); + Color close_color = get_theme_color(SNAME("close_color")); + Color resizer_color = get_theme_color(SNAME("resizer_color")); + int title_offset = get_theme_constant(SNAME("title_offset")); + int title_h_offset = get_theme_constant(SNAME("title_h_offset")); + Color title_color = get_theme_color(SNAME("title_color")); Point2i icofs = -port->get_size() * 0.5; - int edgeofs = get_theme_constant("port_offset"); + int edgeofs = get_theme_constant(SNAME("port_offset")); icofs.y += sb->get_margin(SIDE_TOP); draw_style_box(sb, Rect2(Point2(), get_size())); @@ -375,10 +375,10 @@ void GraphNode::_notification(int p_what) { case OVERLAY_DISABLED: { } break; case OVERLAY_BREAKPOINT: { - draw_style_box(get_theme_stylebox("breakpoint"), Rect2(Point2(), get_size())); + draw_style_box(get_theme_stylebox(SNAME("breakpoint")), Rect2(Point2(), get_size())); } break; case OVERLAY_POSITION: { - draw_style_box(get_theme_stylebox("position"), Rect2(Point2(), get_size())); + draw_style_box(get_theme_stylebox(SNAME("position")), Rect2(Point2(), get_size())); } break; } @@ -446,8 +446,8 @@ void GraphNode::_notification(int p_what) { } void GraphNode::_shape() { - Ref<Font> font = get_theme_font("title_font"); - int font_size = get_theme_font_size("title_font_size"); + Ref<Font> font = get_theme_font(SNAME("title_font")); + int font_size = get_theme_font_size(SNAME("title_font_size")); title_buf->clear(); if (text_direction == Control::TEXT_DIRECTION_INHERITED) { @@ -481,7 +481,7 @@ void GraphNode::set_slot(int p_idx, bool p_enable_left, int p_type_left, const C update(); connpos_dirty = true; - emit_signal("slot_updated", p_idx); + emit_signal(SNAME("slot_updated"), p_idx); } void GraphNode::clear_slot(int p_idx) { @@ -510,7 +510,7 @@ void GraphNode::set_slot_enabled_left(int p_idx, bool p_enable_left) { update(); connpos_dirty = true; - emit_signal("slot_updated", p_idx); + emit_signal(SNAME("slot_updated"), p_idx); } void GraphNode::set_slot_type_left(int p_idx, int p_type_left) { @@ -520,7 +520,7 @@ void GraphNode::set_slot_type_left(int p_idx, int p_type_left) { update(); connpos_dirty = true; - emit_signal("slot_updated", p_idx); + emit_signal(SNAME("slot_updated"), p_idx); } int GraphNode::get_slot_type_left(int p_idx) const { @@ -537,7 +537,7 @@ void GraphNode::set_slot_color_left(int p_idx, const Color &p_color_left) { update(); connpos_dirty = true; - emit_signal("slot_updated", p_idx); + emit_signal(SNAME("slot_updated"), p_idx); } Color GraphNode::get_slot_color_left(int p_idx) const { @@ -561,7 +561,7 @@ void GraphNode::set_slot_enabled_right(int p_idx, bool p_enable_right) { update(); connpos_dirty = true; - emit_signal("slot_updated", p_idx); + emit_signal(SNAME("slot_updated"), p_idx); } void GraphNode::set_slot_type_right(int p_idx, int p_type_right) { @@ -571,7 +571,7 @@ void GraphNode::set_slot_type_right(int p_idx, int p_type_right) { update(); connpos_dirty = true; - emit_signal("slot_updated", p_idx); + emit_signal(SNAME("slot_updated"), p_idx); } int GraphNode::get_slot_type_right(int p_idx) const { @@ -588,7 +588,7 @@ void GraphNode::set_slot_color_right(int p_idx, const Color &p_color_right) { update(); connpos_dirty = true; - emit_signal("slot_updated", p_idx); + emit_signal(SNAME("slot_updated"), p_idx); } Color GraphNode::get_slot_color_right(int p_idx) const { @@ -599,14 +599,14 @@ Color GraphNode::get_slot_color_right(int p_idx) const { } Size2 GraphNode::get_minimum_size() const { - int sep = get_theme_constant("separation"); - Ref<StyleBox> sb = get_theme_stylebox("frame"); + int sep = get_theme_constant(SNAME("separation")); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("frame")); bool first = true; Size2 minsize; minsize.x = title_buf->get_size().x; if (show_close) { - Ref<Texture2D> close = get_theme_icon("close"); + Ref<Texture2D> close = get_theme_icon(SNAME("close")); minsize.x += sep + close->get_width(); } @@ -699,7 +699,7 @@ String GraphNode::get_language() const { void GraphNode::set_position_offset(const Vector2 &p_offset) { position_offset = p_offset; - emit_signal("position_offset_changed"); + emit_signal(SNAME("position_offset_changed")); update(); } @@ -720,7 +720,7 @@ void GraphNode::set_drag(bool p_drag) { if (p_drag) { drag_from = get_position_offset(); } else { - emit_signal("dragged", drag_from, get_position_offset()); //useful for undo/redo + emit_signal(SNAME("dragged"), drag_from, get_position_offset()); //useful for undo/redo } } @@ -738,10 +738,10 @@ bool GraphNode::is_close_button_visible() const { } void GraphNode::_connpos_update() { - int edgeofs = get_theme_constant("port_offset"); - int sep = get_theme_constant("separation"); + int edgeofs = get_theme_constant(SNAME("port_offset")); + int sep = get_theme_constant(SNAME("separation")); - Ref<StyleBox> sb = get_theme_stylebox("frame"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("frame")); conn_input_cache.clear(); conn_output_cache.clear(); int vofs = 0; @@ -757,7 +757,7 @@ void GraphNode::_connpos_update() { continue; } - Size2i size = c->get_combined_minimum_size(); + Size2i size = c->get_rect().size; int y = sb->get_margin(SIDE_TOP) + vofs; int h = size.y; @@ -863,7 +863,7 @@ Color GraphNode::get_connection_output_color(int p_idx) { return conn_output_cache[p_idx].color; } -void GraphNode::_gui_input(const Ref<InputEvent> &p_ev) { +void GraphNode::gui_input(const Ref<InputEvent> &p_ev) { ERR_FAIL_COND(p_ev.is_null()); Ref<InputEventMouseButton> mb = p_ev; @@ -875,12 +875,12 @@ void GraphNode::_gui_input(const Ref<InputEvent> &p_ev) { if (close_rect.size != Size2() && close_rect.has_point(mpos)) { //send focus to parent get_parent_control()->grab_focus(); - emit_signal("close_request"); + emit_signal(SNAME("close_request")); accept_event(); return; } - Ref<Texture2D> resizer = get_theme_icon("resizer"); + Ref<Texture2D> resizer = get_theme_icon(SNAME("resizer")); if (resizable && mpos.x > get_size().x - resizer->get_width() && mpos.y > get_size().y - resizer->get_height()) { resizing = true; @@ -890,7 +890,7 @@ void GraphNode::_gui_input(const Ref<InputEvent> &p_ev) { return; } - emit_signal("raise_request"); + emit_signal(SNAME("raise_request")); } if (!mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { @@ -904,7 +904,7 @@ void GraphNode::_gui_input(const Ref<InputEvent> &p_ev) { Vector2 diff = mpos - resizing_from; - emit_signal("resize_request", resizing_from_size + diff); + emit_signal(SNAME("resize_request"), resizing_from_size + diff); } } @@ -946,8 +946,6 @@ void GraphNode::_bind_methods() { ClassDB::bind_method(D_METHOD("set_language", "language"), &GraphNode::set_language); ClassDB::bind_method(D_METHOD("get_language"), &GraphNode::get_language); - ClassDB::bind_method(D_METHOD("_gui_input"), &GraphNode::_gui_input); - ClassDB::bind_method(D_METHOD("set_slot", "idx", "enable_left", "type_left", "color_left", "enable_right", "type_right", "color_right", "custom_left", "custom_right"), &GraphNode::set_slot, DEFVAL(Ref<Texture2D>()), DEFVAL(Ref<Texture2D>())); ClassDB::bind_method(D_METHOD("clear_slot", "idx"), &GraphNode::clear_slot); ClassDB::bind_method(D_METHOD("clear_all_slots"), &GraphNode::clear_all_slots); @@ -1021,6 +1019,6 @@ void GraphNode::_bind_methods() { } GraphNode::GraphNode() { - title_buf.instance(); + title_buf.instantiate(); set_mouse_filter(MOUSE_FILTER_STOP); } diff --git a/scene/gui/graph_node.h b/scene/gui/graph_node.h index c70f616b47..c7c7006bfc 100644 --- a/scene/gui/graph_node.h +++ b/scene/gui/graph_node.h @@ -99,7 +99,7 @@ private: Overlay overlay = OVERLAY_DISABLED; protected: - void _gui_input(const Ref<InputEvent> &p_ev); + virtual void gui_input(const Ref<InputEvent> &p_ev) override; void _notification(int p_what); static void _bind_methods(); diff --git a/scene/gui/grid_container.cpp b/scene/gui/grid_container.cpp index 541925a802..1107e3a4af 100644 --- a/scene/gui/grid_container.cpp +++ b/scene/gui/grid_container.cpp @@ -33,13 +33,13 @@ void GridContainer::_notification(int p_what) { switch (p_what) { case NOTIFICATION_SORT_CHILDREN: { - Map<int, int> col_minw; // Max of min_width of all controls in each col (indexed by col). + Map<int, int> col_minw; // Max of min_width of all controls in each col (indexed by col). Map<int, int> row_minh; // Max of min_height of all controls in each row (indexed by row). Set<int> col_expanded; // Columns which have the SIZE_EXPAND flag set. Set<int> row_expanded; // Rows which have the SIZE_EXPAND flag set. - int hsep = get_theme_constant("hseparation"); - int vsep = get_theme_constant("vseparation"); + int hsep = get_theme_constant(SNAME("hseparation")); + int vsep = get_theme_constant(SNAME("vseparation")); int max_col = MIN(get_child_count(), columns); int max_row = ceil((float)get_child_count() / (float)columns); @@ -213,8 +213,8 @@ Size2 GridContainer::get_minimum_size() const { Map<int, int> col_minw; Map<int, int> row_minh; - int hsep = get_theme_constant("hseparation"); - int vsep = get_theme_constant("vseparation"); + int hsep = get_theme_constant(SNAME("hseparation")); + int vsep = get_theme_constant(SNAME("vseparation")); int max_row = 0; int max_col = 0; diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index 150980b2e9..d10ad90c1f 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -42,12 +42,14 @@ void ItemList::_shape(int p_idx) { } else { item.text_buf->set_direction((TextServer::Direction)item.text_direction); } - item.text_buf->add_string(item.text, get_theme_font("font"), get_theme_font_size("font_size"), item.opentype_features, (item.language != "") ? item.language : TranslationServer::get_singleton()->get_tool_locale()); + item.text_buf->add_string(item.text, get_theme_font(SNAME("font")), get_theme_font_size(SNAME("font_size")), item.opentype_features, (item.language != "") ? item.language : TranslationServer::get_singleton()->get_tool_locale()); if (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { item.text_buf->set_flags(TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::BREAK_GRAPHEME_BOUND); } else { item.text_buf->set_flags(TextServer::BREAK_NONE); } + item.text_buf->set_text_overrun_behavior(text_overrun_behavior); + item.text_buf->set_max_lines_visible(max_text_lines); } int ItemList::add_item(const String &p_item, const Ref<Texture2D> &p_texture, bool p_selectable) { @@ -57,7 +59,7 @@ int ItemList::add_item(const String &p_item, const Ref<Texture2D> &p_texture, bo item.icon_region = Rect2i(); item.icon_modulate = Color(1, 1, 1, 1); item.text = p_item; - item.text_buf.instance(); + item.text_buf.instantiate(); item.selectable = p_selectable; item.selected = false; item.disabled = false; @@ -80,7 +82,7 @@ int ItemList::add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable) { item.icon_region = Rect2i(); item.icon_modulate = Color(1, 1, 1, 1); //item.text=p_item; - item.text_buf.instance(); + item.text_buf.instantiate(); item.selectable = p_selectable; item.selected = false; item.disabled = false; @@ -245,6 +247,7 @@ void ItemList::set_item_custom_bg_color(int p_idx, const Color &p_custom_bg_colo ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].custom_bg = p_custom_bg_color; + update(); } Color ItemList::get_item_custom_bg_color(int p_idx) const { @@ -257,6 +260,7 @@ void ItemList::set_item_custom_fg_color(int p_idx, const Color &p_custom_fg_colo ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].custom_fg = p_custom_fg_color; + update(); } Color ItemList::get_item_custom_fg_color(int p_idx) const { @@ -451,6 +455,7 @@ void ItemList::set_max_text_lines(int p_lines) { for (int i = 0; i < items.size(); i++) { if (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { items.write[i].text_buf->set_flags(TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::BREAK_GRAPHEME_BOUND); + items.write[i].text_buf->set_max_lines_visible(p_lines); } else { items.write[i].text_buf->set_flags(TextServer::BREAK_NONE); } @@ -532,7 +537,7 @@ Size2 ItemList::Item::get_icon_size() const { return size_result; } -void ItemList::_gui_input(const Ref<InputEvent> &p_event) { +void ItemList::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); double prev_scroll = scroll_bar->get_value(); @@ -548,7 +553,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { if (defer_select_single >= 0 && mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_LEFT && !mb->is_pressed()) { select(defer_select_single, true); - emit_signal("multi_selected", defer_select_single, true); + emit_signal(SNAME("multi_selected"), defer_select_single, true); defer_select_single = -1; return; } @@ -556,7 +561,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { if (mb.is_valid() && (mb->get_button_index() == MOUSE_BUTTON_LEFT || (allow_rmb_select && mb->get_button_index() == MOUSE_BUTTON_RIGHT)) && mb->is_pressed()) { search_string = ""; //any mousepress cancels Vector2 pos = mb->get_position(); - Ref<StyleBox> bg = get_theme_stylebox("bg"); + Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); pos -= bg->get_offset(); pos.y += scroll_bar->get_value(); @@ -583,7 +588,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { if (select_mode == SELECT_MULTI && items[i].selected && mb->is_command_pressed()) { deselect(i); - emit_signal("multi_selected", i, false); + emit_signal(SNAME("multi_selected"), i, false); } else if (select_mode == SELECT_MULTI && mb->is_shift_pressed() && current >= 0 && current < items.size() && current != i) { int from = current; @@ -595,12 +600,12 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { bool selected = !items[j].selected; select(j, false); if (selected) { - emit_signal("multi_selected", j, true); + emit_signal(SNAME("multi_selected"), j, true); } } if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { - emit_signal("item_rmb_selected", i, get_local_mouse_position()); + emit_signal(SNAME("item_rmb_selected"), i, get_local_mouse_position()); } } else { if (!mb->is_double_click() && !mb->is_command_pressed() && select_mode == SELECT_MULTI && items[i].selectable && !items[i].disabled && items[i].selected && mb->get_button_index() == MOUSE_BUTTON_LEFT) { @@ -609,7 +614,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { } if (items[i].selected && mb->get_button_index() == MOUSE_BUTTON_RIGHT) { - emit_signal("item_rmb_selected", i, get_local_mouse_position()); + emit_signal(SNAME("item_rmb_selected"), i, get_local_mouse_position()); } else { bool selected = items[i].selected; @@ -617,16 +622,16 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { if (!selected || allow_reselect) { if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", i); + emit_signal(SNAME("item_selected"), i); } else { - emit_signal("multi_selected", i, true); + emit_signal(SNAME("multi_selected"), i, true); } } if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { - emit_signal("item_rmb_selected", i, get_local_mouse_position()); + emit_signal(SNAME("item_rmb_selected"), i, get_local_mouse_position()); } else if (/*select_mode==SELECT_SINGLE &&*/ mb->is_double_click()) { - emit_signal("item_activated", i); + emit_signal(SNAME("item_activated"), i); } } } @@ -634,13 +639,13 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { return; } if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { - emit_signal("rmb_clicked", mb->get_position()); + emit_signal(SNAME("rmb_clicked"), mb->get_position()); return; } // Since closest is null, more likely we clicked on empty space, so send signal to interested controls. Allows, for example, implement items deselecting. - emit_signal("nothing_selected"); + emit_signal(SNAME("nothing_selected")); } if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && mb->is_pressed()) { scroll_bar->set_value(scroll_bar->get_value() - scroll_bar->get_page() * mb->get_factor() / 8); @@ -661,7 +666,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } break; @@ -676,7 +681,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(current - current_columns); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } accept_event(); } @@ -691,7 +696,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } break; } @@ -705,7 +710,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(current + current_columns); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } accept_event(); } @@ -717,7 +722,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(current - current_columns * i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } accept_event(); break; @@ -731,7 +736,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(current + current_columns * i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } accept_event(); @@ -745,7 +750,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(current - 1); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } accept_event(); } @@ -756,7 +761,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(current + 1); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } accept_event(); } @@ -766,17 +771,17 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { if (current >= 0 && current < items.size()) { if (items[current].selectable && !items[current].disabled && !items[current].selected) { select(current, false); - emit_signal("multi_selected", current, true); + emit_signal(SNAME("multi_selected"), current, true); } else if (items[current].selected) { deselect(current); - emit_signal("multi_selected", current, false); + emit_signal(SNAME("multi_selected"), current, false); } } } else if (p_event->is_action("ui_accept")) { search_string = ""; //any mousepress cancels if (current >= 0 && current < items.size()) { - emit_signal("item_activated", current); + emit_signal(SNAME("item_activated"), current); } } else { Ref<InputEventKey> k = p_event; @@ -812,7 +817,7 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { set_current(i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } break; } @@ -867,7 +872,7 @@ void ItemList::_notification(int p_what) { } if (p_what == NOTIFICATION_DRAW) { - Ref<StyleBox> bg = get_theme_stylebox("bg"); + Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); int mw = scroll_bar->get_minimum_size().x; scroll_bar->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -mw); @@ -884,24 +889,24 @@ void ItemList::_notification(int p_what) { draw_style_box(bg, Rect2(Point2(), size)); - int hseparation = get_theme_constant("hseparation"); - int vseparation = get_theme_constant("vseparation"); - int icon_margin = get_theme_constant("icon_margin"); - int line_separation = get_theme_constant("line_separation"); - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + int hseparation = get_theme_constant(SNAME("hseparation")); + int vseparation = get_theme_constant(SNAME("vseparation")); + int icon_margin = get_theme_constant(SNAME("icon_margin")); + int line_separation = get_theme_constant(SNAME("line_separation")); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); - Ref<StyleBox> sbsel = has_focus() ? get_theme_stylebox("selected_focus") : get_theme_stylebox("selected"); - Ref<StyleBox> cursor = has_focus() ? get_theme_stylebox("cursor") : get_theme_stylebox("cursor_unfocused"); + Ref<StyleBox> sbsel = has_focus() ? get_theme_stylebox(SNAME("selected_focus")) : get_theme_stylebox(SNAME("selected")); + Ref<StyleBox> cursor = has_focus() ? get_theme_stylebox(SNAME("cursor")) : get_theme_stylebox(SNAME("cursor_unfocused")); bool rtl = is_layout_rtl(); - Color guide_color = get_theme_color("guide_color"); - Color font_color = get_theme_color("font_color"); - Color font_selected_color = get_theme_color("font_selected_color"); + Color guide_color = get_theme_color(SNAME("guide_color")); + Color font_color = get_theme_color(SNAME("font_color")); + Color font_selected_color = get_theme_color(SNAME("font_selected_color")); if (has_focus()) { RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), true); - draw_style_box(get_theme_stylebox("bg_focus"), Rect2(Point2(), size)); + draw_style_box(get_theme_stylebox(SNAME("bg_focus")), Rect2(Point2(), size)); RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), false); } @@ -928,8 +933,14 @@ void ItemList::_notification(int p_what) { } if (items[i].text != "") { + int max_width = -1; + if (fixed_column_width) { + max_width = fixed_column_width; + } else if (same_column_width) { + max_width = items[i].rect_cache.size.x; + } + items.write[i].text_buf->set_width(max_width); Size2 s = items[i].text_buf->get_size(); - //s.width=MIN(s.width,fixed_column_width); if (icon_mode == ICON_MODE_TOP) { minsize.x = MAX(minsize.x, s.width); @@ -1137,11 +1148,8 @@ void ItemList::_notification(int p_what) { if (icon_mode == ICON_MODE_TOP) { pos.x += Math::floor((items[i].rect_cache.size.width - icon_size.width) / 2); - pos.y += MIN( - Math::floor((items[i].rect_cache.size.height - icon_size.height) / 2), - items[i].rect_cache.size.height - items[i].min_rect_cache.size.height); - text_ofs.y = icon_size.height + icon_margin; - text_ofs.y += items[i].rect_cache.size.height - items[i].min_rect_cache.size.height; + pos.y += icon_margin; + text_ofs.y = icon_size.height + icon_margin * 2; } else { pos.y += Math::floor((items[i].rect_cache.size.height - icon_size.height) / 2); text_ofs.x = icon_size.width + icon_margin; @@ -1208,7 +1216,6 @@ void ItemList::_notification(int p_what) { text_ofs.x = size.width - text_ofs.x - max_len; } - items.write[i].text_buf->set_width(max_len); items.write[i].text_buf->set_align(HALIGN_CENTER); if (outline_size > 0 && font_outline_color.a > 0) { @@ -1299,7 +1306,7 @@ void ItemList::_scroll_changed(double) { int ItemList::get_item_at_position(const Point2 &p_pos, bool p_exact) const { Vector2 pos = p_pos; - Ref<StyleBox> bg = get_theme_stylebox("bg"); + Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); pos -= bg->get_offset(); pos.y += scroll_bar->get_value(); @@ -1337,7 +1344,7 @@ bool ItemList::is_pos_at_end_of_items(const Point2 &p_pos) const { } Vector2 pos = p_pos; - Ref<StyleBox> bg = get_theme_stylebox("bg"); + Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); pos -= bg->get_offset(); pos.y += scroll_bar->get_value(); @@ -1486,6 +1493,21 @@ bool ItemList::has_auto_height() const { return auto_height; } +void ItemList::set_text_overrun_behavior(TextParagraph::OverrunBehavior p_behavior) { + if (text_overrun_behavior != p_behavior) { + text_overrun_behavior = p_behavior; + for (int i = 0; i < items.size(); i++) { + items.write[i].text_buf->set_text_overrun_behavior(p_behavior); + } + shape_changed = true; + update(); + } +} + +TextParagraph::OverrunBehavior ItemList::get_text_overrun_behavior() const { + return text_overrun_behavior; +} + void ItemList::_bind_methods() { ClassDB::bind_method(D_METHOD("add_item", "text", "icon", "selectable"), &ItemList::add_item, DEFVAL(Variant()), DEFVAL(true)); ClassDB::bind_method(D_METHOD("add_icon_item", "icon", "selectable"), &ItemList::add_icon_item, DEFVAL(true)); @@ -1592,11 +1614,12 @@ void ItemList::_bind_methods() { ClassDB::bind_method(D_METHOD("get_v_scroll"), &ItemList::get_v_scroll); - ClassDB::bind_method(D_METHOD("_gui_input"), &ItemList::_gui_input); - ClassDB::bind_method(D_METHOD("_set_items"), &ItemList::_set_items); ClassDB::bind_method(D_METHOD("_get_items"), &ItemList::_get_items); + ClassDB::bind_method(D_METHOD("set_text_overrun_behavior", "overrun_behavior"), &ItemList::set_text_overrun_behavior); + ClassDB::bind_method(D_METHOD("get_text_overrun_behavior"), &ItemList::get_text_overrun_behavior); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "items", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL), "_set_items", "_get_items"); ADD_PROPERTY(PropertyInfo(Variant::INT, "select_mode", PROPERTY_HINT_ENUM, "Single,Multi"), "set_select_mode", "get_select_mode"); @@ -1604,6 +1627,7 @@ void ItemList::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_rmb_select"), "set_allow_rmb_select", "get_allow_rmb_select"); ADD_PROPERTY(PropertyInfo(Variant::INT, "max_text_lines", PROPERTY_HINT_RANGE, "1,10,1,or_greater"), "set_max_text_lines", "get_max_text_lines"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_height"), "set_auto_height", "has_auto_height"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "text_overrun_behavior", PROPERTY_HINT_ENUM, "Trim Nothing,Trim Characters,Trim Words,Ellipsis,Word Ellipsis"), "set_text_overrun_behavior", "get_text_overrun_behavior"); ADD_GROUP("Columns", ""); ADD_PROPERTY(PropertyInfo(Variant::INT, "max_columns", PROPERTY_HINT_RANGE, "0,10,1,or_greater"), "set_max_columns", "get_max_columns"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "same_column_width"), "set_same_column_width", "is_same_column_width"); @@ -1632,7 +1656,7 @@ void ItemList::_bind_methods() { ItemList::ItemList() { scroll_bar = memnew(VScrollBar); - add_child(scroll_bar); + add_child(scroll_bar, false, INTERNAL_MODE_FRONT); scroll_bar->connect("value_changed", callable_mp(this, &ItemList::_scroll_changed)); diff --git a/scene/gui/item_list.h b/scene/gui/item_list.h index 86a0174a20..148fa7ba9f 100644 --- a/scene/gui/item_list.h +++ b/scene/gui/item_list.h @@ -95,6 +95,7 @@ private: SelectMode select_mode = SELECT_SINGLE; IconMode icon_mode = ICON_MODE_LEFT; VScrollBar *scroll_bar; + TextParagraph::OverrunBehavior text_overrun_behavior = TextParagraph::OVERRUN_NO_TRIMMING; uint64_t search_time_msec = 0; String search_string; @@ -122,7 +123,6 @@ private: void _set_items(const Array &p_items); void _scroll_changed(double); - void _gui_input(const Ref<InputEvent> &p_event); void _shape(int p_idx); protected: @@ -130,6 +130,8 @@ protected: static void _bind_methods(); public: + virtual void gui_input(const Ref<InputEvent> &p_event) override; + int add_item(const String &p_item, const Ref<Texture2D> &p_texture = Ref<Texture2D>(), bool p_selectable = true); int add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable = true); @@ -182,6 +184,9 @@ public: void set_item_custom_fg_color(int p_idx, const Color &p_custom_fg_color); Color get_item_custom_fg_color(int p_idx) const; + void set_text_overrun_behavior(TextParagraph::OverrunBehavior p_behavior); + TextParagraph::OverrunBehavior get_text_overrun_behavior() const; + void select(int p_idx, bool p_single = true); void deselect(int p_idx); void deselect_all(); diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index de0ee626b9..5600816b2d 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -36,20 +36,20 @@ #include "servers/text_server.h" -void Label::set_autowrap(bool p_autowrap) { - if (autowrap != p_autowrap) { - autowrap = p_autowrap; +void Label::set_autowrap_mode(Label::AutowrapMode p_mode) { + if (autowrap_mode != p_mode) { + autowrap_mode = p_mode; lines_dirty = true; } update(); - if (clip) { + if (clip || overrun_behavior != OVERRUN_NO_TRIMMING) { minimum_size_changed(); } } -bool Label::has_autowrap() const { - return autowrap; +Label::AutowrapMode Label::get_autowrap_mode() const { + return autowrap_mode; } void Label::set_uppercase(bool p_uppercase) { @@ -64,22 +64,22 @@ bool Label::is_uppercase() const { } int Label::get_line_height(int p_line) const { - Ref<Font> font = get_theme_font("font"); + Ref<Font> font = get_theme_font(SNAME("font")); if (p_line >= 0 && p_line < lines_rid.size()) { - return TS->shaped_text_get_size(lines_rid[p_line]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM); + return TS->shaped_text_get_size(lines_rid[p_line]).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM); } else if (lines_rid.size() > 0) { int h = 0; for (int i = 0; i < lines_rid.size(); i++) { - h = MAX(h, TS->shaped_text_get_size(lines_rid[i]).y) + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM); + h = MAX(h, TS->shaped_text_get_size(lines_rid[i]).y) + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM); } return h; } else { - return font->get_height(get_theme_font_size("font_size")); + return font->get_height(get_theme_font_size(SNAME("font_size"))); } } void Label::_shape() { - Ref<StyleBox> style = get_theme_stylebox("normal", "Label"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal"), SNAME("Label")); int width = (get_size().width - style->get_minimum_size().width); if (dirty) { @@ -89,20 +89,39 @@ void Label::_shape() { } else { TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction); } - TS->shaped_text_add_string(text_rid, (uppercase) ? xl_text.to_upper() : xl_text, get_theme_font("font")->get_rids(), get_theme_font_size("font_size"), opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + const Ref<Font> &font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); + ERR_FAIL_COND(font.is_null()); + TS->shaped_text_add_string(text_rid, (uppercase) ? xl_text.to_upper() : xl_text, font->get_rids(), font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, xl_text)); dirty = false; lines_dirty = true; } + if (lines_dirty) { for (int i = 0; i < lines_rid.size(); i++) { TS->free(lines_rid[i]); } lines_rid.clear(); - Vector<Vector2i> lines = TS->shaped_text_get_line_breaks(text_rid, width, 0, (autowrap) ? (TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND) : TextServer::BREAK_MANDATORY); - for (int i = 0; i < lines.size(); i++) { - RID line = TS->shaped_text_substr(text_rid, lines[i].x, lines[i].y - lines[i].x); + uint8_t autowrap_flags = TextServer::BREAK_MANDATORY; + switch (autowrap_mode) { + case AUTOWRAP_WORD_SMART: + autowrap_flags = TextServer::BREAK_WORD_BOUND_ADAPTIVE | TextServer::BREAK_MANDATORY; + break; + case AUTOWRAP_WORD: + autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_MANDATORY; + break; + case AUTOWRAP_ARBITRARY: + autowrap_flags = TextServer::BREAK_GRAPHEME_BOUND | TextServer::BREAK_MANDATORY; + break; + case AUTOWRAP_OFF: + break; + } + Vector<Vector2i> line_breaks = TS->shaped_text_get_line_breaks(text_rid, width, 0, autowrap_flags); + + for (int i = 0; i < line_breaks.size(); i++) { + RID line = TS->shaped_text_substr(text_rid, line_breaks[i].x, line_breaks[i].y - line_breaks[i].x); lines_rid.push_back(line); } } @@ -111,7 +130,8 @@ void Label::_shape() { minsize = Size2(1, get_line_height()); return; } - if (!autowrap) { + + if (autowrap_mode == AUTOWRAP_OFF) { minsize.width = 0.0f; for (int i = 0; i < lines_rid.size(); i++) { if (minsize.width < TS->shaped_text_get_size(lines_rid[i]).x) { @@ -120,10 +140,61 @@ void Label::_shape() { } } - if (lines_dirty) { // Fill after min_size calculation. - if (align == ALIGN_FILL) { + if (lines_dirty) { + uint8_t overrun_flags = TextServer::OVERRUN_NO_TRIMMING; + switch (overrun_behavior) { + case OVERRUN_TRIM_WORD_ELLIPSIS: + overrun_flags |= TextServer::OVERRUN_TRIM; + overrun_flags |= TextServer::OVERRUN_TRIM_WORD_ONLY; + overrun_flags |= TextServer::OVERRUN_ADD_ELLIPSIS; + break; + case OVERRUN_TRIM_ELLIPSIS: + overrun_flags |= TextServer::OVERRUN_TRIM; + overrun_flags |= TextServer::OVERRUN_ADD_ELLIPSIS; + break; + case OVERRUN_TRIM_WORD: + overrun_flags |= TextServer::OVERRUN_TRIM; + overrun_flags |= TextServer::OVERRUN_TRIM_WORD_ONLY; + break; + case OVERRUN_TRIM_CHAR: + overrun_flags |= TextServer::OVERRUN_TRIM; + break; + case OVERRUN_NO_TRIMMING: + break; + } + + // Fill after min_size calculation. + + if (autowrap_mode != AUTOWRAP_OFF) { + int visible_lines = get_visible_line_count(); + bool lines_hidden = visible_lines > 0 && visible_lines < lines_rid.size(); + if (lines_hidden) { + overrun_flags |= TextServer::OVERRUN_ENFORCE_ELLIPSIS; + } + if (align == ALIGN_FILL) { + for (int i = 0; i < lines_rid.size(); i++) { + if (i < visible_lines - 1 || lines_rid.size() == 1) { + TS->shaped_text_fit_to_width(lines_rid[i], width); + } else if (i == (visible_lines - 1)) { + TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], width, overrun_flags); + } + } + + } else if (lines_hidden) { + TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], width, overrun_flags); + } + + } else { + // Autowrap disabled. for (int i = 0; i < lines_rid.size(); i++) { - TS->shaped_text_fit_to_width(lines_rid.write[i], width); + if (align == ALIGN_FILL) { + TS->shaped_text_fit_to_width(lines_rid[i], width); + overrun_flags |= TextServer::OVERRUN_JUSTIFICATION_AWARE; + TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags); + TS->shaped_text_fit_to_width(lines_rid[i], width, TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_CONSTRAIN_ELLIPSIS); + } else { + TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags); + } } } lines_dirty = false; @@ -131,15 +202,15 @@ void Label::_shape() { _update_visible(); - if (!autowrap || !clip) { + if (autowrap_mode == AUTOWRAP_OFF || !clip || overrun_behavior == OVERRUN_NO_TRIMMING) { minimum_size_changed(); } } void Label::_update_visible() { - int line_spacing = get_theme_constant("line_spacing", "Label"); - Ref<StyleBox> style = get_theme_stylebox("normal", "Label"); - Ref<Font> font = get_theme_font("font"); + int line_spacing = get_theme_constant(SNAME("line_spacing"), SNAME("Label")); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal"), SNAME("Label")); + Ref<Font> font = get_theme_font(SNAME("font")); int lines_visible = lines_rid.size(); if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { @@ -149,18 +220,42 @@ void Label::_update_visible() { minsize.height = 0; int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); for (int64_t i = lines_skipped; i < last_line; i++) { - minsize.height += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM) + line_spacing; + minsize.height += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM) + line_spacing; if (minsize.height > (get_size().height - style->get_minimum_size().height + line_spacing)) { break; } } } +inline void draw_glyph(const TextServer::Glyph &p_gl, const RID &p_canvas, const Color &p_font_color, const Vector2 &p_ofs) { + if (p_gl.font_rid != RID()) { + TS->font_draw_glyph(p_gl.font_rid, p_canvas, p_gl.font_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off), p_gl.index, p_font_color); + } else { + TS->draw_hex_code_box(p_canvas, p_gl.font_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off), p_gl.index, p_font_color); + } +} + +inline void draw_glyph_outline(const TextServer::Glyph &p_gl, const RID &p_canvas, const Color &p_font_color, const Color &p_font_shadow_color, const Color &p_font_outline_color, const int &p_shadow_outline_size, const int &p_outline_size, const Vector2 &p_ofs, const Vector2 &shadow_ofs) { + if (p_gl.font_rid != RID()) { + if (p_font_shadow_color.a > 0) { + TS->font_draw_glyph(p_gl.font_rid, p_canvas, p_gl.font_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off) + shadow_ofs, p_gl.index, p_font_shadow_color); + if (p_shadow_outline_size > 0) { + TS->font_draw_glyph_outline(p_gl.font_rid, p_canvas, p_gl.font_size, p_shadow_outline_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off) + Vector2(-shadow_ofs.x, shadow_ofs.y), p_gl.index, p_font_shadow_color); + TS->font_draw_glyph_outline(p_gl.font_rid, p_canvas, p_gl.font_size, p_shadow_outline_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off) + Vector2(shadow_ofs.x, -shadow_ofs.y), p_gl.index, p_font_shadow_color); + TS->font_draw_glyph_outline(p_gl.font_rid, p_canvas, p_gl.font_size, p_shadow_outline_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off) + Vector2(-shadow_ofs.x, -shadow_ofs.y), p_gl.index, p_font_shadow_color); + } + } + if (p_font_outline_color.a != 0.0 && p_outline_size > 0) { + TS->font_draw_glyph_outline(p_gl.font_rid, p_canvas, p_gl.font_size, p_outline_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off), p_gl.index, p_font_outline_color); + } + } +} + void Label::_notification(int p_what) { if (p_what == NOTIFICATION_TRANSLATION_CHANGED) { - String new_text = tr(text); + String new_text = atr(text); if (new_text == xl_text) { - return; //nothing new + return; // Nothing new. } xl_text = new_text; dirty = true; @@ -181,16 +276,16 @@ void Label::_notification(int p_what) { Size2 string_size; Size2 size = get_size(); - Ref<StyleBox> style = get_theme_stylebox("normal"); - Ref<Font> font = get_theme_font("font"); - Color font_color = get_theme_color("font_color"); - Color font_shadow_color = get_theme_color("font_shadow_color"); - Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); - int line_spacing = get_theme_constant("line_spacing"); - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); - int shadow_outline_size = get_theme_constant("shadow_outline_size"); - bool rtl = is_layout_rtl(); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); + Ref<Font> font = get_theme_font(SNAME("font")); + Color font_color = get_theme_color(SNAME("font_color")); + Color font_shadow_color = get_theme_color(SNAME("font_shadow_color")); + Point2 shadow_ofs(get_theme_constant(SNAME("shadow_offset_x")), get_theme_constant(SNAME("shadow_offset_y"))); + int line_spacing = get_theme_constant(SNAME("line_spacing")); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); + int shadow_outline_size = get_theme_constant(SNAME("shadow_outline_size")); + bool rtl = TS->shaped_text_get_direction(text_rid); style->draw(ci, Rect2(Point2(0, 0), get_size())); @@ -199,7 +294,7 @@ void Label::_notification(int p_what) { // Get number of lines to fit to the height. for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { - total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM) + line_spacing; + total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM) + line_spacing; if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { break; } @@ -215,14 +310,15 @@ void Label::_notification(int p_what) { // Get real total height. total_h = 0; for (int64_t i = lines_skipped; i < last_line; i++) { - total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM) + line_spacing; + total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM) + line_spacing; } + total_h += style->get_margin(SIDE_TOP) + style->get_margin(SIDE_BOTTOM); int vbegin = 0, vsep = 0; if (lines_visible > 0) { switch (valign) { case VALIGN_TOP: { - //nothing + // Nothing. } break; case VALIGN_CENTER: { vbegin = (size.y - (total_h - line_spacing)) / 2; @@ -267,84 +363,143 @@ void Label::_notification(int p_what) { Vector2 ofs; ofs.y = style->get_offset().y + vbegin; for (int i = lines_skipped; i < last_line; i++) { + Size2 line_size = TS->shaped_text_get_size(lines_rid[i]); ofs.x = 0; - ofs.y += TS->shaped_text_get_ascent(lines_rid[i]) + font->get_spacing(Font::SPACING_TOP); + ofs.y += TS->shaped_text_get_ascent(lines_rid[i]) + font->get_spacing(TextServer::SPACING_TOP); switch (align) { case ALIGN_FILL: - case ALIGN_LEFT: { - if (rtl) { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - TS->shaped_text_get_size(lines_rid[i]).x); + if (rtl && autowrap_mode != AUTOWRAP_OFF) { + ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); } else { ofs.x = style->get_offset().x; } + break; + case ALIGN_LEFT: { + ofs.x = style->get_offset().x; } break; case ALIGN_CENTER: { - ofs.x = int(size.width - TS->shaped_text_get_size(lines_rid[i]).x) / 2; + ofs.x = int(size.width - line_size.width) / 2; } break; case ALIGN_RIGHT: { - if (rtl) { - ofs.x = style->get_offset().x; - } else { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - TS->shaped_text_get_size(lines_rid[i]).x); - } + ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); } break; } const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(lines_rid[i]); const TextServer::Glyph *glyphs = visual.ptr(); int gl_size = visual.size(); + TextServer::TrimData trim_data = TS->shaped_text_get_trim_data(lines_rid[i]); + + // Draw outline. Note: Do not merge this into the single loop with the main text, to prevent overlaps. + if (font_shadow_color.a > 0 || (font_outline_color.a != 0.0 && outline_size > 0)) { + Vector2 offset = ofs; + // Draw RTL ellipsis string when necessary. + if (rtl && trim_data.ellipsis_pos >= 0) { + for (int gl_idx = trim_data.ellipsis_glyph_buf.size() - 1; gl_idx >= 0; gl_idx--) { + for (int j = 0; j < trim_data.ellipsis_glyph_buf[gl_idx].repeat; j++) { + //Draw glyph outlines and shadow. + draw_glyph_outline(trim_data.ellipsis_glyph_buf[gl_idx], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); + offset.x += trim_data.ellipsis_glyph_buf[gl_idx].advance; + } + } + } - float x = ofs.x; - int outlines_drawn = glyhps_drawn; - for (int j = 0; j < gl_size; j++) { - for (int k = 0; k < glyphs[j].repeat; k++) { - if (glyphs[j].font_rid != RID()) { - if (font_shadow_color.a > 0) { - TS->font_draw_glyph(glyphs[j].font_rid, ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + shadow_ofs, glyphs[j].index, font_shadow_color); - if (shadow_outline_size > 0) { - //draw shadow - TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(-shadow_ofs.x, shadow_ofs.y), glyphs[j].index, font_shadow_color); - TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(shadow_ofs.x, -shadow_ofs.y), glyphs[j].index, font_shadow_color); - TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, shadow_outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off) + Vector2(-shadow_ofs.x, -shadow_ofs.y), glyphs[j].index, font_shadow_color); + // Draw main text. + for (int j = 0; j < gl_size; j++) { + for (int k = 0; k < glyphs[j].repeat; k++) { + if (visible_glyphs != -1) { + if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + if (glyhps_drawn >= visible_glyphs) { + return; + } } } - if (font_outline_color.a != 0.0 && outline_size > 0) { - TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, outline_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_outline_color); + + // Trim when necessary. + if (trim_data.trim_pos >= 0) { + if (rtl) { + if (j < trim_data.trim_pos && (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + continue; + } + } else { + if (j >= trim_data.trim_pos && (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + break; + } + } } + + // Draw glyph outlines and shadow. + draw_glyph_outline(glyphs[j], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); + offset.x += glyphs[j].advance; + glyhps_drawn++; } - ofs.x += glyphs[j].advance; } - if (visible_glyphs != -1) { - if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { - outlines_drawn++; - if (outlines_drawn >= visible_glyphs) { - break; + // Draw LTR ellipsis string when necessary. + if (!rtl && trim_data.ellipsis_pos >= 0) { + for (int gl_idx = 0; gl_idx < trim_data.ellipsis_glyph_buf.size(); gl_idx++) { + for (int j = 0; j < trim_data.ellipsis_glyph_buf[gl_idx].repeat; j++) { + //Draw glyph outlines and shadow. + draw_glyph_outline(trim_data.ellipsis_glyph_buf[gl_idx], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); + offset.x += trim_data.ellipsis_glyph_buf[gl_idx].advance; } } } } - ofs.x = x; + // Draw main text. Note: Do not merge this into the single loop with the outline, to prevent overlaps. + + // Draw RTL ellipsis string when necessary. + if (rtl && trim_data.ellipsis_pos >= 0) { + for (int gl_idx = trim_data.ellipsis_glyph_buf.size() - 1; gl_idx >= 0; gl_idx--) { + for (int j = 0; j < trim_data.ellipsis_glyph_buf[gl_idx].repeat; j++) { + //Draw glyph outlines and shadow. + draw_glyph(trim_data.ellipsis_glyph_buf[gl_idx], ci, font_color, ofs); + ofs.x += trim_data.ellipsis_glyph_buf[gl_idx].advance; + } + } + } + + // Draw main text. for (int j = 0; j < gl_size; j++) { for (int k = 0; k < glyphs[j].repeat; k++) { - if (glyphs[j].font_rid != RID()) { - TS->font_draw_glyph(glyphs[j].font_rid, ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_color); - } else if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { - TS->draw_hex_code_box(ci, glyphs[j].font_size, ofs + Vector2(glyphs[j].x_off, glyphs[j].y_off), glyphs[j].index, font_color); + if (visible_glyphs != -1) { + if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + if (glyhps_drawn >= visible_glyphs) { + return; + } + } } + + // Trim when necessary. + if (trim_data.trim_pos >= 0) { + if (rtl) { + if (j < trim_data.trim_pos && (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + continue; + } + } else { + if (j >= trim_data.trim_pos && (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + break; + } + } + } + + // Draw glyph outlines and shadow. + draw_glyph(glyphs[j], ci, font_color, ofs); ofs.x += glyphs[j].advance; + glyhps_drawn++; } - if (visible_glyphs != -1) { - if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { - glyhps_drawn++; - if (glyhps_drawn >= visible_glyphs) { - return; - } + } + // Draw LTR ellipsis string when necessary. + if (!rtl && trim_data.ellipsis_pos >= 0) { + for (int gl_idx = 0; gl_idx < trim_data.ellipsis_glyph_buf.size(); gl_idx++) { + for (int j = 0; j < trim_data.ellipsis_glyph_buf[gl_idx].repeat; j++) { + //Draw glyph outlines and shadow. + draw_glyph(trim_data.ellipsis_glyph_buf[gl_idx], ci, font_color, ofs); + ofs.x += trim_data.ellipsis_glyph_buf[gl_idx].advance; } } } - - ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing + font->get_spacing(Font::SPACING_BOTTOM); + ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing + font->get_spacing(TextServer::SPACING_BOTTOM); } } @@ -365,17 +520,16 @@ Size2 Label::get_minimum_size() const { Size2 min_size = minsize; - Ref<Font> font = get_theme_font("font"); - min_size.height = MAX(min_size.height, font->get_height(get_theme_font_size("font_size")) + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM)); + Ref<Font> font = get_theme_font(SNAME("font")); + min_size.height = MAX(min_size.height, font->get_height(get_theme_font_size(SNAME("font_size"))) + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM)); - Size2 min_style = get_theme_stylebox("normal")->get_minimum_size(); - if (autowrap) { - return Size2(1, clip ? 1 : min_size.height) + min_style; + Size2 min_style = get_theme_stylebox(SNAME("normal"))->get_minimum_size(); + if (autowrap_mode != AUTOWRAP_OFF) { + return Size2(1, (clip || overrun_behavior != OVERRUN_NO_TRIMMING) ? 1 : min_size.height) + min_style; } else { - if (clip) { + if (clip || overrun_behavior != OVERRUN_NO_TRIMMING) { min_size.width = 1; } - return min_size + min_style; } } @@ -392,13 +546,13 @@ int Label::get_line_count() const { } int Label::get_visible_line_count() const { - Ref<Font> font = get_theme_font("font"); - Ref<StyleBox> style = get_theme_stylebox("normal"); - int line_spacing = get_theme_constant("line_spacing"); + Ref<Font> font = get_theme_font(SNAME("font")); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); + int line_spacing = get_theme_constant(SNAME("line_spacing")); int lines_visible = 0; float total_h = 0.0; for (int64_t i = lines_skipped; i < lines_rid.size(); i++) { - total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM) + line_spacing; + total_h += TS->shaped_text_get_size(lines_rid[i]).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM) + line_spacing; if (total_h > (get_size().height - style->get_minimum_size().height + line_spacing)) { break; } @@ -446,12 +600,13 @@ void Label::set_text(const String &p_string) { return; } text = p_string; - xl_text = tr(p_string); + xl_text = atr(p_string); dirty = true; if (percent_visible < 1) { visible_chars = get_total_character_count() * percent_visible; } update(); + minimum_size_changed(); } void Label::set_text_direction(Control::TextDirection p_text_direction) { @@ -534,6 +689,21 @@ bool Label::is_clipping_text() const { return clip; } +void Label::set_text_overrun_behavior(Label::OverrunBehavior p_behavior) { + if (overrun_behavior != p_behavior) { + overrun_behavior = p_behavior; + lines_dirty = true; + } + update(); + if (clip || overrun_behavior != OVERRUN_NO_TRIMMING) { + minimum_size_changed(); + } +} + +Label::OverrunBehavior Label::get_text_overrun_behavior() const { + return overrun_behavior; +} + String Label::get_text() const { return text; } @@ -545,6 +715,9 @@ void Label::set_visible_characters(int p_amount) { } else { percent_visible = 1.0; } + if (p_amount == -1) { + lines_dirty = true; + } update(); } @@ -556,7 +729,7 @@ void Label::set_percent_visible(float p_percent) { if (p_percent < 0 || p_percent >= 1) { visible_chars = -1; percent_visible = 1; - + lines_dirty = true; } else { visible_chars = get_total_character_count() * p_percent; percent_visible = p_percent; @@ -661,10 +834,12 @@ void Label::_bind_methods() { ClassDB::bind_method(D_METHOD("clear_opentype_features"), &Label::clear_opentype_features); ClassDB::bind_method(D_METHOD("set_language", "language"), &Label::set_language); ClassDB::bind_method(D_METHOD("get_language"), &Label::get_language); - ClassDB::bind_method(D_METHOD("set_autowrap", "enable"), &Label::set_autowrap); - ClassDB::bind_method(D_METHOD("has_autowrap"), &Label::has_autowrap); + ClassDB::bind_method(D_METHOD("set_autowrap_mode", "autowrap_mode"), &Label::set_autowrap_mode); + ClassDB::bind_method(D_METHOD("get_autowrap_mode"), &Label::get_autowrap_mode); ClassDB::bind_method(D_METHOD("set_clip_text", "enable"), &Label::set_clip_text); ClassDB::bind_method(D_METHOD("is_clipping_text"), &Label::is_clipping_text); + ClassDB::bind_method(D_METHOD("set_text_overrun_behavior", "overrun_behavior"), &Label::set_text_overrun_behavior); + ClassDB::bind_method(D_METHOD("get_text_overrun_behavior"), &Label::get_text_overrun_behavior); ClassDB::bind_method(D_METHOD("set_uppercase", "enable"), &Label::set_uppercase); ClassDB::bind_method(D_METHOD("is_uppercase"), &Label::is_uppercase); ClassDB::bind_method(D_METHOD("get_line_height", "line"), &Label::get_line_height, DEFVAL(-1)); @@ -694,13 +869,25 @@ void Label::_bind_methods() { BIND_ENUM_CONSTANT(VALIGN_BOTTOM); BIND_ENUM_CONSTANT(VALIGN_FILL); + BIND_ENUM_CONSTANT(AUTOWRAP_OFF); + BIND_ENUM_CONSTANT(AUTOWRAP_ARBITRARY); + BIND_ENUM_CONSTANT(AUTOWRAP_WORD); + BIND_ENUM_CONSTANT(AUTOWRAP_WORD_SMART); + + BIND_ENUM_CONSTANT(OVERRUN_NO_TRIMMING); + BIND_ENUM_CONSTANT(OVERRUN_TRIM_CHAR); + BIND_ENUM_CONSTANT(OVERRUN_TRIM_WORD); + BIND_ENUM_CONSTANT(OVERRUN_TRIM_ELLIPSIS); + BIND_ENUM_CONSTANT(OVERRUN_TRIM_WORD_ELLIPSIS); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text"); ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); ADD_PROPERTY(PropertyInfo(Variant::INT, "align", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_align", "get_align"); ADD_PROPERTY(PropertyInfo(Variant::INT, "valign", PROPERTY_HINT_ENUM, "Top,Center,Bottom,Fill"), "set_valign", "get_valign"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autowrap"), "set_autowrap", "has_autowrap"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "autowrap_mode", PROPERTY_HINT_ENUM, "Off,Arbitrary,Word,Word (Smart)"), "set_autowrap_mode", "get_autowrap_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_text"), "set_clip_text", "is_clipping_text"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "text_overrun_behavior", PROPERTY_HINT_ENUM, "Trim Nothing,Trim Characters,Trim Words,Ellipsis,Word Ellipsis"), "set_text_overrun_behavior", "get_text_overrun_behavior"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "uppercase"), "set_uppercase", "is_uppercase"); ADD_PROPERTY(PropertyInfo(Variant::INT, "visible_characters", PROPERTY_HINT_RANGE, "-1,128000,1", PROPERTY_USAGE_EDITOR), "set_visible_characters", "get_visible_characters"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "percent_visible", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_percent_visible", "get_percent_visible"); diff --git a/scene/gui/label.h b/scene/gui/label.h index 032b4112e1..8b48eb9670 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -51,13 +51,29 @@ public: VALIGN_FILL }; + enum AutowrapMode { + AUTOWRAP_OFF, + AUTOWRAP_ARBITRARY, + AUTOWRAP_WORD, + AUTOWRAP_WORD_SMART + }; + + enum OverrunBehavior { + OVERRUN_NO_TRIMMING, + OVERRUN_TRIM_CHAR, + OVERRUN_TRIM_WORD, + OVERRUN_TRIM_ELLIPSIS, + OVERRUN_TRIM_WORD_ELLIPSIS, + }; + private: Align align = ALIGN_LEFT; VAlign valign = VALIGN_TOP; String text; String xl_text; - bool autowrap = false; + AutowrapMode autowrap_mode = AUTOWRAP_OFF; bool clip = false; + OverrunBehavior overrun_behavior = OVERRUN_NO_TRIMMING; Size2 minsize; bool uppercase = false; @@ -118,8 +134,8 @@ public: void set_structured_text_bidi_override_options(Array p_args); Array get_structured_text_bidi_override_options() const; - void set_autowrap(bool p_autowrap); - bool has_autowrap() const; + void set_autowrap_mode(AutowrapMode p_mode); + AutowrapMode get_autowrap_mode() const; void set_uppercase(bool p_uppercase); bool is_uppercase() const; @@ -131,6 +147,9 @@ public: void set_clip_text(bool p_clip); bool is_clipping_text() const; + void set_text_overrun_behavior(OverrunBehavior p_behavior); + OverrunBehavior get_text_overrun_behavior() const; + void set_percent_visible(float p_percent); float get_percent_visible() const; @@ -150,5 +169,7 @@ public: VARIANT_ENUM_CAST(Label::Align); VARIANT_ENUM_CAST(Label::VAlign); +VARIANT_ENUM_CAST(Label::AutowrapMode); +VARIANT_ENUM_CAST(Label::OverrunBehavior); #endif diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index c2ed9c1a3c..d9acbeb828 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -216,7 +216,7 @@ void LineEdit::_delete(bool p_word, bool p_all_to_right) { } } -void LineEdit::_gui_input(Ref<InputEvent> p_event) { +void LineEdit::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventMouseButton> b = p_event; @@ -227,9 +227,9 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { return; } if (b->is_pressed() && b->get_button_index() == MOUSE_BUTTON_RIGHT && context_menu_enabled) { + _ensure_menu(); menu->set_position(get_screen_transform().xform(get_local_mouse_position())); menu->set_size(Vector2(1, 1)); - _generate_context_menu(); menu->popup(); grab_focus(); accept_event(); @@ -348,18 +348,18 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { if (context_menu_enabled) { if (k->is_action("ui_menu", true)) { - Point2 pos = Point2(get_caret_pixel_pos().x, (get_size().y + get_theme_font("font")->get_height(get_theme_font_size("font_size"))) / 2); + _ensure_menu(); + Point2 pos = Point2(get_caret_pixel_pos().x, (get_size().y + get_theme_font(SNAME("font"))->get_height(get_theme_font_size(SNAME("font_size")))) / 2); menu->set_position(get_global_transform().xform(pos)); menu->set_size(Vector2(1, 1)); - _generate_context_menu(); menu->popup(); menu->grab_focus(); } } - // Default is ENTER, KP_ENTER. Cannot use ui_accept as default includes SPACE - if (k->is_action("ui_text_newline", true)) { - emit_signal("text_entered", text); + // Default is ENTER and KP_ENTER. Cannot use ui_accept as default includes SPACE + if (k->is_action("ui_text_submit", false)) { + emit_signal(SNAME("text_submitted"), text); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); } @@ -567,8 +567,8 @@ bool LineEdit::_is_over_clear_button(const Point2 &p_pos) const { if (!clear_button_enabled || !has_point(p_pos)) { return false; } - Ref<Texture2D> icon = Control::get_theme_icon("clear"); - int x_ofs = get_theme_stylebox("normal")->get_offset().x; + Ref<Texture2D> icon = Control::get_theme_icon(SNAME("clear")); + int x_ofs = get_theme_stylebox(SNAME("normal"))->get_offset().x; return p_pos.x > get_size().width - icon->get_width() - x_ofs; } @@ -577,8 +577,8 @@ void LineEdit::_notification(int p_what) { #ifdef TOOLS_ENABLED case NOTIFICATION_ENTER_TREE: { if (Engine::get_singleton()->is_editor_hint() && !get_tree()->is_node_being_edited(this)) { - set_caret_blink_enabled(EDITOR_DEF("text_editor/cursor/caret_blink", false)); - set_caret_blink_speed(EDITOR_DEF("text_editor/cursor/caret_blink_speed", 0.65)); + set_caret_blink_enabled(EDITOR_DEF("text_editor/appearance/caret/caret_blink", false)); + set_caret_blink_speed(EDITOR_DEF("text_editor/appearance/caret/caret_blink_speed", 0.65)); if (!EditorSettings::get_singleton()->is_connected("settings_changed", callable_mp(this, &LineEdit::_editor_settings_changed))) { EditorSettings::get_singleton()->connect("settings_changed", callable_mp(this, &LineEdit::_editor_settings_changed)); @@ -597,7 +597,7 @@ void LineEdit::_notification(int p_what) { update(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { - placeholder_translated = tr(placeholder); + placeholder_translated = atr(placeholder); _shape(); update(); } break; @@ -612,7 +612,7 @@ void LineEdit::_notification(int p_what) { update(); } break; case NOTIFICATION_DRAW: { - if ((!has_focus() && !menu->has_focus() && !caret_force_displayed) || !window_has_focus) { + if ((!has_focus() && !(menu && menu->has_focus()) && !caret_force_displayed) || !window_has_focus) { draw_caret = false; } @@ -625,23 +625,23 @@ void LineEdit::_notification(int p_what) { RID ci = get_canvas_item(); - Ref<StyleBox> style = get_theme_stylebox("normal"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); if (!is_editable()) { - style = get_theme_stylebox("read_only"); + style = get_theme_stylebox(SNAME("read_only")); draw_caret = false; } - Ref<Font> font = get_theme_font("font"); + Ref<Font> font = get_theme_font(SNAME("font")); style->draw(ci, Rect2(Point2(), size)); if (has_focus()) { - get_theme_stylebox("focus")->draw(ci, Rect2(Point2(), size)); + get_theme_stylebox(SNAME("focus"))->draw(ci, Rect2(Point2(), size)); } int x_ofs = 0; bool using_placeholder = text.is_empty() && ime_text.is_empty(); float text_width = TS->shaped_text_get_size(text_rid).x; - float text_height = TS->shaped_text_get_size(text_rid).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM); + float text_height = TS->shaped_text_get_size(text_rid).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM); switch (align) { case ALIGN_FILL: @@ -673,10 +673,10 @@ void LineEdit::_notification(int p_what) { int y_area = height - style->get_minimum_size().height; int y_ofs = style->get_offset().y + (y_area - text_height) / 2; - Color selection_color = get_theme_color("selection_color"); - Color font_color = is_editable() ? get_theme_color("font_color") : get_theme_color("font_uneditable_color"); - Color font_selected_color = get_theme_color("font_selected_color"); - Color caret_color = get_theme_color("caret_color"); + Color selection_color = get_theme_color(SNAME("selection_color")); + Color font_color = get_theme_color(is_editable() ? SNAME("font_color") : SNAME("font_uneditable_color")); + Color font_selected_color = get_theme_color(SNAME("font_selected_color")); + Color caret_color = get_theme_color(SNAME("caret_color")); // Draw placeholder color. if (using_placeholder) { @@ -685,13 +685,13 @@ void LineEdit::_notification(int p_what) { bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; if (right_icon.is_valid() || display_clear_icon) { - Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon(SNAME("clear")) : right_icon; Color color_icon(1, 1, 1, !is_editable() ? .5 * .9 : .9); if (display_clear_icon) { if (clear_button_status.press_attempt && clear_button_status.pressing_inside) { - color_icon = get_theme_color("clear_button_color_pressed"); + color_icon = get_theme_color(SNAME("clear_button_color_pressed")); } else { - color_icon = get_theme_color("clear_button_color"); + color_icon = get_theme_color(SNAME("clear_button_color")); } } @@ -738,8 +738,8 @@ void LineEdit::_notification(int p_what) { // Draw text. ofs.y += TS->shaped_text_get_ascent(text_rid); - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); if (outline_size > 0 && font_outline_color.a > 0) { Vector2 oofs = ofs; for (int i = 0; i < gl_size; i++) { @@ -784,7 +784,7 @@ void LineEdit::_notification(int p_what) { if (l_caret == Rect2() && t_caret == Rect2()) { // No carets, add one at the start. - int h = get_theme_font("font")->get_height(get_theme_font_size("font_size")); + int h = get_theme_font(SNAME("font"))->get_height(get_theme_font_size(SNAME("font_size"))); int y = style->get_offset().y + (y_area - h) / 2; if (rtl) { l_dir = TextServer::DIRECTION_RTL; @@ -946,6 +946,17 @@ void LineEdit::paste_text() { } } +bool LineEdit::has_undo() const { + if (undo_stack_pos == nullptr) { + return undo_stack.size() > 1; + } + return undo_stack_pos != undo_stack.front(); +} + +bool LineEdit::has_redo() const { + return undo_stack_pos != nullptr && undo_stack_pos != undo_stack.back(); +} + void LineEdit::undo() { if (!editable) { return; @@ -1006,7 +1017,7 @@ void LineEdit::shift_selection_check_post(bool p_shift) { } void LineEdit::set_caret_at_pixel_pos(int p_x) { - Ref<StyleBox> style = get_theme_stylebox("normal"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); bool rtl = is_layout_rtl(); int x_ofs = 0; @@ -1039,7 +1050,7 @@ void LineEdit::set_caret_at_pixel_pos(int p_x) { bool using_placeholder = text.is_empty() && ime_text.is_empty(); bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; if (right_icon.is_valid() || display_clear_icon) { - Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon(SNAME("clear")) : right_icon; if (align == ALIGN_CENTER) { if (scroll_offset == 0) { x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); @@ -1054,7 +1065,7 @@ void LineEdit::set_caret_at_pixel_pos(int p_x) { } Vector2i LineEdit::get_caret_pixel_pos() { - Ref<StyleBox> style = get_theme_stylebox("normal"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); bool rtl = is_layout_rtl(); int x_ofs = 0; @@ -1087,7 +1098,7 @@ Vector2i LineEdit::get_caret_pixel_pos() { bool using_placeholder = text.is_empty() && ime_text.is_empty(); bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; if (right_icon.is_valid() || display_clear_icon) { - Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon(SNAME("clear")) : right_icon; if (align == ALIGN_CENTER) { if (scroll_offset == 0) { x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); @@ -1252,10 +1263,12 @@ void LineEdit::set_text_direction(Control::TextDirection p_text_direction) { } _shape(); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_AUTO), text_direction == TEXT_DIRECTION_AUTO); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); + if (menu_dir) { + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_AUTO), text_direction == TEXT_DIRECTION_AUTO); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); + } update(); } } @@ -1302,7 +1315,9 @@ String LineEdit::get_language() const { void LineEdit::set_draw_control_chars(bool p_draw_control_chars) { if (draw_control_chars != p_draw_control_chars) { draw_control_chars = p_draw_control_chars; - menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); + if (menu && menu->get_item_index(MENU_DISPLAY_UCC) >= 0) { + menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); + } _shape(); update(); } @@ -1360,7 +1375,7 @@ String LineEdit::get_text() const { void LineEdit::set_placeholder(String p_text) { placeholder = p_text; - placeholder_translated = tr(placeholder); + placeholder_translated = atr(placeholder); _shape(); update(); } @@ -1396,7 +1411,7 @@ void LineEdit::set_caret_column(int p_column) { return; } - Ref<StyleBox> style = get_theme_stylebox("normal"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); bool rtl = is_layout_rtl(); int x_ofs = 0; @@ -1430,7 +1445,7 @@ void LineEdit::set_caret_column(int p_column) { bool using_placeholder = text.is_empty() && ime_text.is_empty(); bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; if (right_icon.is_valid() || display_clear_icon) { - Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon(SNAME("clear")) : right_icon; if (align == ALIGN_CENTER) { if (scroll_offset == 0) { x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); @@ -1470,19 +1485,23 @@ int LineEdit::get_scroll_offset() const { } void LineEdit::insert_text_at_caret(String p_text) { - if ((max_length <= 0) || (text.length() + p_text.length() <= max_length)) { - String pre = text.substr(0, caret_column); - String post = text.substr(caret_column, text.length() - caret_column); - text = pre + p_text + post; - _shape(); - TextServer::Direction dir = TS->shaped_text_get_dominant_direciton_in_range(text_rid, caret_column, caret_column + p_text.length()); - if (dir != TextServer::DIRECTION_AUTO) { - input_direction = (TextDirection)dir; + if (max_length > 0) { + // Truncate text to append to fit in max_length, if needed. + int available_chars = max_length - text.length(); + if (p_text.length() > available_chars) { + emit_signal(SNAME("text_change_rejected"), p_text.substr(available_chars)); + p_text = p_text.substr(0, available_chars); } - set_caret_column(caret_column + p_text.length()); - } else { - emit_signal("text_change_rejected"); } + String pre = text.substr(0, caret_column); + String post = text.substr(caret_column, text.length() - caret_column); + text = pre + p_text + post; + _shape(); + TextServer::Direction dir = TS->shaped_text_get_dominant_direciton_in_range(text_rid, caret_column, caret_column + p_text.length()); + if (dir != TextServer::DIRECTION_AUTO) { + input_direction = (TextDirection)dir; + } + set_caret_column(caret_column + p_text.length()); } void LineEdit::clear_internal() { @@ -1497,28 +1516,28 @@ void LineEdit::clear_internal() { } Size2 LineEdit::get_minimum_size() const { - Ref<StyleBox> style = get_theme_stylebox("normal"); - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); Size2 min_size; // Minimum size of text. int em_space_size = font->get_char_size('M', 0, font_size).x; - min_size.width = get_theme_constant("minimum_character_width") * em_space_size; + min_size.width = get_theme_constant(SNAME("minimum_character_width")) * em_space_size; if (expand_to_text_length) { // Add a space because some fonts are too exact, and because caret needs a bit more when at the end. min_size.width = MAX(min_size.width, full_width + em_space_size); } - min_size.height = MAX(TS->shaped_text_get_size(text_rid).y + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM), font->get_height(font_size)); + min_size.height = MAX(TS->shaped_text_get_size(text_rid).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM), font->get_height(font_size)); // Take icons into account. bool using_placeholder = text.is_empty() && ime_text.is_empty(); bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; if (right_icon.is_valid() || display_clear_icon) { - Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon(SNAME("clear")) : right_icon; min_size.width += r_icon->get_width(); min_size.height = MAX(min_size.height, r_icon->get_height()); } @@ -1592,7 +1611,6 @@ void LineEdit::set_editable(bool p_editable) { } editable = p_editable; - _generate_context_menu(); minimum_size_changed(); update(); @@ -1806,14 +1824,19 @@ bool LineEdit::is_context_menu_enabled() { return context_menu_enabled; } +bool LineEdit::is_menu_visible() const { + return menu && menu->is_visible(); +} + PopupMenu *LineEdit::get_menu() const { + const_cast<LineEdit *>(this)->_ensure_menu(); return menu; } void LineEdit::_editor_settings_changed() { #ifdef TOOLS_ENABLED - set_caret_blink_enabled(EDITOR_DEF("text_editor/cursor/caret_blink", false)); - set_caret_blink_speed(EDITOR_DEF("text_editor/cursor/caret_blink_speed", 0.65)); + set_caret_blink_enabled(EDITOR_DEF("text_editor/appearance/caret/caret_blink", false)); + set_caret_blink_speed(EDITOR_DEF("text_editor/appearance/caret/caret_blink_speed", 0.65)); #endif } @@ -1843,8 +1866,6 @@ bool LineEdit::is_clear_button_enabled() const { void LineEdit::set_shortcut_keys_enabled(bool p_enabled) { shortcut_keys_enabled = p_enabled; - - _generate_context_menu(); } bool LineEdit::is_shortcut_keys_enabled() const { @@ -1865,8 +1886,6 @@ void LineEdit::set_selecting_enabled(bool p_enabled) { if (!selecting_enabled) { deselect(); } - - _generate_context_menu(); } bool LineEdit::is_selecting_enabled() const { @@ -1893,7 +1912,7 @@ void LineEdit::_text_changed() { } void LineEdit::_emit_text_change() { - emit_signal("text_changed", text); + emit_signal(SNAME("text_changed"), text); text_changed_dirty = false; } @@ -1920,8 +1939,9 @@ void LineEdit::_shape() { } TS->shaped_text_set_preserve_control(text_rid, draw_control_chars); - const Ref<Font> &font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); + const Ref<Font> &font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); + ERR_FAIL_COND(font.is_null()); TS->shaped_text_add_string(text_rid, t, font->get_rids(), font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, t)); @@ -1937,12 +1957,12 @@ void LineEdit::_shape() { void LineEdit::_fit_to_width() { if (align == ALIGN_FILL) { - Ref<StyleBox> style = get_theme_stylebox("normal"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); int t_width = get_size().width - style->get_margin(SIDE_RIGHT) - style->get_margin(SIDE_LEFT); bool using_placeholder = text.is_empty() && ime_text.is_empty(); bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; if (right_icon.is_valid() || display_clear_icon) { - Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon("clear") : right_icon; + Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon(SNAME("clear")) : right_icon; t_width -= r_icon->get_width(); } TS->shaped_text_fit_to_width(text_rid, MAX(t_width, full_width)); @@ -2003,35 +2023,6 @@ int LineEdit::_get_menu_action_accelerator(const String &p_action) { } } -void LineEdit::_generate_context_menu() { - // Reorganize context menu. - menu->clear(); - if (editable) { - menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_cut") : 0); - } - menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_copy") : 0); - if (editable) { - menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_paste") : 0); - } - menu->add_separator(); - if (is_selecting_enabled()) { - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : 0); - } - if (editable) { - menu->add_item(RTR("Clear"), MENU_CLEAR); - menu->add_separator(); - menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_undo") : 0); - menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_redo") : 0); - } - menu->add_separator(); - menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); - menu->add_separator(); - menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); - if (editable) { - menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); - } -} - bool LineEdit::_set(const StringName &p_name, const Variant &p_value) { String str = p_name; if (str.begins_with("opentype_features/")) { @@ -2094,7 +2085,6 @@ void LineEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_align", "align"), &LineEdit::set_align); ClassDB::bind_method(D_METHOD("get_align"), &LineEdit::get_align); - ClassDB::bind_method(D_METHOD("_gui_input"), &LineEdit::_gui_input); ClassDB::bind_method(D_METHOD("clear"), &LineEdit::clear); ClassDB::bind_method(D_METHOD("select", "from", "to"), &LineEdit::select, DEFVAL(0), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("select_all"), &LineEdit::select_all); @@ -2144,6 +2134,7 @@ void LineEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_secret_character"), &LineEdit::get_secret_character); ClassDB::bind_method(D_METHOD("menu_option", "option"), &LineEdit::menu_option); ClassDB::bind_method(D_METHOD("get_menu"), &LineEdit::get_menu); + ClassDB::bind_method(D_METHOD("is_menu_visible"), &LineEdit::is_menu_visible); ClassDB::bind_method(D_METHOD("set_context_menu_enabled", "enable"), &LineEdit::set_context_menu_enabled); ClassDB::bind_method(D_METHOD("is_context_menu_enabled"), &LineEdit::is_context_menu_enabled); ClassDB::bind_method(D_METHOD("set_virtual_keyboard_enabled", "enable"), &LineEdit::set_virtual_keyboard_enabled); @@ -2158,8 +2149,8 @@ void LineEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_right_icon"), &LineEdit::get_right_icon); ADD_SIGNAL(MethodInfo("text_changed", PropertyInfo(Variant::STRING, "new_text"))); - ADD_SIGNAL(MethodInfo("text_change_rejected")); - ADD_SIGNAL(MethodInfo("text_entered", PropertyInfo(Variant::STRING, "new_text"))); + ADD_SIGNAL(MethodInfo("text_change_rejected", PropertyInfo(Variant::STRING, "rejected_substring"))); + ADD_SIGNAL(MethodInfo("text_submitted", PropertyInfo(Variant::STRING, "new_text"))); BIND_ENUM_CONSTANT(ALIGN_LEFT); BIND_ENUM_CONSTANT(ALIGN_CENTER); @@ -2198,7 +2189,7 @@ void LineEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "text"), "set_text", "get_text"); ADD_PROPERTY(PropertyInfo(Variant::INT, "align", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_align", "get_align"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "max_length"), "set_max_length", "get_max_length"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "max_length", PROPERTY_HINT_RANGE, "0,1000,1,or_greater"), "set_max_length", "get_max_length"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editable"), "set_editable", "is_editable"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "secret"), "set_secret", "is_secret"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "secret_character"), "set_secret_character", "get_secret_character"); @@ -2221,11 +2212,89 @@ void LineEdit::_bind_methods() { ADD_GROUP("Caret", "caret_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_blink"), "set_caret_blink_enabled", "is_caret_blink_enabled"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "caret_blink_speed", PROPERTY_HINT_RANGE, "0.1,10,0.01"), "set_caret_blink_speed", "get_caret_blink_speed"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "caret_column"), "set_caret_column", "get_caret_column"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "caret_column", PROPERTY_HINT_RANGE, "0,1000,1,or_greater"), "set_caret_column", "get_caret_column"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_force_displayed"), "set_caret_force_displayed", "is_caret_force_displayed"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_mid_grapheme"), "set_caret_mid_grapheme_enabled", "is_caret_mid_grapheme_enabled"); } +void LineEdit::_ensure_menu() { + if (!menu) { + menu = memnew(PopupMenu); + add_child(menu, false, INTERNAL_MODE_FRONT); + + menu_dir = memnew(PopupMenu); + menu_dir->set_name("DirMenu"); + menu_dir->add_radio_check_item(RTR("Same as layout direction"), MENU_DIR_INHERITED); + menu_dir->add_radio_check_item(RTR("Auto-detect direction"), MENU_DIR_AUTO); + menu_dir->add_radio_check_item(RTR("Left-to-right"), MENU_DIR_LTR); + menu_dir->add_radio_check_item(RTR("Right-to-left"), MENU_DIR_RTL); + menu->add_child(menu_dir); + + menu_ctl = memnew(PopupMenu); + menu_ctl->set_name("CTLMenu"); + menu_ctl->add_item(RTR("Left-to-right mark (LRM)"), MENU_INSERT_LRM); + menu_ctl->add_item(RTR("Right-to-left mark (RLM)"), MENU_INSERT_RLM); + menu_ctl->add_item(RTR("Start of left-to-right embedding (LRE)"), MENU_INSERT_LRE); + menu_ctl->add_item(RTR("Start of right-to-left embedding (RLE)"), MENU_INSERT_RLE); + menu_ctl->add_item(RTR("Start of left-to-right override (LRO)"), MENU_INSERT_LRO); + menu_ctl->add_item(RTR("Start of right-to-left override (RLO)"), MENU_INSERT_RLO); + menu_ctl->add_item(RTR("Pop direction formatting (PDF)"), MENU_INSERT_PDF); + menu_ctl->add_separator(); + menu_ctl->add_item(RTR("Arabic letter mark (ALM)"), MENU_INSERT_ALM); + menu_ctl->add_item(RTR("Left-to-right isolate (LRI)"), MENU_INSERT_LRI); + menu_ctl->add_item(RTR("Right-to-left isolate (RLI)"), MENU_INSERT_RLI); + menu_ctl->add_item(RTR("First strong isolate (FSI)"), MENU_INSERT_FSI); + menu_ctl->add_item(RTR("Pop direction isolate (PDI)"), MENU_INSERT_PDI); + menu_ctl->add_separator(); + menu_ctl->add_item(RTR("Zero width joiner (ZWJ)"), MENU_INSERT_ZWJ); + menu_ctl->add_item(RTR("Zero width non-joiner (ZWNJ)"), MENU_INSERT_ZWNJ); + menu_ctl->add_item(RTR("Word joiner (WJ)"), MENU_INSERT_WJ); + menu_ctl->add_item(RTR("Soft hyphen (SHY)"), MENU_INSERT_SHY); + menu->add_child(menu_ctl); + + menu->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); + menu_dir->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); + menu_ctl->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); + } + + // Reorganize context menu. + menu->clear(); + if (editable) { + menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_cut") : 0); + } + menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_copy") : 0); + if (editable) { + menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_paste") : 0); + } + menu->add_separator(); + if (is_selecting_enabled()) { + menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : 0); + } + if (editable) { + menu->add_item(RTR("Clear"), MENU_CLEAR); + menu->add_separator(); + menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_undo") : 0); + menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_redo") : 0); + } + menu->add_separator(); + menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); + menu->add_separator(); + menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); + menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); + if (editable) { + menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); + } + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_AUTO), text_direction == TEXT_DIRECTION_AUTO); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); + + if (editable) { + menu->set_item_disabled(menu->get_item_index(MENU_UNDO), !has_undo()); + menu->set_item_disabled(menu->get_item_index(MENU_REDO), !has_redo()); + } +} + LineEdit::LineEdit() { text_rid = TS->create_shaped_text(); _create_undo_state(); @@ -2236,49 +2305,12 @@ LineEdit::LineEdit() { set_mouse_filter(MOUSE_FILTER_STOP); caret_blink_timer = memnew(Timer); - add_child(caret_blink_timer); + add_child(caret_blink_timer, false, INTERNAL_MODE_FRONT); caret_blink_timer->set_wait_time(0.65); caret_blink_timer->connect("timeout", callable_mp(this, &LineEdit::_toggle_draw_caret)); set_caret_blink_enabled(false); - menu = memnew(PopupMenu); - add_child(menu); - - menu_dir = memnew(PopupMenu); - menu_dir->set_name("DirMenu"); - menu_dir->add_radio_check_item(RTR("Same as layout direction"), MENU_DIR_INHERITED); - menu_dir->add_radio_check_item(RTR("Auto-detect direction"), MENU_DIR_AUTO); - menu_dir->add_radio_check_item(RTR("Left-to-right"), MENU_DIR_LTR); - menu_dir->add_radio_check_item(RTR("Right-to-left"), MENU_DIR_RTL); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), true); - menu->add_child(menu_dir); - - menu_ctl = memnew(PopupMenu); - menu_ctl->set_name("CTLMenu"); - menu_ctl->add_item(RTR("Left-to-right mark (LRM)"), MENU_INSERT_LRM); - menu_ctl->add_item(RTR("Right-to-left mark (RLM)"), MENU_INSERT_RLM); - menu_ctl->add_item(RTR("Start of left-to-right embedding (LRE)"), MENU_INSERT_LRE); - menu_ctl->add_item(RTR("Start of right-to-left embedding (RLE)"), MENU_INSERT_RLE); - menu_ctl->add_item(RTR("Start of left-to-right override (LRO)"), MENU_INSERT_LRO); - menu_ctl->add_item(RTR("Start of right-to-left override (RLO)"), MENU_INSERT_RLO); - menu_ctl->add_item(RTR("Pop direction formatting (PDF)"), MENU_INSERT_PDF); - menu_ctl->add_separator(); - menu_ctl->add_item(RTR("Arabic letter mark (ALM)"), MENU_INSERT_ALM); - menu_ctl->add_item(RTR("Left-to-right isolate (LRI)"), MENU_INSERT_LRI); - menu_ctl->add_item(RTR("Right-to-left isolate (RLI)"), MENU_INSERT_RLI); - menu_ctl->add_item(RTR("First strong isolate (FSI)"), MENU_INSERT_FSI); - menu_ctl->add_item(RTR("Pop direction isolate (PDI)"), MENU_INSERT_PDI); - menu_ctl->add_separator(); - menu_ctl->add_item(RTR("Zero width joiner (ZWJ)"), MENU_INSERT_ZWJ); - menu_ctl->add_item(RTR("Zero width non-joiner (ZWNJ)"), MENU_INSERT_ZWNJ); - menu_ctl->add_item(RTR("Word joiner (WJ)"), MENU_INSERT_WJ); - menu_ctl->add_item(RTR("Soft hyphen (SHY)"), MENU_INSERT_SHY); - menu->add_child(menu_ctl); - set_editable(true); // Initialise to opposite first, so we get past the early-out in set_editable. - menu->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); - menu_dir->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); - menu_ctl->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); } LineEdit::~LineEdit() { diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index 12fec2f98b..e364a79c83 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -165,7 +165,6 @@ private: void _create_undo_state(); int _get_menu_action_accelerator(const String &p_action); - void _generate_context_menu(); void _shape(); void _fit_to_width(); @@ -198,10 +197,12 @@ private: void _backspace(bool p_word = false, bool p_all_to_left = false); void _delete(bool p_word = false, bool p_all_to_right = false); + void _ensure_menu(); + protected: void _notification(int p_what); static void _bind_methods(); - void _gui_input(Ref<InputEvent> p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; @@ -222,6 +223,7 @@ public: void set_context_menu_enabled(bool p_enable); bool is_context_menu_enabled(); PopupMenu *get_menu() const; + bool is_menu_visible() const; void select(int p_from = 0, int p_to = -1); void select_all(); @@ -283,6 +285,8 @@ public: void copy_text(); void cut_text(); void paste_text(); + bool has_undo() const; + bool has_redo() const; void undo(); void redo(); diff --git a/scene/gui/link_button.cpp b/scene/gui/link_button.cpp index d45ffde715..925e6f5b97 100644 --- a/scene/gui/link_button.cpp +++ b/scene/gui/link_button.cpp @@ -32,8 +32,8 @@ #include "core/string/translation.h" void LinkButton::_shape() { - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); text_buf->clear(); if (text_direction == Control::TEXT_DIRECTION_INHERITED) { @@ -41,12 +41,16 @@ void LinkButton::_shape() { } else { text_buf->set_direction((TextServer::Direction)text_direction); } - TS->shaped_text_set_bidi_override(text_buf->get_rid(), structured_text_parser(st_parser, st_args, text)); - text_buf->add_string(text, font, font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + TS->shaped_text_set_bidi_override(text_buf->get_rid(), structured_text_parser(st_parser, st_args, xl_text)); + text_buf->add_string(xl_text, font, font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); } void LinkButton::set_text(const String &p_text) { + if (text == p_text) { + return; + } text = p_text; + xl_text = atr(text); _shape(); minimum_size_changed(); update(); @@ -141,7 +145,13 @@ Size2 LinkButton::get_minimum_size() const { void LinkButton::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: { + xl_text = atr(text); + _shape(); + + minimum_size_changed(); + update(); + } break; case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { update(); } break; @@ -158,41 +168,41 @@ void LinkButton::_notification(int p_what) { switch (get_draw_mode()) { case DRAW_NORMAL: { - color = get_theme_color("font_color"); + color = get_theme_color(SNAME("font_color")); do_underline = underline_mode == UNDERLINE_MODE_ALWAYS; } break; case DRAW_HOVER_PRESSED: case DRAW_PRESSED: { - if (has_theme_color("font_pressed_color")) { - color = get_theme_color("font_pressed_color"); + if (has_theme_color(SNAME("font_pressed_color"))) { + color = get_theme_color(SNAME("font_pressed_color")); } else { - color = get_theme_color("font_color"); + color = get_theme_color(SNAME("font_color")); } do_underline = underline_mode != UNDERLINE_MODE_NEVER; } break; case DRAW_HOVER: { - color = get_theme_color("font_hover_color"); + color = get_theme_color(SNAME("font_hover_color")); do_underline = underline_mode != UNDERLINE_MODE_NEVER; } break; case DRAW_DISABLED: { - color = get_theme_color("font_disabled_color"); + color = get_theme_color(SNAME("font_disabled_color")); do_underline = underline_mode == UNDERLINE_MODE_ALWAYS; } break; } if (has_focus()) { - Ref<StyleBox> style = get_theme_stylebox("focus"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("focus")); style->draw(ci, Rect2(Point2(), size)); } int width = text_buf->get_line_width(); - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); if (is_layout_rtl()) { if (outline_size > 0 && font_outline_color.a > 0) { text_buf->draw_outline(get_canvas_item(), Vector2(size.width - width, 0), outline_size, font_outline_color); @@ -206,7 +216,7 @@ void LinkButton::_notification(int p_what) { } if (do_underline) { - int underline_spacing = get_theme_constant("underline_spacing") + text_buf->get_line_underline_position(); + int underline_spacing = get_theme_constant(SNAME("underline_spacing")) + text_buf->get_line_underline_position(); int y = text_buf->get_line_ascent() + underline_spacing; if (is_layout_rtl()) { @@ -301,7 +311,7 @@ void LinkButton::_bind_methods() { } LinkButton::LinkButton() { - text_buf.instance(); + text_buf.instantiate(); set_focus_mode(FOCUS_NONE); set_default_cursor_shape(CURSOR_POINTING_HAND); } diff --git a/scene/gui/link_button.h b/scene/gui/link_button.h index 7eaa9f88b6..231543c63c 100644 --- a/scene/gui/link_button.h +++ b/scene/gui/link_button.h @@ -47,6 +47,7 @@ public: private: String text; + String xl_text; Ref<TextLine> text_buf; UnderlineMode underline_mode = UNDERLINE_MODE_ALWAYS; diff --git a/scene/gui/margin_container.cpp b/scene/gui/margin_container.cpp index 0e9610d0a3..50b4d192a9 100644 --- a/scene/gui/margin_container.cpp +++ b/scene/gui/margin_container.cpp @@ -31,10 +31,10 @@ #include "margin_container.h" Size2 MarginContainer::get_minimum_size() const { - int margin_left = get_theme_constant("margin_left"); - int margin_top = get_theme_constant("margin_top"); - int margin_right = get_theme_constant("margin_right"); - int margin_bottom = get_theme_constant("margin_bottom"); + int margin_left = get_theme_constant(SNAME("margin_left")); + int margin_top = get_theme_constant(SNAME("margin_top")); + int margin_right = get_theme_constant(SNAME("margin_right")); + int margin_bottom = get_theme_constant(SNAME("margin_bottom")); Size2 max; @@ -68,10 +68,10 @@ Size2 MarginContainer::get_minimum_size() const { void MarginContainer::_notification(int p_what) { switch (p_what) { case NOTIFICATION_SORT_CHILDREN: { - int margin_left = get_theme_constant("margin_left"); - int margin_top = get_theme_constant("margin_top"); - int margin_right = get_theme_constant("margin_right"); - int margin_bottom = get_theme_constant("margin_bottom"); + int margin_left = get_theme_constant(SNAME("margin_left")); + int margin_top = get_theme_constant(SNAME("margin_top")); + int margin_right = get_theme_constant(SNAME("margin_right")); + int margin_bottom = get_theme_constant(SNAME("margin_bottom")); Size2 s = get_size(); diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index 1e9baa77fc..737ba84617 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -33,7 +33,7 @@ #include "core/os/keyboard.h" #include "scene/main/window.h" -void MenuButton::_unhandled_key_input(Ref<InputEvent> p_event) { +void MenuButton::unhandled_key_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (!_is_focus_owner_in_shorcut_context()) { @@ -44,7 +44,7 @@ void MenuButton::_unhandled_key_input(Ref<InputEvent> p_event) { return; } - if (p_event->is_pressed() && !p_event->is_echo() && (Object::cast_to<InputEventKey>(p_event.ptr()) || Object::cast_to<InputEventJoypadButton>(p_event.ptr()) || Object::cast_to<InputEventAction>(*p_event))) { + if (p_event->is_pressed() && !p_event->is_echo() && (Object::cast_to<InputEventKey>(p_event.ptr()) || Object::cast_to<InputEventJoypadButton>(p_event.ptr()) || Object::cast_to<InputEventAction>(*p_event) || Object::cast_to<InputEventShortcut>(*p_event))) { if (!get_parent() || !is_visible_in_tree() || is_disabled()) { return; } @@ -55,7 +55,38 @@ void MenuButton::_unhandled_key_input(Ref<InputEvent> p_event) { } } +void MenuButton::_popup_visibility_changed(bool p_visible) { + set_pressed(p_visible); + + if (!p_visible) { + set_process_internal(false); + return; + } + + if (switch_on_hover) { + Window *window = Object::cast_to<Window>(get_viewport()); + if (window) { + mouse_pos_adjusted = window->get_position(); + + if (window->is_embedded()) { + Window *window_parent = Object::cast_to<Window>(window->get_parent()->get_viewport()); + while (window_parent) { + if (!window_parent->is_embedded()) { + mouse_pos_adjusted += window_parent->get_position(); + break; + } + + window_parent = Object::cast_to<Window>(window_parent->get_parent()->get_viewport()); + } + } + + set_process_internal(true); + } + } +} + void MenuButton::pressed() { + emit_signal(SNAME("about_to_popup")); Size2 size = get_size(); Point2 gp = get_screen_position(); @@ -69,8 +100,8 @@ void MenuButton::pressed() { popup->popup(); } -void MenuButton::_gui_input(Ref<InputEvent> p_event) { - BaseButton::_gui_input(p_event); +void MenuButton::gui_input(const Ref<InputEvent> &p_event) { + BaseButton::gui_input(p_event); } PopupMenu *MenuButton::get_popup() const { @@ -94,10 +125,22 @@ bool MenuButton::is_switch_on_hover() { } void MenuButton::_notification(int p_what) { - if (p_what == NOTIFICATION_VISIBILITY_CHANGED) { - if (!is_visible_in_tree()) { - popup->hide(); - } + switch (p_what) { + case NOTIFICATION_VISIBILITY_CHANGED: { + if (!is_visible_in_tree()) { + popup->hide(); + } + } break; + case NOTIFICATION_INTERNAL_PROCESS: { + Vector2i mouse_pos = DisplayServer::get_singleton()->mouse_get_position() - mouse_pos_adjusted; + MenuButton *menu_btn_other = Object::cast_to<MenuButton>(get_viewport()->gui_find_control(mouse_pos)); + + if (menu_btn_other && menu_btn_other != this && menu_btn_other->is_switch_on_hover() && !menu_btn_other->is_disabled() && + (get_parent()->is_ancestor_of(menu_btn_other) || menu_btn_other->get_parent()->is_ancestor_of(popup))) { + popup->hide(); + menu_btn_other->pressed(); + } + } break; } } @@ -129,9 +172,9 @@ MenuButton::MenuButton() { popup = memnew(PopupMenu); popup->hide(); - add_child(popup); - popup->connect("about_to_popup", callable_mp((BaseButton *)this, &BaseButton::set_pressed), varray(true)); // For when switching from another MenuButton. - popup->connect("popup_hide", callable_mp((BaseButton *)this, &BaseButton::set_pressed), varray(false)); + add_child(popup, false, INTERNAL_MODE_FRONT); + popup->connect("about_to_popup", callable_mp(this, &MenuButton::_popup_visibility_changed), varray(true)); + popup->connect("popup_hide", callable_mp(this, &MenuButton::_popup_visibility_changed), varray(false)); } MenuButton::~MenuButton() { diff --git a/scene/gui/menu_button.h b/scene/gui/menu_button.h index fd9ae6021e..730495b65d 100644 --- a/scene/gui/menu_button.h +++ b/scene/gui/menu_button.h @@ -42,15 +42,19 @@ class MenuButton : public Button { bool disable_shortcuts = false; PopupMenu *popup; + Vector2i mouse_pos_adjusted; + Array _get_items() const; void _set_items(const Array &p_items); - void _gui_input(Ref<InputEvent> p_event) override; + virtual void gui_input(const Ref<InputEvent> &p_event) override; + + void _popup_visibility_changed(bool p_visible); protected: void _notification(int p_what); static void _bind_methods(); - virtual void _unhandled_key_input(Ref<InputEvent> p_event) override; + virtual void unhandled_key_input(const Ref<InputEvent> &p_event) override; public: virtual void pressed() override; diff --git a/scene/gui/nine_patch_rect.cpp b/scene/gui/nine_patch_rect.cpp index 29a38ad5e3..8bf25ac915 100644 --- a/scene/gui/nine_patch_rect.cpp +++ b/scene/gui/nine_patch_rect.cpp @@ -30,6 +30,7 @@ #include "nine_patch_rect.h" +#include "scene/scene_string_names.h" #include "servers/rendering_server.h" void NinePatchRect::_notification(int p_what) { @@ -97,7 +98,7 @@ void NinePatchRect::set_texture(const Ref<Texture2D> &p_tex) { texture->set_flags(texture->get_flags()&(~Texture::FLAG_REPEAT)); //remove repeat from texture, it looks bad in sprites */ minimum_size_changed(); - emit_signal("texture_changed"); + emit_signal(SceneStringNames::get_singleton()->texture_changed); } Ref<Texture2D> NinePatchRect::get_texture() const { diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp index e52b6917be..d16e96dbec 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -35,12 +35,12 @@ Size2 OptionButton::get_minimum_size() const { Size2 minsize = Button::get_minimum_size(); - if (has_theme_icon("arrow")) { - const Size2 padding = get_theme_stylebox("normal")->get_minimum_size(); - const Size2 arrow_size = Control::get_theme_icon("arrow")->get_size(); + if (has_theme_icon(SNAME("arrow"))) { + const Size2 padding = get_theme_stylebox(SNAME("normal"))->get_minimum_size(); + const Size2 arrow_size = Control::get_theme_icon(SNAME("arrow"))->get_size(); Size2 content_size = minsize - padding; - content_size.width += arrow_size.width + get_theme_constant("hseparation"); + content_size.width += arrow_size.width + get_theme_constant(SNAME("hseparation")); content_size.height = MAX(content_size.height, arrow_size.height); minsize = content_size + padding; @@ -52,26 +52,26 @@ Size2 OptionButton::get_minimum_size() const { void OptionButton::_notification(int p_what) { switch (p_what) { case NOTIFICATION_DRAW: { - if (!has_theme_icon("arrow")) { + if (!has_theme_icon(SNAME("arrow"))) { return; } RID ci = get_canvas_item(); - Ref<Texture2D> arrow = Control::get_theme_icon("arrow"); + Ref<Texture2D> arrow = Control::get_theme_icon(SNAME("arrow")); Color clr = Color(1, 1, 1); - if (get_theme_constant("modulate_arrow")) { + if (get_theme_constant(SNAME("modulate_arrow"))) { switch (get_draw_mode()) { case DRAW_PRESSED: - clr = get_theme_color("font_pressed_color"); + clr = get_theme_color(SNAME("font_pressed_color")); break; case DRAW_HOVER: - clr = get_theme_color("font_hover_color"); + clr = get_theme_color(SNAME("font_hover_color")); break; case DRAW_DISABLED: - clr = get_theme_color("font_disabled_color"); + clr = get_theme_color(SNAME("font_disabled_color")); break; default: - clr = get_theme_color("font_color"); + clr = get_theme_color(SNAME("font_color")); } } @@ -79,22 +79,22 @@ void OptionButton::_notification(int p_what) { Point2 ofs; if (is_layout_rtl()) { - ofs = Point2(get_theme_constant("arrow_margin"), int(Math::abs((size.height - arrow->get_height()) / 2))); + ofs = Point2(get_theme_constant(SNAME("arrow_margin")), int(Math::abs((size.height - arrow->get_height()) / 2))); } else { - ofs = Point2(size.width - arrow->get_width() - get_theme_constant("arrow_margin"), int(Math::abs((size.height - arrow->get_height()) / 2))); + ofs = Point2(size.width - arrow->get_width() - get_theme_constant(SNAME("arrow_margin")), int(Math::abs((size.height - arrow->get_height()) / 2))); } arrow->draw(ci, ofs, clr); } break; case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { - if (has_theme_icon("arrow")) { + if (has_theme_icon(SNAME("arrow"))) { if (is_layout_rtl()) { - _set_internal_margin(SIDE_LEFT, Control::get_theme_icon("arrow")->get_width()); + _set_internal_margin(SIDE_LEFT, Control::get_theme_icon(SNAME("arrow"))->get_width()); _set_internal_margin(SIDE_RIGHT, 0.f); } else { _set_internal_margin(SIDE_LEFT, 0.f); - _set_internal_margin(SIDE_RIGHT, Control::get_theme_icon("arrow")->get_width()); + _set_internal_margin(SIDE_RIGHT, Control::get_theme_icon(SNAME("arrow"))->get_width()); } } } break; @@ -107,7 +107,7 @@ void OptionButton::_notification(int p_what) { } void OptionButton::_focused(int p_which) { - emit_signal("item_focused", p_which); + emit_signal(SNAME("item_focused"), p_which); } void OptionButton::_selected(int p_which) { @@ -220,7 +220,7 @@ void OptionButton::_select(int p_which, bool p_emit) { set_icon(popup->get_item_icon(current)); if (is_inside_tree() && p_emit) { - emit_signal("item_selected", current); + emit_signal(SNAME("item_selected"), current); } } @@ -339,19 +339,19 @@ OptionButton::OptionButton() { set_toggle_mode(true); set_text_align(ALIGN_LEFT); if (is_layout_rtl()) { - if (has_theme_icon("arrow")) { - _set_internal_margin(SIDE_LEFT, Control::get_theme_icon("arrow")->get_width()); + if (has_theme_icon(SNAME("arrow"))) { + _set_internal_margin(SIDE_LEFT, Control::get_theme_icon(SNAME("arrow"))->get_width()); } } else { - if (has_theme_icon("arrow")) { - _set_internal_margin(SIDE_RIGHT, Control::get_theme_icon("arrow")->get_width()); + if (has_theme_icon(SNAME("arrow"))) { + _set_internal_margin(SIDE_RIGHT, Control::get_theme_icon(SNAME("arrow"))->get_width()); } } set_action_mode(ACTION_MODE_BUTTON_PRESS); popup = memnew(PopupMenu); popup->hide(); - add_child(popup); + add_child(popup, false, INTERNAL_MODE_FRONT); popup->connect("index_pressed", callable_mp(this, &OptionButton::_selected)); popup->connect("id_focused", callable_mp(this, &OptionButton::_focused)); popup->connect("popup_hide", callable_mp((BaseButton *)this, &BaseButton::set_pressed), varray(false)); diff --git a/scene/gui/option_button.h b/scene/gui/option_button.h index d846e395ad..953337ecce 100644 --- a/scene/gui/option_button.h +++ b/scene/gui/option_button.h @@ -56,6 +56,10 @@ protected: static void _bind_methods(); public: + // ATTENTION: This is used by the POT generator's scene parser. If the number of properties returned by `_get_items()` ever changes, + // this value should be updated to reflect the new size. + static const int ITEM_PROPERTY_SIZE = 5; + void add_icon_item(const Ref<Texture2D> &p_icon, const String &p_label, int p_id = -1); void add_item(const String &p_label, int p_id = -1); diff --git a/scene/gui/panel.cpp b/scene/gui/panel.cpp index 995e985c3a..e8e7e3d997 100644 --- a/scene/gui/panel.cpp +++ b/scene/gui/panel.cpp @@ -35,7 +35,7 @@ void Panel::_notification(int p_what) { if (p_what == NOTIFICATION_DRAW) { RID ci = get_canvas_item(); - Ref<StyleBox> style = mode == MODE_BACKGROUND ? get_theme_stylebox("panel") : get_theme_stylebox("panel_fg"); + Ref<StyleBox> style = mode == MODE_BACKGROUND ? get_theme_stylebox(SNAME("panel")) : get_theme_stylebox(SNAME("panel_fg")); style->draw(ci, Rect2(Point2(), get_size())); } } diff --git a/scene/gui/panel_container.cpp b/scene/gui/panel_container.cpp index 11d822c5e1..d910e1e882 100644 --- a/scene/gui/panel_container.cpp +++ b/scene/gui/panel_container.cpp @@ -33,10 +33,10 @@ Size2 PanelContainer::get_minimum_size() const { Ref<StyleBox> style; - if (has_theme_stylebox("panel")) { - style = get_theme_stylebox("panel"); + if (has_theme_stylebox(SNAME("panel"))) { + style = get_theme_stylebox(SNAME("panel")); } else { - style = get_theme_stylebox("panel", "PanelContainer"); + style = get_theme_stylebox(SNAME("panel"), SNAME("PanelContainer")); } Size2 ms; @@ -65,10 +65,10 @@ void PanelContainer::_notification(int p_what) { RID ci = get_canvas_item(); Ref<StyleBox> style; - if (has_theme_stylebox("panel")) { - style = get_theme_stylebox("panel"); + if (has_theme_stylebox(SNAME("panel"))) { + style = get_theme_stylebox(SNAME("panel")); } else { - style = get_theme_stylebox("panel", "PanelContainer"); + style = get_theme_stylebox(SNAME("panel"), SNAME("PanelContainer")); } style->draw(ci, Rect2(Point2(), get_size())); @@ -77,10 +77,10 @@ void PanelContainer::_notification(int p_what) { if (p_what == NOTIFICATION_SORT_CHILDREN) { Ref<StyleBox> style; - if (has_theme_stylebox("panel")) { - style = get_theme_stylebox("panel"); + if (has_theme_stylebox(SNAME("panel"))) { + style = get_theme_stylebox(SNAME("panel")); } else { - style = get_theme_stylebox("panel", "PanelContainer"); + style = get_theme_stylebox(SNAME("panel"), SNAME("PanelContainer")); } Size2 size = get_size(); diff --git a/scene/gui/popup.cpp b/scene/gui/popup.cpp index 36bcca61a7..e9414598a2 100644 --- a/scene/gui/popup.cpp +++ b/scene/gui/popup.cpp @@ -71,7 +71,8 @@ void Popup::_notification(int p_what) { _initialize_visible_parents(); } else { _deinitialize_visible_parents(); - emit_signal("popup_hide"); + emit_signal(SNAME("popup_hide")); + popped_up = false; } } break; @@ -103,9 +104,9 @@ void Popup::_close_pressed() { _deinitialize_visible_parents(); - call_deferred("hide"); + call_deferred(SNAME("hide")); - emit_signal("cancelled"); + emit_signal(SNAME("cancelled")); } void Popup::set_as_minsize() { @@ -254,5 +255,5 @@ void PopupPanel::_notification(int p_what) { PopupPanel::PopupPanel() { panel = memnew(Panel); - add_child(panel); + add_child(panel, false, INTERNAL_MODE_FRONT); } diff --git a/scene/gui/popup.h b/scene/gui/popup.h index 0355405d7c..c7090e7231 100644 --- a/scene/gui/popup.h +++ b/scene/gui/popup.h @@ -35,6 +35,8 @@ #include "core/templates/local_vector.h" +class Panel; + class Popup : public Window { GDCLASS(Popup, Window); diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 2100707d2d..c0a559e624 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -46,15 +46,15 @@ String PopupMenu::_get_accel_text(const Item &p_item) const { } Size2 PopupMenu::_get_contents_minimum_size() const { - int vseparation = get_theme_constant("vseparation"); - int hseparation = get_theme_constant("hseparation"); + int vseparation = get_theme_constant(SNAME("vseparation")); + int hseparation = get_theme_constant(SNAME("hseparation")); - Size2 minsize = get_theme_stylebox("panel")->get_minimum_size(); // Accounts for margin in the margin container + Size2 minsize = get_theme_stylebox(SNAME("panel"))->get_minimum_size(); // Accounts for margin in the margin container minsize.x += scroll_container->get_v_scrollbar()->get_size().width * 2; // Adds a buffer so that the scrollbar does not render over the top of content float max_w = 0.0; float icon_w = 0.0; - int check_w = MAX(get_theme_icon("checked")->get_width(), get_theme_icon("radio_checked")->get_width()) + hseparation; + int check_w = MAX(get_theme_icon(SNAME("checked"))->get_width(), get_theme_icon(SNAME("radio_checked"))->get_width()) + hseparation; int accel_max_w = 0; bool has_check = false; @@ -74,14 +74,14 @@ Size2 PopupMenu::_get_contents_minimum_size() const { size.width += items[i].text_buf->get_size().x; size.height += vseparation; - if (items[i].accel || (items[i].shortcut.is_valid() && items[i].shortcut->is_valid())) { + if (items[i].accel || (items[i].shortcut.is_valid() && items[i].shortcut->has_valid_event())) { int accel_w = hseparation * 2; accel_w += items[i].accel_text_buf->get_size().x; accel_max_w = MAX(accel_w, accel_max_w); } if (items[i].submenu != "") { - size.width += get_theme_icon("submenu")->get_width(); + size.width += get_theme_icon(SNAME("submenu"))->get_width(); } max_w = MAX(max_w, size.width); @@ -89,7 +89,7 @@ Size2 PopupMenu::_get_contents_minimum_size() const { minsize.height += size.height; } - int item_side_padding = get_theme_constant("item_start_padding") + get_theme_constant("item_end_padding"); + int item_side_padding = get_theme_constant(SNAME("item_start_padding")) + get_theme_constant(SNAME("item_end_padding")); minsize.width += max_w + icon_w + accel_max_w + item_side_padding; if (has_check) { @@ -112,24 +112,24 @@ int PopupMenu::_get_item_height(int p_item) const { int icon_height = items[p_item].get_icon_size().height; if (items[p_item].checkable_type) { - icon_height = MAX(icon_height, MAX(get_theme_icon("checked")->get_height(), get_theme_icon("radio_checked")->get_height())); + icon_height = MAX(icon_height, MAX(get_theme_icon(SNAME("checked"))->get_height(), get_theme_icon(SNAME("radio_checked"))->get_height())); } int text_height = items[p_item].text_buf->get_size().height; if (text_height == 0 && !items[p_item].separator) { - text_height = get_theme_font("font")->get_height(get_theme_font_size("font_size")); + text_height = get_theme_font(SNAME("font"))->get_height(get_theme_font_size(SNAME("font_size"))); } int separator_height = 0; if (items[p_item].separator) { - separator_height = MAX(get_theme_stylebox("separator")->get_minimum_size().height, MAX(get_theme_stylebox("labeled_separator_left")->get_minimum_size().height, get_theme_stylebox("labeled_separator_right")->get_minimum_size().height)); + separator_height = MAX(get_theme_stylebox(SNAME("separator"))->get_minimum_size().height, MAX(get_theme_stylebox(SNAME("labeled_separator_left"))->get_minimum_size().height, get_theme_stylebox(SNAME("labeled_separator_right"))->get_minimum_size().height)); } return MAX(separator_height, MAX(text_height, icon_height)); } int PopupMenu::_get_items_total_height() const { - int vsep = get_theme_constant("vseparation"); + int vsep = get_theme_constant(SNAME("vseparation")); // Get total height of all items by taking max of icon height and font height int items_total_height = 0; @@ -163,9 +163,9 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { return -1; } - Ref<StyleBox> style = get_theme_stylebox("panel"); // Accounts for margin in the margin container + Ref<StyleBox> style = get_theme_stylebox(SNAME("panel")); // Accounts for margin in the margin container - int vseparation = get_theme_constant("vseparation"); + int vseparation = get_theme_constant(SNAME("vseparation")); Point2 ofs = style->get_offset() + Point2(0, vseparation / 2); @@ -195,8 +195,8 @@ void PopupMenu::_activate_submenu(int p_over) { return; //already visible! } - Ref<StyleBox> style = get_theme_stylebox("panel"); - int vsep = get_theme_constant("vseparation"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("panel")); + int vsep = get_theme_constant(SNAME("vseparation")); Point2 this_pos = get_position(); Rect2 this_rect(this_pos, get_size()); @@ -228,6 +228,7 @@ void PopupMenu::_activate_submenu(int p_over) { // Set autohide areas PopupMenu *submenu_pum = Object::cast_to<PopupMenu>(submenu_popup); if (submenu_pum) { + submenu_pum->take_mouse_focus(); // Make the position of the parent popup relative to submenu popup this_rect.position = this_rect.position - submenu_pum->get_position(); @@ -251,7 +252,7 @@ void PopupMenu::_submenu_timeout() { submenu_over = -1; } -void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { +void PopupMenu::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (p_event->is_action("ui_down") && p_event->is_pressed()) { @@ -264,7 +265,7 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { for (int i = search_from; i < items.size(); i++) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; - emit_signal("id_focused", i); + emit_signal(SNAME("id_focused"), i); _scroll_to_item(i); control->update(); set_input_as_handled(); @@ -278,7 +279,7 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { for (int i = 0; i < search_from; i++) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; - emit_signal("id_focused", i); + emit_signal(SNAME("id_focused"), i); _scroll_to_item(i); control->update(); set_input_as_handled(); @@ -296,7 +297,7 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { for (int i = search_from; i >= 0; i--) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; - emit_signal("id_focused", i); + emit_signal(SNAME("id_focused"), i); _scroll_to_item(i); control->update(); set_input_as_handled(); @@ -310,7 +311,7 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { for (int i = items.size() - 1; i >= search_from; i--) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; - emit_signal("id_focused", i); + emit_signal(SNAME("id_focused"), i); _scroll_to_item(i); control->update(); set_input_as_handled(); @@ -358,9 +359,10 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { } int button_idx = b->get_button_index(); - if (b->is_pressed() || (!b->is_pressed() && during_grabbed_click)) { - // Allow activating item by releasing the LMB or any that was down when the popup appeared. - // However, if button was not held when opening menu, do not allow release to activate item. + if (!b->is_pressed()) { + // Activate the item on release of either the left mouse button or + // any mouse button held down when the popup was opened. + // This allows for opening the popup and triggering an action in a single mouse click. if (button_idx == MOUSE_BUTTON_LEFT || (initial_button_mask & (1 << (button_idx - 1)))) { bool was_during_grabbed_click = during_grabbed_click; during_grabbed_click = false; @@ -397,17 +399,17 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseMotion> m = p_event; if (m.is_valid()) { - if (!item_clickable_area.has_point(m->get_position())) { - return; - } - - for (List<Rect2>::Element *E = autohide_areas.front(); E; E = E->next()) { - if (!Rect2(Point2(), get_size()).has_point(m->get_position()) && E->get().has_point(m->get_position())) { + for (const Rect2 &E : autohide_areas) { + if (!Rect2(Point2(), get_size()).has_point(m->get_position()) && E.has_point(m->get_position())) { _close_pressed(); return; } } + if (!item_clickable_area.has_point(m->get_position())) { + return; + } + int over = _get_mouse_over(m->get_position()); int id = (over < 0 || items[over].separator || items[over].disabled) ? -1 : (items[over].id >= 0 ? items[over].id : over); @@ -459,7 +461,7 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { if (items[i].text.findn(search_string) == 0) { mouse_over = i; - emit_signal("id_focused", i); + emit_signal(SNAME("id_focused"), i); _scroll_to_item(i); control->update(); set_input_as_handled(); @@ -474,37 +476,37 @@ void PopupMenu::_draw_items() { RID ci = control->get_canvas_item(); Size2 margin_size; - margin_size.width = margin_container->get_theme_constant("margin_right") + margin_container->get_theme_constant("margin_left"); - margin_size.height = margin_container->get_theme_constant("margin_top") + margin_container->get_theme_constant("margin_bottom"); + margin_size.width = margin_container->get_theme_constant(SNAME("margin_right")) + margin_container->get_theme_constant(SNAME("margin_left")); + margin_size.height = margin_container->get_theme_constant(SNAME("margin_top")) + margin_container->get_theme_constant(SNAME("margin_bottom")); // Space between the item content and the sides of popup menu. - int item_start_padding = get_theme_constant("item_start_padding"); - int item_end_padding = get_theme_constant("item_end_padding"); + int item_start_padding = get_theme_constant(SNAME("item_start_padding")); + int item_end_padding = get_theme_constant(SNAME("item_end_padding")); bool rtl = control->is_layout_rtl(); - Ref<StyleBox> style = get_theme_stylebox("panel"); - Ref<StyleBox> hover = get_theme_stylebox("hover"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("panel")); + Ref<StyleBox> hover = get_theme_stylebox(SNAME("hover")); // In Item::checkable_type enum order (less the non-checkable member) - Ref<Texture2D> check[] = { get_theme_icon("checked"), get_theme_icon("radio_checked") }; - Ref<Texture2D> uncheck[] = { get_theme_icon("unchecked"), get_theme_icon("radio_unchecked") }; + Ref<Texture2D> check[] = { get_theme_icon(SNAME("checked")), get_theme_icon(SNAME("radio_checked")) }; + Ref<Texture2D> uncheck[] = { get_theme_icon(SNAME("unchecked")), get_theme_icon(SNAME("radio_unchecked")) }; Ref<Texture2D> submenu; if (rtl) { - submenu = get_theme_icon("submenu_mirrored"); + submenu = get_theme_icon(SNAME("submenu_mirrored")); } else { - submenu = get_theme_icon("submenu"); + submenu = get_theme_icon(SNAME("submenu")); } - Ref<StyleBox> separator = get_theme_stylebox("separator"); - Ref<StyleBox> labeled_separator_left = get_theme_stylebox("labeled_separator_left"); - Ref<StyleBox> labeled_separator_right = get_theme_stylebox("labeled_separator_right"); + Ref<StyleBox> separator = get_theme_stylebox(SNAME("separator")); + Ref<StyleBox> labeled_separator_left = get_theme_stylebox(SNAME("labeled_separator_left")); + Ref<StyleBox> labeled_separator_right = get_theme_stylebox(SNAME("labeled_separator_right")); - int vseparation = get_theme_constant("vseparation"); - int hseparation = get_theme_constant("hseparation"); - Color font_color = get_theme_color("font_color"); - Color font_disabled_color = get_theme_color("font_disabled_color"); - Color font_accelerator_color = get_theme_color("font_accelerator_color"); - Color font_hover_color = get_theme_color("font_hover_color"); - Color font_separator_color = get_theme_color("font_separator_color"); + int vseparation = get_theme_constant(SNAME("vseparation")); + int hseparation = get_theme_constant(SNAME("hseparation")); + Color font_color = get_theme_color(SNAME("font_color")); + Color font_disabled_color = get_theme_color(SNAME("font_disabled_color")); + Color font_accelerator_color = get_theme_color(SNAME("font_accelerator_color")); + Color font_hover_color = get_theme_color(SNAME("font_hover_color")); + Color font_separator_color = get_theme_color(SNAME("font_separator_color")); float scroll_width = scroll_container->get_v_scrollbar()->is_visible_in_tree() ? scroll_container->get_v_scrollbar()->get_size().width : 0; float display_width = control->get_size().width - scroll_width; @@ -525,7 +527,7 @@ void PopupMenu::_draw_items() { float check_ofs = 0.0; if (has_check) { - check_ofs = MAX(get_theme_icon("checked")->get_width(), get_theme_icon("radio_checked")->get_width()) + hseparation; + check_ofs = MAX(get_theme_icon(SNAME("checked"))->get_width(), get_theme_icon(SNAME("radio_checked"))->get_width()) + hseparation; } Point2 ofs = Point2(); @@ -606,8 +608,8 @@ void PopupMenu::_draw_items() { } // Text - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); if (items[i].separator) { if (text != String()) { int center = (display_width - items[i].text_buf->get_size().width) / 2; @@ -635,7 +637,7 @@ void PopupMenu::_draw_items() { } // Accelerator / Shortcut - if (items[i].accel || (items[i].shortcut.is_valid() && items[i].shortcut->is_valid())) { + if (items[i].accel || (items[i].shortcut.is_valid() && items[i].shortcut->has_valid_event())) { if (rtl) { item_ofs.x = scroll_width + style->get_margin(SIDE_LEFT) + item_end_padding; } else { @@ -657,7 +659,7 @@ void PopupMenu::_draw_items() { } void PopupMenu::_draw_background() { - Ref<StyleBox> style = get_theme_stylebox("panel"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("panel")); RID ci2 = margin_container->get_canvas_item(); style->draw(ci2, Rect2(Point2(), margin_container->get_size())); } @@ -691,8 +693,8 @@ void PopupMenu::_shape_item(int p_item) { if (items.write[p_item].dirty) { items.write[p_item].text_buf->clear(); - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); if (items[p_item].text_direction == Control::TEXT_DIRECTION_INHERITED) { items.write[p_item].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); @@ -722,7 +724,7 @@ void PopupMenu::_notification(int p_what) { case Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { for (int i = 0; i < items.size(); i++) { - items.write[i].xl_text = tr(items[i].text); + items.write[i].xl_text = atr(items[i].text); items.write[i].dirty = true; _shape_item(i); } @@ -747,12 +749,12 @@ void PopupMenu::_notification(int p_what) { } break; case NOTIFICATION_INTERNAL_PROCESS: { //only used when using operating system windows - if (get_window_id() != DisplayServer::INVALID_WINDOW_ID && autohide_areas.size()) { + if (!is_embedded() && autohide_areas.size()) { Point2 mouse_pos = DisplayServer::get_singleton()->mouse_get_position(); mouse_pos -= get_position(); - for (List<Rect2>::Element *E = autohide_areas.front(); E; E = E->next()) { - if (!Rect2(Point2(), get_size()).has_point(mouse_pos) && E->get().has_point(mouse_pos)) { + for (const Rect2 &E : autohide_areas) { + if (!Rect2(Point2(), get_size()).has_point(mouse_pos) && E.has_point(mouse_pos)) { _close_pressed(); return; } @@ -786,12 +788,12 @@ void PopupMenu::_notification(int p_what) { set_process_internal(false); } else { - if (get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (!is_embedded()) { set_process_internal(true); } // Set margin on the margin container - Ref<StyleBox> panel_style = get_theme_stylebox("panel"); + Ref<StyleBox> panel_style = get_theme_stylebox(SNAME("panel")); margin_container->add_theme_constant_override("margin_top", panel_style->get_margin(Side::SIDE_TOP)); margin_container->add_theme_constant_override("margin_bottom", panel_style->get_margin(Side::SIDE_BOTTOM)); margin_container->add_theme_constant_override("margin_left", panel_style->get_margin(Side::SIDE_LEFT)); @@ -807,7 +809,7 @@ void PopupMenu::_notification(int p_what) { #define ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel) \ item.text = p_label; \ - item.xl_text = tr(p_label); \ + item.xl_text = atr(p_label); \ item.id = p_id == -1 ? items.size() : p_id; \ item.accel = p_accel; @@ -887,7 +889,7 @@ void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int ERR_FAIL_COND_MSG(p_shortcut.is_null(), "Cannot add item with invalid Shortcut."); \ _ref_shortcut(p_shortcut); \ item.text = p_shortcut->get_name(); \ - item.xl_text = tr(item.text); \ + item.xl_text = atr(item.text); \ item.id = p_id == -1 ? items.size() : p_id; \ item.shortcut = p_shortcut; \ item.shortcut_is_global = p_global; @@ -956,7 +958,7 @@ void PopupMenu::add_icon_radio_check_shortcut(const Ref<Texture2D> &p_icon, cons void PopupMenu::add_submenu_item(const String &p_label, const String &p_submenu, int p_id) { Item item; item.text = p_label; - item.xl_text = tr(p_label); + item.xl_text = atr(p_label); item.id = p_id == -1 ? items.size() : p_id; item.submenu = p_submenu; items.push_back(item); @@ -973,7 +975,7 @@ void PopupMenu::add_submenu_item(const String &p_label, const String &p_submenu, void PopupMenu::set_item_text(int p_idx, const String &p_text) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].text = p_text; - items.write[p_idx].xl_text = tr(p_text); + items.write[p_idx].xl_text = atr(p_text); _shape_item(p_idx); control->update(); @@ -1274,13 +1276,13 @@ int PopupMenu::get_item_count() const { } bool PopupMenu::activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only) { - uint32_t code = 0; + Key code = KEY_NONE; Ref<InputEventKey> k = p_event; if (k.is_valid()) { code = k->get_keycode(); - if (code == 0) { - code = k->get_unicode(); + if (code == KEY_NONE) { + code = (Key)k->get_unicode(); } if (k->is_ctrl_pressed()) { code |= KEY_MASK_CTRL; @@ -1301,7 +1303,7 @@ bool PopupMenu::activate_item_by_event(const Ref<InputEvent> &p_event, bool p_fo continue; } - if (items[i].shortcut.is_valid() && items[i].shortcut->is_shortcut(p_event) && (items[i].shortcut_is_global || !p_for_global_only)) { + if (items[i].shortcut.is_valid() && items[i].shortcut->matches_event(p_event) && (items[i].shortcut_is_global || !p_for_global_only)) { activate_item(i); return true; } @@ -1376,8 +1378,8 @@ void PopupMenu::activate_item(int p_item) { need_hide = false; } - emit_signal("id_pressed", id); - emit_signal("index_pressed", p_item); + emit_signal(SNAME("id_pressed"), id); + emit_signal(SNAME("index_pressed"), p_item); if (need_hide) { hide(); @@ -1402,7 +1404,7 @@ void PopupMenu::add_separator(const String &p_text, int p_id) { sep.id = p_id; if (p_text != String()) { sep.text = p_text; - sep.xl_text = tr(p_text); + sep.xl_text = atr(p_text); } items.push_back(sep); control->update(); @@ -1580,8 +1582,6 @@ void PopupMenu::take_mouse_focus() { } void PopupMenu::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &PopupMenu::_gui_input); - ClassDB::bind_method(D_METHOD("add_item", "label", "id", "accel"), &PopupMenu::add_item, DEFVAL(-1), DEFVAL(0)); ClassDB::bind_method(D_METHOD("add_icon_item", "texture", "label", "id", "accel"), &PopupMenu::add_icon_item, DEFVAL(-1), DEFVAL(0)); ClassDB::bind_method(D_METHOD("add_check_item", "label", "id", "accel"), &PopupMenu::add_check_item, DEFVAL(-1), DEFVAL(0)); @@ -1690,7 +1690,7 @@ PopupMenu::PopupMenu() { // Margin Container margin_container = memnew(MarginContainer); margin_container->set_anchors_and_offsets_preset(Control::PRESET_WIDE); - add_child(margin_container); + add_child(margin_container, false, INTERNAL_MODE_FRONT); margin_container->connect("draw", callable_mp(this, &PopupMenu::_draw_background)); // Scroll Container @@ -1704,22 +1704,22 @@ PopupMenu::PopupMenu() { control->set_anchors_and_offsets_preset(Control::PRESET_WIDE); control->set_h_size_flags(Control::SIZE_EXPAND_FILL); control->set_v_size_flags(Control::SIZE_EXPAND_FILL); - scroll_container->add_child(control); + scroll_container->add_child(control, false, INTERNAL_MODE_FRONT); control->connect("draw", callable_mp(this, &PopupMenu::_draw_items)); - connect("window_input", callable_mp(this, &PopupMenu::_gui_input)); + connect("window_input", callable_mp(this, &PopupMenu::gui_input)); submenu_timer = memnew(Timer); submenu_timer->set_wait_time(0.3); submenu_timer->set_one_shot(true); submenu_timer->connect("timeout", callable_mp(this, &PopupMenu::_submenu_timeout)); - add_child(submenu_timer); + add_child(submenu_timer, false, INTERNAL_MODE_FRONT); minimum_lifetime_timer = memnew(Timer); minimum_lifetime_timer->set_wait_time(0.3); minimum_lifetime_timer->set_one_shot(true); minimum_lifetime_timer->connect("timeout", callable_mp(this, &PopupMenu::_minimum_lifetime_timeout)); - add_child(minimum_lifetime_timer); + add_child(minimum_lifetime_timer, false, INTERNAL_MODE_FRONT); } PopupMenu::~PopupMenu() { diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index e4cbe984c9..428076c6da 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -31,10 +31,10 @@ #ifndef POPUP_MENU_H #define POPUP_MENU_H +#include "core/input/shortcut.h" #include "scene/gui/margin_container.h" #include "scene/gui/popup.h" #include "scene/gui/scroll_container.h" -#include "scene/gui/shortcut.h" #include "scene/resources/text_line.h" class PopupMenu : public Popup { @@ -80,8 +80,8 @@ class PopupMenu : public Popup { } Item() { - text_buf.instance(); - accel_text_buf.instance(); + text_buf.instantiate(); + accel_text_buf.instantiate(); checkable_type = CHECKABLE_TYPE_NONE; } }; @@ -107,7 +107,7 @@ class PopupMenu : public Popup { void _shape_item(int p_item); - void _gui_input(const Ref<InputEvent> &p_event); + virtual void gui_input(const Ref<InputEvent> &p_event); void _activate_submenu(int p_over); void _submenu_timeout(); @@ -140,11 +140,14 @@ class PopupMenu : public Popup { void _close_pressed(); protected: - friend class MenuButton; void _notification(int p_what); static void _bind_methods(); public: + // ATTENTION: This is used by the POT generator's scene parser. If the number of properties returned by `_get_items()` ever changes, + // this value should be updated to reflect the new size. + static const int ITEM_PROPERTY_SIZE = 10; + void add_item(const String &p_label, int p_id = -1, uint32_t p_accel = 0); void add_icon_item(const Ref<Texture2D> &p_icon, const String &p_label, int p_id = -1, uint32_t p_accel = 0); void add_check_item(const String &p_label, int p_id = -1, uint32_t p_accel = 0); diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index 6e8dfd5994..2cfaaa2fde 100644 --- a/scene/gui/progress_bar.cpp +++ b/scene/gui/progress_bar.cpp @@ -32,10 +32,10 @@ #include "scene/resources/text_line.h" Size2 ProgressBar::get_minimum_size() const { - Ref<StyleBox> bg = get_theme_stylebox("bg"); - Ref<StyleBox> fg = get_theme_stylebox("fg"); - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); + Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); + Ref<StyleBox> fg = get_theme_stylebox(SNAME("fg")); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); Size2 minimum_size = bg->get_minimum_size(); minimum_size.height = MAX(minimum_size.height, fg->get_minimum_size().height); @@ -53,11 +53,11 @@ Size2 ProgressBar::get_minimum_size() const { void ProgressBar::_notification(int p_what) { if (p_what == NOTIFICATION_DRAW) { - Ref<StyleBox> bg = get_theme_stylebox("bg"); - Ref<StyleBox> fg = get_theme_stylebox("fg"); - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); - Color font_color = get_theme_color("font_color"); + Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); + Ref<StyleBox> fg = get_theme_stylebox(SNAME("fg")); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); + Color font_color = get_theme_color(SNAME("font_color")); draw_style_box(bg, Rect2(Point2(), get_size())); float r = get_as_ratio(); @@ -75,8 +75,8 @@ void ProgressBar::_notification(int p_what) { String txt = TS->format_number(itos(int(get_as_ratio() * 100))) + TS->percent_sign(); TextLine tl = TextLine(txt, font, font_size); Vector2 text_pos = (Point2(get_size().width - tl.get_size().x, get_size().height - tl.get_size().y) / 2).round(); - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); if (outline_size > 0 && font_outline_color.a > 0) { tl.draw_outline(get_canvas_item(), text_pos, outline_size, font_outline_color); } diff --git a/scene/gui/range.cpp b/scene/gui/range.cpp index adc1ed67ca..92d4261d8d 100644 --- a/scene/gui/range.cpp +++ b/scene/gui/range.cpp @@ -42,7 +42,7 @@ TypedArray<String> Range::get_configuration_warnings() const { void Range::_value_changed_notify() { _value_changed(shared->val); - emit_signal("value_changed", shared->val); + emit_signal(SNAME("value_changed"), shared->val); update(); } @@ -57,7 +57,7 @@ void Range::Shared::emit_value_changed() { } void Range::_changed_notify(const char *p_what) { - emit_signal("changed"); + emit_signal(SNAME("changed")); update(); } @@ -265,7 +265,7 @@ void Range::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "step"), "set_step", "get_step"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "page"), "set_page", "get_page"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "value"), "set_value", "get_value"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ratio", PROPERTY_HINT_RANGE, "0,1,0.01", 0), "set_as_ratio", "get_as_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ratio", PROPERTY_HINT_RANGE, "0,1,0.01", PROPERTY_USAGE_NONE), "set_as_ratio", "get_as_ratio"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "exp_edit"), "set_exp_ratio", "is_ratio_exp"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "rounded"), "set_use_rounded_values", "is_using_rounded_values"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_greater"), "set_allow_greater", "is_greater_allowed"); diff --git a/scene/gui/rich_text_effect.cpp b/scene/gui/rich_text_effect.cpp index 39718a269a..236d106af8 100644 --- a/scene/gui/rich_text_effect.cpp +++ b/scene/gui/rich_text_effect.cpp @@ -32,8 +32,15 @@ #include "core/object/script_language.h" -void RichTextEffect::_bind_methods() { - BIND_VMETHOD(MethodInfo(Variant::BOOL, "_process_custom_fx", PropertyInfo(Variant::OBJECT, "char_fx", PROPERTY_HINT_RESOURCE_TYPE, "CharFXTransform"))); +CharFXTransform::CharFXTransform() { +} + +CharFXTransform::~CharFXTransform() { + environment.clear(); +} + +void RichTextEffect::_bind_methods(){ + GDVIRTUAL_BIND(_process_custom_fx, "char_fx") } Variant RichTextEffect::get_bbcode() const { @@ -49,15 +56,10 @@ Variant RichTextEffect::get_bbcode() const { bool RichTextEffect::_process_effect_impl(Ref<CharFXTransform> p_cfx) { bool return_value = false; - if (get_script_instance()) { - Variant v = get_script_instance()->call("_process_custom_fx", p_cfx); - if (v.get_type() != Variant::BOOL) { - return_value = false; - } else { - return_value = (bool)v; - } + if (GDVIRTUAL_CALL(_process_custom_fx, p_cfx, return_value)) { + return return_value; } - return return_value; + return false; } RichTextEffect::RichTextEffect() { @@ -101,10 +103,3 @@ void CharFXTransform::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "glyph_index"), "set_glyph_index", "get_glyph_index"); ADD_PROPERTY(PropertyInfo(Variant::RID, "font"), "set_font", "get_font"); } - -CharFXTransform::CharFXTransform() { -} - -CharFXTransform::~CharFXTransform() { - environment.clear(); -} diff --git a/scene/gui/rich_text_effect.h b/scene/gui/rich_text_effect.h index f2e2823eff..f5506542bb 100644 --- a/scene/gui/rich_text_effect.h +++ b/scene/gui/rich_text_effect.h @@ -32,23 +32,11 @@ #define RICH_TEXT_EFFECT_H #include "core/io/resource.h" +#include "core/object/gdvirtual.gen.inc" +#include "core/object/script_language.h" -class RichTextEffect : public Resource { - GDCLASS(RichTextEffect, Resource); - OBJ_SAVE_TYPE(RichTextEffect); - -protected: - static void _bind_methods(); - -public: - Variant get_bbcode() const; - bool _process_effect_impl(Ref<class CharFXTransform> p_cfx); - - RichTextEffect(); -}; - -class CharFXTransform : public Reference { - GDCLASS(CharFXTransform, Reference); +class CharFXTransform : public RefCounted { + GDCLASS(CharFXTransform, RefCounted); protected: static void _bind_methods(); @@ -59,9 +47,9 @@ public: bool outline = false; Point2 offset; Color color; - float elapsed_time = 0.0f; + double elapsed_time = 0.0f; Dictionary environment; - uint32_t glpyh_index = 0; + uint32_t glyph_index = 0; RID font; CharFXTransform(); @@ -69,8 +57,8 @@ public: Vector2i get_range() { return range; } void set_range(const Vector2i &p_range) { range = p_range; } - float get_elapsed_time() { return elapsed_time; } - void set_elapsed_time(float p_elapsed_time) { elapsed_time = p_elapsed_time; } + double get_elapsed_time() { return elapsed_time; } + void set_elapsed_time(double p_elapsed_time) { elapsed_time = p_elapsed_time; } bool is_visible() { return visibility; } void set_visibility(bool p_visibility) { visibility = p_visibility; } bool is_outline() { return outline; } @@ -80,8 +68,8 @@ public: Color get_color() { return color; } void set_color(Color p_color) { color = p_color; } - uint32_t get_glyph_index() const { return glpyh_index; }; - void set_glyph_index(uint32_t p_glpyh_index) { glpyh_index = p_glpyh_index; }; + uint32_t get_glyph_index() const { return glyph_index; }; + void set_glyph_index(uint32_t p_glyph_index) { glyph_index = p_glyph_index; }; RID get_font() const { return font; }; void set_font(RID p_font) { font = p_font; }; @@ -89,4 +77,20 @@ public: void set_environment(Dictionary p_environment) { environment = p_environment; } }; +class RichTextEffect : public Resource { + GDCLASS(RichTextEffect, Resource); + OBJ_SAVE_TYPE(RichTextEffect); + +protected: + static void _bind_methods(); + + GDVIRTUAL1RC(bool, _process_custom_fx, Ref<CharFXTransform>) + +public: + Variant get_bbcode() const; + bool _process_effect_impl(Ref<class CharFXTransform> p_cfx); + + RichTextEffect(); +}; + #endif // RICH_TEXT_EFFECT_H diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index a2c3b4ed8a..562bac60c2 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -136,7 +136,7 @@ RichTextLabel::Item *RichTextLabel::_get_prev_item(Item *p_item, bool p_free) co } Rect2 RichTextLabel::_get_text_rect() { - Ref<StyleBox> style = get_theme_stylebox("normal"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); return Rect2(style->get_offset(), get_size() - style->get_minimum_size()); } @@ -229,8 +229,8 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> switch (it->type) { case ITEM_TABLE: { ItemTable *table = static_cast<ItemTable *>(it); - int hseparation = get_theme_constant("table_hseparation"); - int vseparation = get_theme_constant("table_vseparation"); + int hseparation = get_theme_constant(SNAME("table_hseparation")); + int vseparation = get_theme_constant(SNAME("table_vseparation")); int col_count = table->columns.size(); for (int i = 0; i < col_count; i++) { @@ -238,9 +238,9 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> } int idx = 0; - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + for (Item *E : table->subitems) { + ERR_CONTINUE(E->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E); for (int i = 0; i < frame->lines.size(); i++) { _resize_line(frame, i, p_base_font, p_base_font_size, 1); } @@ -316,9 +316,9 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> Vector2 offset; float row_height = 0.0; - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + for (Item *E : table->subitems) { + ERR_CONTINUE(E->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E); int column = idx % col_count; @@ -366,7 +366,7 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> } if (p_line > 0) { - l.offset.y = p_frame->lines[p_line - 1].offset.y + p_frame->lines[p_line - 1].text_buf->get_size().y; + l.offset.y = p_frame->lines[p_line - 1].offset.y + p_frame->lines[p_line - 1].text_buf->get_size().y + get_theme_constant(SNAME("line_separation")); } else { l.offset.y = 0; } @@ -458,8 +458,8 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> } break; case ITEM_TABLE: { ItemTable *table = static_cast<ItemTable *>(it); - int hseparation = get_theme_constant("table_hseparation"); - int vseparation = get_theme_constant("table_vseparation"); + int hseparation = get_theme_constant(SNAME("table_hseparation")); + int vseparation = get_theme_constant(SNAME("table_vseparation")); int col_count = table->columns.size(); int t_char_count = 0; // Set minimums to zero. @@ -472,9 +472,9 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> const int available_width = p_width - hseparation * (col_count - 1); int idx = 0; - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + for (Item *E : table->subitems) { + ERR_CONTINUE(E->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E); int column = idx % col_count; for (int i = 0; i < frame->lines.size(); i++) { @@ -556,7 +556,7 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> Vector2 offset; float row_height = 0.0; - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { + for (const List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. ItemFrame *frame = static_cast<ItemFrame *>(E->get()); @@ -614,7 +614,7 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> *r_char_offset = l.char_offset + l.char_count; if (p_line > 0) { - l.offset.y = p_frame->lines[p_line - 1].offset.y + p_frame->lines[p_line - 1].text_buf->get_size().y; + l.offset.y = p_frame->lines[p_line - 1].offset.y + p_frame->lines[p_line - 1].text_buf->get_size().y + get_theme_constant(SNAME("line_separation")); } else { l.offset.y = 0; } @@ -672,11 +672,11 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o if (prefix != "") { Ref<Font> font = _find_font(l.from); if (font.is_null()) { - font = get_theme_font("normal_font"); + font = get_theme_font(SNAME("normal_font")); } int font_size = _find_font_size(l.from); if (font_size == -1) { - font_size = get_theme_font_size("normal_font_size"); + font_size = get_theme_font_size(SNAME("normal_font_size")); } if (rtl) { float offx = 0.0f; @@ -775,16 +775,16 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } break; case ITEM_TABLE: { ItemTable *table = static_cast<ItemTable *>(it); - Color odd_row_bg = get_theme_color("table_odd_row_bg"); - Color even_row_bg = get_theme_color("table_even_row_bg"); - Color border = get_theme_color("table_border"); - int hseparation = get_theme_constant("table_hseparation"); + Color odd_row_bg = get_theme_color(SNAME("table_odd_row_bg")); + Color even_row_bg = get_theme_color(SNAME("table_even_row_bg")); + Color border = get_theme_color(SNAME("table_border")); + int hseparation = get_theme_constant(SNAME("table_hseparation")); int col_count = table->columns.size(); int row_count = table->rows.size(); int idx = 0; - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + for (Item *E : table->subitems) { + ItemFrame *frame = static_cast<ItemFrame *>(E); int col = idx % col_count; int row = idx / col_count; @@ -874,7 +874,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o charfx->visibility = visible; charfx->outline = true; charfx->font = frid; - charfx->glpyh_index = gl; + charfx->glyph_index = gl; charfx->offset = fx_offset; charfx->color = font_color; @@ -884,7 +884,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o fx_offset += charfx->offset; font_color = charfx->color; frid = charfx->font; - gl = charfx->glpyh_index; + gl = charfx->glyph_index; visible &= charfx->visibility; } } else if (item_fx->type == ITEM_SHAKE) { @@ -916,7 +916,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } } - Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); + Point2 shadow_ofs(get_theme_constant(SNAME("shadow_offset_x")), get_theme_constant(SNAME("shadow_offset_y"))); // Draw glyph outlines. for (int j = 0; j < glyphs[i].repeat; j++) { @@ -934,9 +934,14 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } } + Vector2 fbg_line_off = off + p_ofs; + // Draw background color box + Vector2i chr_range = TS->shaped_text_get_range(rid); + _draw_fbg_boxes(ci, rid, fbg_line_off, it_from, it_to, chr_range.x, chr_range.y, 0); + // Draw main text. - Color selection_fg = get_theme_color("font_selected_color"); - Color selection_bg = get_theme_color("selection_color"); + Color selection_fg = get_theme_color(SNAME("font_selected_color")); + Color selection_bg = get_theme_color(SNAME("selection_color")); int sel_start = -1; int sel_end = -1; @@ -1021,7 +1026,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o charfx->visibility = visible; charfx->outline = false; charfx->font = frid; - charfx->glpyh_index = gl; + charfx->glyph_index = gl; charfx->offset = fx_offset; charfx->color = font_color; @@ -1031,7 +1036,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o fx_offset += charfx->offset; font_color = charfx->color; frid = charfx->font; - gl = charfx->glpyh_index; + gl = charfx->glyph_index; visible &= charfx->visibility; } } else if (item_fx->type == ITEM_SHAKE) { @@ -1079,6 +1084,9 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o off.x += glyphs[i].advance; } } + // Draw foreground color box + _draw_fbg_boxes(ci, rid, fbg_line_off, it_from, it_to, chr_range.x, chr_range.y, 1); + off.y += TS->shaped_text_get_descent(rid) + l.text_buf->get_spacing_bottom(); } @@ -1119,7 +1127,7 @@ void RichTextLabel::_find_click(ItemFrame *p_frame, const Point2i &p_click, Item Point2 ofs = text_rect.get_position() + Vector2(0, main->lines[from_line].offset.y - vofs); while (ofs.y < size.height && from_line < main->lines.size()) { _find_click_in_line(p_frame, from_line, ofs, text_rect.size.x, p_click, r_click_frame, r_click_line, r_click_item, r_click_char); - ofs.y += main->lines[from_line].text_buf->get_size().y; + ofs.y += main->lines[from_line].text_buf->get_size().y + get_theme_constant(SNAME("line_separation")); if (((r_click_item != nullptr) && ((*r_click_item) != nullptr)) || ((r_click_frame != nullptr) && ((*r_click_frame) != nullptr))) { if (r_outside != nullptr) { *r_outside = false; @@ -1184,8 +1192,8 @@ float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const V if (rect.has_point(p_click - p_ofs - off)) { switch (it->type) { case ITEM_TABLE: { - int hseparation = get_theme_constant("table_hseparation"); - int vseparation = get_theme_constant("table_vseparation"); + int hseparation = get_theme_constant(SNAME("table_hseparation")); + int vseparation = get_theme_constant(SNAME("table_vseparation")); ItemTable *table = static_cast<ItemTable *>(it); @@ -1195,8 +1203,8 @@ float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const V int col_count = table->columns.size(); int row_count = table->rows.size(); - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + for (Item *E : table->subitems) { + ItemFrame *frame = static_cast<ItemFrame *>(E); int col = idx % col_count; int row = idx / col_count; @@ -1315,7 +1323,7 @@ void RichTextLabel::_update_scroll() { } } -void RichTextLabel::_update_fx(RichTextLabel::ItemFrame *p_frame, float p_delta_time) { +void RichTextLabel::_update_fx(RichTextLabel::ItemFrame *p_frame, double p_delta_time) { Item *it = p_frame; while (it) { ItemFX *ifx = nullptr; @@ -1354,7 +1362,7 @@ void RichTextLabel::_notification(int p_what) { case NOTIFICATION_MOUSE_EXIT: { if (meta_hovering) { meta_hovering = nullptr; - emit_signal("meta_hover_ended", current_meta); + emit_signal(SNAME("meta_hover_ended"), current_meta); current_meta = false; update(); } @@ -1387,11 +1395,11 @@ void RichTextLabel::_notification(int p_what) { Size2 size = get_size(); Rect2 text_rect = _get_text_rect(); - draw_style_box(get_theme_stylebox("normal"), Rect2(Point2(), size)); + draw_style_box(get_theme_stylebox(SNAME("normal")), Rect2(Point2(), size)); if (has_focus()) { RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, true); - draw_style_box(get_theme_stylebox("focus"), Rect2(Point2(), size)); + draw_style_box(get_theme_stylebox(SNAME("focus")), Rect2(Point2(), size)); RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, false); } @@ -1411,13 +1419,13 @@ void RichTextLabel::_notification(int p_what) { if (from_line >= main->lines.size()) { break; //nothing to draw } - Ref<Font> base_font = get_theme_font("normal_font"); - Color base_color = get_theme_color("default_color"); - Color outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); - Color font_shadow_color = get_theme_color("font_shadow_color"); - bool use_outline = get_theme_constant("shadow_as_outline"); - Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); + Ref<Font> base_font = get_theme_font(SNAME("normal_font")); + Color base_color = get_theme_color(SNAME("default_color")); + Color outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); + Color font_shadow_color = get_theme_color(SNAME("font_shadow_color")); + bool use_outline = get_theme_constant(SNAME("shadow_as_outline")); + Point2 shadow_ofs(get_theme_constant(SNAME("shadow_offset_x")), get_theme_constant(SNAME("shadow_offset_y"))); visible_paragraph_count = 0; visible_line_count = 0; @@ -1427,13 +1435,13 @@ void RichTextLabel::_notification(int p_what) { while (ofs.y < size.height && from_line < main->lines.size()) { visible_paragraph_count++; visible_line_count += _draw_line(main, from_line, ofs, text_rect.size.x, base_color, outline_size, outline_color, font_shadow_color, use_outline, shadow_ofs); - ofs.y += main->lines[from_line].text_buf->get_size().y; + ofs.y += main->lines[from_line].text_buf->get_size().y + get_theme_constant(SNAME("line_separation")); from_line++; } } break; case NOTIFICATION_INTERNAL_PROCESS: { if (is_visible_in_tree()) { - float dt = get_process_delta_time(); + double dt = get_process_delta_time(); _update_fx(main, dt); update(); } @@ -1443,7 +1451,7 @@ void RichTextLabel::_notification(int p_what) { Control::CursorShape RichTextLabel::get_cursor_shape(const Point2 &p_pos) const { if (!underline_meta) { - return CURSOR_ARROW; + return get_default_cursor_shape(); } if (selection.click_item) { @@ -1451,11 +1459,11 @@ Control::CursorShape RichTextLabel::get_cursor_shape(const Point2 &p_pos) const } if (main->first_invalid_line < main->lines.size()) { - return CURSOR_ARROW; //invalid + return get_default_cursor_shape(); //invalid } if (main->first_resized_line < main->lines.size()) { - return CURSOR_ARROW; //invalid + return get_default_cursor_shape(); //invalid } Item *item = nullptr; @@ -1466,10 +1474,10 @@ Control::CursorShape RichTextLabel::get_cursor_shape(const Point2 &p_pos) const return CURSOR_POINTING_HAND; } - return CURSOR_ARROW; + return get_default_cursor_shape(); } -void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { +void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventMouseButton> b = p_event; @@ -1560,7 +1568,7 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { Variant meta; if (!outside && _find_meta(c_item, &meta)) { //meta clicked - emit_signal("meta_clicked", meta); + emit_signal(SNAME("meta_clicked"), meta); } } } @@ -1603,11 +1611,11 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { handled = true; } if (k->is_action("ui_up") && vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() - get_theme_font("normal_font")->get_height(get_theme_font_size("normal_font_size"))); + vscroll->set_value(vscroll->get_value() - get_theme_font(SNAME("normal_font"))->get_height(get_theme_font_size(SNAME("normal_font_size")))); handled = true; } if (k->is_action("ui_down") && vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() + get_theme_font("normal_font")->get_height(get_theme_font_size("normal_font_size"))); + vscroll->set_value(vscroll->get_value() + get_theme_font(SNAME("normal_font"))->get_height(get_theme_font_size(SNAME("normal_font_size")))); handled = true; } if (k->is_action("ui_home") && vscroll->is_visible_in_tree()) { @@ -1685,15 +1693,15 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { if (c_item && !outside && _find_meta(c_item, &meta, &item_meta)) { if (meta_hovering != item_meta) { if (meta_hovering) { - emit_signal("meta_hover_ended", current_meta); + emit_signal(SNAME("meta_hover_ended"), current_meta); } meta_hovering = item_meta; current_meta = meta; - emit_signal("meta_hover_started", meta); + emit_signal(SNAME("meta_hover_started"), meta); } } else if (meta_hovering) { meta_hovering = nullptr; - emit_signal("meta_hover_ended", current_meta); + emit_signal(SNAME("meta_hover_ended"), current_meta); current_meta = false; } } @@ -2036,14 +2044,44 @@ bool RichTextLabel::_find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item) return false; } +Color RichTextLabel::_find_bgcolor(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_BGCOLOR) { + ItemBGColor *color = static_cast<ItemBGColor *>(item); + return color->color; + } + + item = item->parent; + } + + return Color(0, 0, 0, 0); +} + +Color RichTextLabel::_find_fgcolor(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_FGCOLOR) { + ItemFGColor *color = static_cast<ItemFGColor *>(item); + return color->color; + } + + item = item->parent; + } + + return Color(0, 0, 0, 0); +} + bool RichTextLabel::_find_layout_subitem(Item *from, Item *to) { if (from && from != to) { if (from->type != ITEM_FONT && from->type != ITEM_COLOR && from->type != ITEM_UNDERLINE && from->type != ITEM_STRIKETHROUGH) { return true; } - for (List<Item *>::Element *E = from->subitems.front(); E; E = E->next()) { - bool layout = _find_layout_subitem(E->get(), to); + for (Item *E : from->subitems) { + bool layout = _find_layout_subitem(E, to); if (layout) { return true; @@ -2067,8 +2105,8 @@ void RichTextLabel::_validate_line_caches(ItemFrame *p_frame) { } Rect2 text_rect = _get_text_rect(); - Ref<Font> base_font = get_theme_font("normal_font"); - int base_font_size = get_theme_font_size("normal_font_size"); + Ref<Font> base_font = get_theme_font(SNAME("normal_font")); + int base_font_size = get_theme_font_size(SNAME("normal_font_size")); for (int i = p_frame->first_resized_line; i < p_frame->lines.size(); i++) { _resize_line(p_frame, i, base_font, base_font_size, text_rect.get_size().width - scroll_w); @@ -2102,8 +2140,8 @@ void RichTextLabel::_validate_line_caches(ItemFrame *p_frame) { } Rect2 text_rect = _get_text_rect(); - Ref<Font> base_font = get_theme_font("normal_font"); - int base_font_size = get_theme_font_size("normal_font_size"); + Ref<Font> base_font = get_theme_font(SNAME("normal_font")); + int base_font_size = get_theme_font_size(SNAME("normal_font_size")); int total_chars = (p_frame->first_invalid_line == 0) ? 0 : (p_frame->lines[p_frame->first_invalid_line].char_offset + p_frame->lines[p_frame->first_invalid_line].char_count); for (int i = p_frame->first_invalid_line; i < p_frame->lines.size(); i++) { @@ -2251,7 +2289,7 @@ void RichTextLabel::_remove_item(Item *p_item, const int p_line, const int p_sub } } -void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color, VAlign p_align) { +void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlign p_align) { if (current->type == ITEM_TABLE) { return; } @@ -2368,35 +2406,35 @@ void RichTextLabel::push_font(const Ref<Font> &p_font) { } void RichTextLabel::push_normal() { - Ref<Font> normal_font = get_theme_font("normal_font"); + Ref<Font> normal_font = get_theme_font(SNAME("normal_font")); ERR_FAIL_COND(normal_font.is_null()); push_font(normal_font); } void RichTextLabel::push_bold() { - Ref<Font> bold_font = get_theme_font("bold_font"); + Ref<Font> bold_font = get_theme_font(SNAME("bold_font")); ERR_FAIL_COND(bold_font.is_null()); push_font(bold_font); } void RichTextLabel::push_bold_italics() { - Ref<Font> bold_italics_font = get_theme_font("bold_italics_font"); + Ref<Font> bold_italics_font = get_theme_font(SNAME("bold_italics_font")); ERR_FAIL_COND(bold_italics_font.is_null()); push_font(bold_italics_font); } void RichTextLabel::push_italics() { - Ref<Font> italics_font = get_theme_font("italics_font"); + Ref<Font> italics_font = get_theme_font(SNAME("italics_font")); ERR_FAIL_COND(italics_font.is_null()); push_font(italics_font); } void RichTextLabel::push_mono() { - Ref<Font> mono_font = get_theme_font("mono_font"); + Ref<Font> mono_font = get_theme_font(SNAME("mono_font")); ERR_FAIL_COND(mono_font.is_null()); push_font(mono_font); @@ -2496,7 +2534,7 @@ void RichTextLabel::push_meta(const Variant &p_meta) { _add_item(item, true); } -void RichTextLabel::push_table(int p_columns, VAlign p_align) { +void RichTextLabel::push_table(int p_columns, InlineAlign p_align) { ERR_FAIL_COND(p_columns < 1); ItemTable *item = memnew(ItemTable); @@ -2546,6 +2584,22 @@ void RichTextLabel::push_rainbow(float p_saturation, float p_value, float p_freq _add_item(item, true); } +void RichTextLabel::push_bgcolor(const Color &p_color) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemBGColor *item = memnew(ItemBGColor); + + item->color = p_color; + _add_item(item, true); +} + +void RichTextLabel::push_fgcolor(const Color &p_color) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemFGColor *item = memnew(ItemFGColor); + + item->color = p_color; + _add_item(item, true); +} + void RichTextLabel::push_customfx(Ref<RichTextEffect> p_custom_effect, Dictionary p_environment) { ItemCustomFX *item = memnew(ItemCustomFX); item->custom_effect = p_custom_effect; @@ -2720,13 +2774,13 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { int pos = 0; List<String> tag_stack; - Ref<Font> normal_font = get_theme_font("normal_font"); - Ref<Font> bold_font = get_theme_font("bold_font"); - Ref<Font> italics_font = get_theme_font("italics_font"); - Ref<Font> bold_italics_font = get_theme_font("bold_italics_font"); - Ref<Font> mono_font = get_theme_font("mono_font"); + Ref<Font> normal_font = get_theme_font(SNAME("normal_font")); + Ref<Font> bold_font = get_theme_font(SNAME("bold_font")); + Ref<Font> italics_font = get_theme_font(SNAME("italics_font")); + Ref<Font> bold_italics_font = get_theme_font(SNAME("bold_italics_font")); + Ref<Font> mono_font = get_theme_font(SNAME("mono_font")); - Color base_color = get_theme_color("default_color"); + Color base_color = get_theme_color(SNAME("default_color")); int indent_level = 0; @@ -2843,18 +2897,35 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { columns = 1; } - VAlign align = VALIGN_TOP; - if (subtag.size() > 1) { + int align = INLINE_ALIGN_TOP; + if (subtag.size() > 2) { if (subtag[1] == "top" || subtag[1] == "t") { - align = VALIGN_TOP; + align = INLINE_ALIGN_TOP_TO; } else if (subtag[1] == "center" || subtag[1] == "c") { - align = VALIGN_CENTER; + align = INLINE_ALIGN_CENTER_TO; } else if (subtag[1] == "bottom" || subtag[1] == "b") { - align = VALIGN_BOTTOM; + align = INLINE_ALIGN_BOTTOM_TO; + } + if (subtag[2] == "top" || subtag[2] == "t") { + align |= INLINE_ALIGN_TO_TOP; + } else if (subtag[2] == "center" || subtag[2] == "c") { + align |= INLINE_ALIGN_TO_CENTER; + } else if (subtag[2] == "baseline" || subtag[2] == "l") { + align |= INLINE_ALIGN_TO_BASELINE; + } else if (subtag[2] == "bottom" || subtag[2] == "b") { + align |= INLINE_ALIGN_TO_BOTTOM; + } + } else if (subtag.size() > 1) { + if (subtag[1] == "top" || subtag[1] == "t") { + align = INLINE_ALIGN_TOP; + } else if (subtag[1] == "center" || subtag[1] == "c") { + align = INLINE_ALIGN_CENTER; + } else if (subtag[1] == "bottom" || subtag[1] == "b") { + align = INLINE_ALIGN_BOTTOM; } } - push_table(columns, align); + push_table(columns, (InlineAlign)align); pos = brk_end + 1; tag_stack.push_front("table"); } else if (tag == "cell") { @@ -3086,11 +3157,11 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { tag_stack.push_front("url"); } else if (tag.begins_with("dropcap")) { Vector<String> subtag = tag.substr(5, tag.length()).split(" "); - Ref<Font> f = get_theme_font("normal_font"); - int fs = get_theme_font_size("normal_font_size") * 3; - Color color = get_theme_color("default_color"); - Color outline_color = get_theme_color("outline_color"); - int outline_size = get_theme_constant("outline_size"); + Ref<Font> f = get_theme_font(SNAME("normal_font")); + int fs = get_theme_font_size(SNAME("normal_font_size")) * 3; + Color color = get_theme_color(SNAME("default_color")); + Color outline_color = get_theme_color(SNAME("outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); Rect2 dropcap_margins = Rect2(); for (int i = 0; i < subtag.size(); i++) { @@ -3133,15 +3204,34 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { pos = end; tag_stack.push_front(bbcode_name); } else if (tag.begins_with("img")) { - VAlign align = VALIGN_TOP; + int align = INLINE_ALIGN_CENTER; if (tag.begins_with("img=")) { - String al = tag.substr(4, tag.length()); - if (al == "top" || al == "t") { - align = VALIGN_TOP; - } else if (al == "center" || al == "c") { - align = VALIGN_CENTER; - } else if (al == "bottom" || al == "b") { - align = VALIGN_BOTTOM; + Vector<String> subtag = tag.substr(4, tag.length()).split(","); + if (subtag.size() > 1) { + if (subtag[0] == "top" || subtag[0] == "t") { + align = INLINE_ALIGN_TOP_TO; + } else if (subtag[0] == "center" || subtag[0] == "c") { + align = INLINE_ALIGN_CENTER_TO; + } else if (subtag[0] == "bottom" || subtag[0] == "b") { + align = INLINE_ALIGN_BOTTOM_TO; + } + if (subtag[1] == "top" || subtag[1] == "t") { + align |= INLINE_ALIGN_TO_TOP; + } else if (subtag[1] == "center" || subtag[1] == "c") { + align |= INLINE_ALIGN_TO_CENTER; + } else if (subtag[1] == "baseline" || subtag[1] == "l") { + align |= INLINE_ALIGN_TO_BASELINE; + } else if (subtag[1] == "bottom" || subtag[1] == "b") { + align |= INLINE_ALIGN_TO_BOTTOM; + } + } else if (subtag.size() > 0) { + if (subtag[0] == "top" || subtag[0] == "t") { + align = INLINE_ALIGN_TOP; + } else if (subtag[0] == "center" || subtag[0] == "c") { + align = INLINE_ALIGN_CENTER; + } else if (subtag[0] == "bottom" || subtag[0] == "b") { + align = INLINE_ALIGN_BOTTOM; + } } } @@ -3182,7 +3272,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { } } - add_image(texture, width, height, color, align); + add_image(texture, width, height, color, (InlineAlign)align); } pos = end; @@ -3352,6 +3442,23 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { pos = brk_end + 1; tag_stack.push_front("rainbow"); set_process_internal(true); + + } else if (tag.begins_with("bgcolor=")) { + String color_str = tag.substr(8, tag.length()); + Color color = Color::from_string(color_str, base_color); + + push_bgcolor(color); + pos = brk_end + 1; + tag_stack.push_front("bgcolor"); + + } else if (tag.begins_with("fgcolor=")) { + String color_str = tag.substr(8, tag.length()); + Color color = Color::from_string(color_str, base_color); + + push_fgcolor(color); + pos = brk_end + 1; + tag_stack.push_front("fgcolor"); + } else { Vector<String> &expr = split_tag_block; if (expr.size() < 1) { @@ -3377,8 +3484,8 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { } Vector<ItemFX *> fx_items; - for (List<Item *>::Element *E = main->subitems.front(); E; E = E->next()) { - Item *subitem = static_cast<Item *>(E->get()); + for (Item *E : main->subitems) { + Item *subitem = static_cast<Item *>(E); _fetch_item_fx_stack(subitem, fx_items); if (fx_items.size()) { @@ -3452,7 +3559,30 @@ void RichTextLabel::set_selection_enabled(bool p_enabled) { } } -bool RichTextLabel::_search_line(ItemFrame *p_frame, int p_line, const String &p_string, Item *p_from, Item *p_to) { +bool RichTextLabel::_search_table(ItemTable *p_table, List<Item *>::Element *p_from, const String &p_string, bool p_reverse_search) { + List<Item *>::Element *E = p_from; + while (E != nullptr) { + ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + if (p_reverse_search) { + for (int i = frame->lines.size() - 1; i >= 0; i--) { + if (_search_line(frame, i, p_string, -1, p_reverse_search)) { + return true; + } + } + } else { + for (int i = 0; i < frame->lines.size(); i++) { + if (_search_line(frame, i, p_string, 0, p_reverse_search)) { + return true; + } + } + } + E = p_reverse_search ? E->prev() : E->next(); + } + return false; +} + +bool RichTextLabel::_search_line(ItemFrame *p_frame, int p_line, const String &p_string, int p_char_idx, bool p_reverse_search) { ERR_FAIL_COND_V(p_frame == nullptr, false); ERR_FAIL_COND_V(p_line < 0 || p_line >= p_frame->lines.size(), false); @@ -3474,24 +3604,23 @@ bool RichTextLabel::_search_line(ItemFrame *p_frame, int p_line, const String &p } break; case ITEM_TABLE: { ItemTable *table = static_cast<ItemTable *>(it); - int idx = 0; - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); - - for (int i = 0; i < frame->lines.size(); i++) { - if (_search_line(frame, i, p_string, p_from, p_to)) { - return true; - } - } - idx++; + List<Item *>::Element *E = p_reverse_search ? table->subitems.back() : table->subitems.front(); + if (_search_table(table, E, p_string, p_reverse_search)) { + return true; } } break; default: break; } } - int sp = text.findn(p_string, 0); + + int sp = -1; + if (p_reverse_search) { + sp = text.rfindn(p_string, p_char_idx); + } else { + sp = text.findn(p_string, p_char_idx); + } + if (sp != -1) { selection.from_frame = p_frame; selection.from_line = p_line; @@ -3499,8 +3628,8 @@ bool RichTextLabel::_search_line(ItemFrame *p_frame, int p_line, const String &p selection.from_char = sp; selection.to_frame = p_frame; selection.to_line = p_line; - selection.to_item = _get_item_at_pos(l.from, it_to, sp + p_string.length() - 1); - selection.to_char = sp + p_string.length() - 1; + selection.to_item = _get_item_at_pos(l.from, it_to, sp + p_string.length()); + selection.to_char = sp + p_string.length(); selection.active = true; return true; } @@ -3511,23 +3640,81 @@ bool RichTextLabel::_search_line(ItemFrame *p_frame, int p_line, const String &p bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p_search_previous) { ERR_FAIL_COND_V(!selection.enabled, false); + if (p_string.size() == 0) { + selection.active = false; + return false; + } + + int char_idx = p_search_previous ? -1 : 0; + int current_line = 0; + int ending_line = main->lines.size() - 1; if (p_from_selection && selection.active) { - for (int i = 0; i < main->lines.size(); i++) { - if (_search_line(main, i, p_string, selection.from_item, selection.to_item)) { - update(); - return true; - } + // First check to see if other results exist in current line + char_idx = p_search_previous ? selection.from_char - 1 : selection.to_char; + if (!(p_search_previous && char_idx < 0) && + _search_line(selection.from_frame, selection.from_line, p_string, char_idx, p_search_previous)) { + scroll_to_line(selection.from_frame->line + selection.from_line); + update(); + return true; } - } else { - for (int i = 0; i < main->lines.size(); i++) { - if (_search_line(main, i, p_string, nullptr, nullptr)) { - update(); - return true; + char_idx = p_search_previous ? -1 : 0; + + // Next, check to see if the current search result is in a table + if (selection.from_frame->parent != nullptr && selection.from_frame->parent->type == ITEM_TABLE) { + // Find last search result in table + ItemTable *parent_table = static_cast<ItemTable *>(selection.from_frame->parent); + List<Item *>::Element *parent_element = p_search_previous ? parent_table->subitems.back() : parent_table->subitems.front(); + + while (parent_element->get() != selection.from_frame) { + parent_element = p_search_previous ? parent_element->prev() : parent_element->next(); + ERR_FAIL_COND_V(parent_element == nullptr, false); + } + + // Search remainder of table + if (!(p_search_previous && parent_element == parent_table->subitems.front()) && + parent_element != parent_table->subitems.back()) { + parent_element = p_search_previous ? parent_element->prev() : parent_element->next(); // Don't want to search current item + ERR_FAIL_COND_V(parent_element == nullptr, false); + + // Search for next element + if (_search_table(parent_table, parent_element, p_string, p_search_previous)) { + scroll_to_line(selection.from_frame->line + selection.from_line); + update(); + return true; + } } } + + ending_line = selection.from_frame->line + selection.from_line; + current_line = p_search_previous ? ending_line - 1 : ending_line + 1; + } else if (p_search_previous) { + current_line = ending_line; + ending_line = 0; } - return false; + // Search remainder of the file + while (current_line != ending_line) { + // Wrap around + if (current_line < 0) { + current_line = main->lines.size() - 1; + } else if (current_line >= main->lines.size()) { + current_line = 0; + } + + if (_search_line(main, current_line, p_string, char_idx, p_search_previous)) { + scroll_to_line(current_line); + update(); + return true; + } + p_search_previous ? current_line-- : current_line++; + } + + if (p_from_selection && selection.active) { + // Check contents of selection + return _search_line(main, current_line, p_string, char_idx, p_search_previous); + } else { + return false; + } } String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line, Selection p_selection) const { @@ -3568,9 +3755,9 @@ String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line, Selection p ItemTable *table = static_cast<ItemTable *>(it); int idx = 0; int col_count = table->columns.size(); - for (List<Item *>::Element *E = table->subitems.front(); E; E = E->next()) { - ERR_CONTINUE(E->get()->type != ITEM_FRAME); // Children should all be frames. - ItemFrame *frame = static_cast<ItemFrame *>(E->get()); + for (Item *E : table->subitems) { + ERR_CONTINUE(E->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E); int column = idx % col_count; for (int i = 0; i < frame->lines.size(); i++) { @@ -3759,24 +3946,15 @@ float RichTextLabel::get_percent_visible() const { return percent_visible; } -void RichTextLabel::set_effects(const Vector<Variant> &effects) { - custom_effects.clear(); - for (int i = 0; i < effects.size(); i++) { - Ref<RichTextEffect> effect = Ref<RichTextEffect>(effects[i]); - custom_effects.push_back(effect); - } - +void RichTextLabel::set_effects(Array p_effects) { + custom_effects = p_effects; if ((bbcode != "") && use_bbcode) { parse_bbcode(bbcode); } } -Vector<Variant> RichTextLabel::get_effects() { - Vector<Variant> r; - for (int i = 0; i < custom_effects.size(); i++) { - r.push_back(custom_effects[i]); - } - return r; +Array RichTextLabel::get_effects() { + return custom_effects; } void RichTextLabel::install_effect(const Variant effect) { @@ -3806,11 +3984,10 @@ void RichTextLabel::_validate_property(PropertyInfo &property) const { } void RichTextLabel::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &RichTextLabel::_gui_input); ClassDB::bind_method(D_METHOD("get_text"), &RichTextLabel::get_text); ClassDB::bind_method(D_METHOD("add_text", "text"), &RichTextLabel::add_text); ClassDB::bind_method(D_METHOD("set_text", "text"), &RichTextLabel::set_text); - ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(VALIGN_TOP)); + ClassDB::bind_method(D_METHOD("add_image", "image", "width", "height", "color", "inline_align"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0)), DEFVAL(INLINE_ALIGN_CENTER)); ClassDB::bind_method(D_METHOD("newline"), &RichTextLabel::add_newline); ClassDB::bind_method(D_METHOD("remove_line", "line"), &RichTextLabel::remove_line); ClassDB::bind_method(D_METHOD("push_font", "font"), &RichTextLabel::push_font); @@ -3830,7 +4007,7 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("push_meta", "data"), &RichTextLabel::push_meta); ClassDB::bind_method(D_METHOD("push_underline"), &RichTextLabel::push_underline); ClassDB::bind_method(D_METHOD("push_strikethrough"), &RichTextLabel::push_strikethrough); - ClassDB::bind_method(D_METHOD("push_table", "columns", "inline_align"), &RichTextLabel::push_table, DEFVAL(VALIGN_TOP)); + ClassDB::bind_method(D_METHOD("push_table", "columns", "inline_align"), &RichTextLabel::push_table, DEFVAL(INLINE_ALIGN_TOP)); ClassDB::bind_method(D_METHOD("push_dropcap", "string", "font", "size", "dropcap_margins", "color", "outline_size", "outline_color"), &RichTextLabel::push_dropcap, DEFVAL(Rect2()), DEFVAL(Color(1, 1, 1)), DEFVAL(0), DEFVAL(Color(0, 0, 0, 0))); ClassDB::bind_method(D_METHOD("set_table_column_expand", "column", "expand", "ratio"), &RichTextLabel::set_table_column_expand); ClassDB::bind_method(D_METHOD("set_cell_row_background_color", "odd_row_bg", "even_row_bg"), &RichTextLabel::set_cell_row_background_color); @@ -3838,6 +4015,8 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("set_cell_size_override", "min_size", "max_size"), &RichTextLabel::set_cell_size_override); ClassDB::bind_method(D_METHOD("set_cell_padding", "padding"), &RichTextLabel::set_cell_padding); ClassDB::bind_method(D_METHOD("push_cell"), &RichTextLabel::push_cell); + ClassDB::bind_method(D_METHOD("push_fgcolor", "fgcolor"), &RichTextLabel::push_fgcolor); + ClassDB::bind_method(D_METHOD("push_bgcolor", "bgcolor"), &RichTextLabel::push_bgcolor); ClassDB::bind_method(D_METHOD("pop"), &RichTextLabel::pop); ClassDB::bind_method(D_METHOD("clear"), &RichTextLabel::clear); @@ -3932,7 +4111,7 @@ void RichTextLabel::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selection_enabled"), "set_selection_enabled", "is_selection_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "override_selected_font_color"), "set_override_selected_font_color", "is_overriding_selected_font_color"); - ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "custom_effects", PROPERTY_HINT_ARRAY_TYPE, "RichTextEffect", (PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE)), "set_effects", "get_effects"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "custom_effects", PROPERTY_HINT_ARRAY_TYPE, vformat("%s/%s:%s", Variant::OBJECT, PROPERTY_HINT_RESOURCE_TYPE, "RichTextEffect"), (PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_SCRIPT_VARIABLE)), "set_effects", "get_effects"); ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); @@ -3976,6 +4155,8 @@ void RichTextLabel::_bind_methods() { BIND_ENUM_CONSTANT(ITEM_WAVE); BIND_ENUM_CONSTANT(ITEM_TORNADO); BIND_ENUM_CONSTANT(ITEM_RAINBOW); + BIND_ENUM_CONSTANT(ITEM_BGCOLOR); + BIND_ENUM_CONSTANT(ITEM_FGCOLOR); BIND_ENUM_CONSTANT(ITEM_META); BIND_ENUM_CONSTANT(ITEM_DROPCAP); BIND_ENUM_CONSTANT(ITEM_CUSTOMFX); @@ -4013,7 +4194,7 @@ void RichTextLabel::set_fixed_size_to_width(int p_width) { } Size2 RichTextLabel::get_minimum_size() const { - Ref<StyleBox> style = get_theme_stylebox("normal"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); Size2 size = style->get_minimum_size(); if (fixed_width != -1) { @@ -4028,14 +4209,74 @@ Size2 RichTextLabel::get_minimum_size() const { return size; } +void RichTextLabel::_draw_fbg_boxes(RID p_ci, RID p_rid, Vector2 line_off, Item *it_from, Item *it_to, int start, int end, int fbg_flag) { + Vector2i fbg_index = Vector2i(end, start); + Color last_color = Color(0, 0, 0, 0); + bool draw_box = false; + // Draw a box based on color tags associated with glyphs + for (int i = start; i < end; i++) { + Item *it = _get_item_at_pos(it_from, it_to, i); + Color color = Color(0, 0, 0, 0); + + if (fbg_flag == 0) { + color = _find_bgcolor(it); + } else { + color = _find_fgcolor(it); + } + + bool change_to_color = ((color.a > 0) && ((last_color.a - 0.0) < 0.01)); + bool change_from_color = (((color.a - 0.0) < 0.01) && (last_color.a > 0.0)); + bool change_color = (((color.a > 0) == (last_color.a > 0)) && (color != last_color)); + + if (change_to_color) { + fbg_index.x = MIN(i, fbg_index.x); + fbg_index.y = MAX(i, fbg_index.y); + } + + if (change_from_color || change_color) { + fbg_index.x = MIN(i, fbg_index.x); + fbg_index.y = MAX(i, fbg_index.y); + draw_box = true; + } + + if (draw_box) { + Vector<Vector2> sel = TS->shaped_text_get_selection(p_rid, fbg_index.x, fbg_index.y); + for (int j = 0; j < sel.size(); j++) { + Vector2 rect_off = line_off + Vector2(sel[j].x, -TS->shaped_text_get_ascent(p_rid)); + Vector2 rect_size = Vector2(sel[j].y - sel[j].x, TS->shaped_text_get_size(p_rid).y); + RenderingServer::get_singleton()->canvas_item_add_rect(p_ci, Rect2(rect_off, rect_size), last_color); + } + fbg_index = Vector2i(end, start); + draw_box = false; + } + + if (change_color) { + fbg_index.x = MIN(i, fbg_index.x); + fbg_index.y = MAX(i, fbg_index.y); + } + + last_color = color; + } + + if (last_color.a > 0) { + Vector<Vector2> sel = TS->shaped_text_get_selection(p_rid, fbg_index.x, end); + for (int i = 0; i < sel.size(); i++) { + Vector2 rect_off = line_off + Vector2(sel[i].x, -TS->shaped_text_get_ascent(p_rid)); + Vector2 rect_size = Vector2(sel[i].y - sel[i].x, TS->shaped_text_get_size(p_rid).y); + RenderingServer::get_singleton()->canvas_item_add_rect(p_ci, Rect2(rect_off, rect_size), last_color); + } + } +} + Ref<RichTextEffect> RichTextLabel::_get_custom_effect_by_code(String p_bbcode_identifier) { for (int i = 0; i < custom_effects.size(); i++) { - if (!custom_effects[i].is_valid()) { + Ref<RichTextEffect> effect = custom_effects[i]; + if (!effect.is_valid()) { continue; } - if (custom_effects[i]->get_bbcode() == p_bbcode_identifier) { - return custom_effects[i]; + if (effect->get_bbcode() == p_bbcode_identifier) { + return effect; } } @@ -4112,7 +4353,7 @@ RichTextLabel::RichTextLabel() { current_frame = main; vscroll = memnew(VScrollBar); - add_child(vscroll); + add_child(vscroll, false, INTERNAL_MODE_FRONT); vscroll->set_drag_node(String("..")); vscroll->set_step(1); vscroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index e3e457d1f2..f25a8bf193 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -75,6 +75,8 @@ public: ITEM_WAVE, ITEM_TORNADO, ITEM_RAINBOW, + ITEM_BGCOLOR, + ITEM_FGCOLOR, ITEM_META, ITEM_DROPCAP, ITEM_CUSTOMFX @@ -100,7 +102,7 @@ private: int char_offset = 0; int char_count = 0; - Line() { text_buf.instance(); } + Line() { text_buf.instantiate(); } }; struct Item { @@ -159,7 +161,7 @@ private: struct ItemImage : public Item { Ref<Texture2D> image; - VAlign inline_align = VALIGN_TOP; + InlineAlign inline_align = INLINE_ALIGN_CENTER; Size2 size; Color color; ItemImage() { type = ITEM_IMAGE; } @@ -246,7 +248,7 @@ private: int total_width = 0; int total_height = 0; - VAlign inline_align = VALIGN_TOP; + InlineAlign inline_align = INLINE_ALIGN_TOP; ItemTable() { type = ITEM_TABLE; } }; @@ -258,7 +260,7 @@ private: }; struct ItemFX : public Item { - float elapsed_time = 0.f; + double elapsed_time = 0.f; }; struct ItemShake : public ItemFX { @@ -307,13 +309,23 @@ private: ItemRainbow() { type = ITEM_RAINBOW; } }; + struct ItemBGColor : public Item { + Color color; + ItemBGColor() { type = ITEM_BGCOLOR; } + }; + + struct ItemFGColor : public Item { + Color color; + ItemFGColor() { type = ITEM_FGCOLOR; } + }; + struct ItemCustomFX : public ItemFX { Ref<CharFXTransform> char_fx_transform; Ref<RichTextEffect> custom_effect; ItemCustomFX() { type = ITEM_CUSTOMFX; - char_fx_transform.instance(); + char_fx_transform.instantiate(); } virtual ~ItemCustomFX() { @@ -351,7 +363,7 @@ private: ItemMeta *meta_hovering = nullptr; Variant current_meta; - Vector<Ref<RichTextEffect>> custom_effects; + Array custom_effects; void _invalidate_current_line(ItemFrame *p_frame); void _validate_line_caches(ItemFrame *p_frame); @@ -392,7 +404,8 @@ private: void _find_click(ItemFrame *p_frame, const Point2i &p_click, ItemFrame **r_click_frame = nullptr, int *r_click_line = nullptr, Item **r_click_item = nullptr, int *r_click_char = nullptr, bool *r_outside = nullptr); String _get_line_text(ItemFrame *p_frame, int p_line, Selection p_sel) const; - bool _search_line(ItemFrame *p_frame, int p_line, const String &p_string, Item *p_from, Item *p_to); + bool _search_line(ItemFrame *p_frame, int p_line, const String &p_string, int p_char_idx, bool p_reverse_search); + bool _search_table(ItemTable *p_table, List<Item *>::Element *p_from, const String &p_string, bool p_reverse_search); void _shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width, int *r_char_offset); void _resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width); @@ -421,14 +434,16 @@ private: bool _find_underline(Item *p_item); bool _find_strikethrough(Item *p_item); bool _find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item = nullptr); + Color _find_bgcolor(Item *p_item); + Color _find_fgcolor(Item *p_item); bool _find_layout_subitem(Item *from, Item *to); void _fetch_item_fx_stack(Item *p_item, Vector<ItemFX *> &r_stack); void _update_scroll(); - void _update_fx(ItemFrame *p_frame, float p_delta_time); + void _update_fx(ItemFrame *p_frame, double p_delta_time); void _scroll_changed(double); - void _gui_input(Ref<InputEvent> p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; Item *_get_next_item(Item *p_item, bool p_free = false) const; Item *_get_prev_item(Item *p_item, bool p_free = false) const; @@ -436,6 +451,8 @@ private: Ref<RichTextEffect> _get_custom_effect_by_code(String p_bbcode_identifier); virtual Dictionary parse_expressions_for_values(Vector<String> p_expressions); + void _draw_fbg_boxes(RID p_ci, RID p_rid, Vector2 line_off, Item *it_from, Item *it_to, int start, int end, int fbg_flag); + bool use_bbcode = false; String bbcode; @@ -446,7 +463,7 @@ private: public: String get_text(); void add_text(const String &p_text); - void add_image(const Ref<Texture2D> &p_image, const int p_width = 0, const int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), VAlign p_align = VALIGN_TOP); + void add_image(const Ref<Texture2D> &p_image, const int p_width = 0, const int p_height = 0, const Color &p_color = Color(1.0, 1.0, 1.0), InlineAlign p_align = INLINE_ALIGN_CENTER); void add_newline(); bool remove_line(const int p_line); void push_dropcap(const String &p_string, const Ref<Font> &p_font, int p_size, const Rect2 &p_dropcap_margins = Rect2(), const Color &p_color = Color(1, 1, 1), int p_ol_size = 0, const Color &p_ol_color = Color(0, 0, 0, 0)); @@ -467,12 +484,14 @@ public: void push_indent(int p_level); void push_list(int p_level, ListType p_list, bool p_capitalize); void push_meta(const Variant &p_meta); - void push_table(int p_columns, VAlign p_align = VALIGN_TOP); + void push_table(int p_columns, InlineAlign p_align = INLINE_ALIGN_TOP); void push_fade(int p_start_index, int p_length); void push_shake(int p_strength, float p_rate); void push_wave(float p_frequency, float p_amplitude); void push_tornado(float p_frequency, float p_radius); void push_rainbow(float p_saturation, float p_value, float p_frequency); + void push_bgcolor(const Color &p_color); + void push_fgcolor(const Color &p_color); void push_customfx(Ref<RichTextEffect> p_custom_effect, Dictionary p_environment); void set_table_column_expand(int p_column, bool p_expand, int p_ratio = 1); void set_cell_row_background_color(const Color &p_odd_row_bg, const Color &p_even_row_bg); @@ -558,8 +577,8 @@ public: void set_percent_visible(float p_percent); float get_percent_visible() const; - void set_effects(const Vector<Variant> &effects); - Vector<Variant> get_effects(); + void set_effects(Array p_effects); + Array get_effects(); void install_effect(const Variant effect); diff --git a/scene/gui/scroll_bar.cpp b/scene/gui/scroll_bar.cpp index 62276e3af0..4edf373fbf 100644 --- a/scene/gui/scroll_bar.cpp +++ b/scene/gui/scroll_bar.cpp @@ -41,12 +41,12 @@ void ScrollBar::set_can_focus_by_default(bool p_can_focus) { focus_by_default = p_can_focus; } -void ScrollBar::_gui_input(Ref<InputEvent> p_event) { +void ScrollBar::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventMouseMotion> m = p_event; if (!m.is_valid() || drag.active) { - emit_signal("scrolling"); + emit_signal(SNAME("scrolling")); } Ref<InputEventMouseButton> b = p_event; @@ -70,8 +70,8 @@ void ScrollBar::_gui_input(Ref<InputEvent> p_event) { if (b->is_pressed()) { double ofs = orientation == VERTICAL ? b->get_position().y : b->get_position().x; - Ref<Texture2D> decr = get_theme_icon("decrement"); - Ref<Texture2D> incr = get_theme_icon("increment"); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); double decr_size = orientation == VERTICAL ? decr->get_height() : decr->get_width(); double incr_size = orientation == VERTICAL ? incr->get_height() : incr->get_width(); @@ -80,12 +80,16 @@ void ScrollBar::_gui_input(Ref<InputEvent> p_event) { double total = orientation == VERTICAL ? get_size().height : get_size().width; if (ofs < decr_size) { + decr_active = true; set_value(get_value() - (custom_step >= 0 ? custom_step : get_step())); + update(); return; } if (ofs > total - incr_size) { + incr_active = true; set_value(get_value() + (custom_step >= 0 ? custom_step : get_step())); + update(); return; } @@ -130,6 +134,8 @@ void ScrollBar::_gui_input(Ref<InputEvent> p_event) { } } else { + incr_active = false; + decr_active = false; drag.active = false; update(); } @@ -140,7 +146,7 @@ void ScrollBar::_gui_input(Ref<InputEvent> p_event) { if (drag.active) { double ofs = orientation == VERTICAL ? m->get_position().y : m->get_position().x; - Ref<Texture2D> decr = get_theme_icon("decrement"); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); double decr_size = orientation == VERTICAL ? decr->get_height() : decr->get_width(); ofs -= decr_size; @@ -150,8 +156,8 @@ void ScrollBar::_gui_input(Ref<InputEvent> p_event) { set_as_ratio(drag.value_at_click + diff); } else { double ofs = orientation == VERTICAL ? m->get_position().y : m->get_position().x; - Ref<Texture2D> decr = get_theme_icon("decrement"); - Ref<Texture2D> incr = get_theme_icon("increment"); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); double decr_size = orientation == VERTICAL ? decr->get_height() : decr->get_width(); double incr_size = orientation == VERTICAL ? incr->get_height() : incr->get_width(); @@ -215,17 +221,33 @@ void ScrollBar::_notification(int p_what) { if (p_what == NOTIFICATION_DRAW) { RID ci = get_canvas_item(); - Ref<Texture2D> decr = highlight == HIGHLIGHT_DECR ? get_theme_icon("decrement_highlight") : get_theme_icon("decrement"); - Ref<Texture2D> incr = highlight == HIGHLIGHT_INCR ? get_theme_icon("increment_highlight") : get_theme_icon("increment"); - Ref<StyleBox> bg = has_focus() ? get_theme_stylebox("scroll_focus") : get_theme_stylebox("scroll"); + Ref<Texture2D> decr, incr; + + if (decr_active) { + decr = get_theme_icon(SNAME("decrement_pressed")); + } else if (highlight == HIGHLIGHT_DECR) { + decr = get_theme_icon(SNAME("decrement_highlight")); + } else { + decr = get_theme_icon(SNAME("decrement")); + } + + if (incr_active) { + incr = get_theme_icon(SNAME("increment_pressed")); + } else if (highlight == HIGHLIGHT_INCR) { + incr = get_theme_icon(SNAME("increment_highlight")); + } else { + incr = get_theme_icon(SNAME("increment")); + } + + Ref<StyleBox> bg = has_focus() ? get_theme_stylebox(SNAME("scroll_focus")) : get_theme_stylebox(SNAME("scroll")); Ref<StyleBox> grabber; if (drag.active) { - grabber = get_theme_stylebox("grabber_pressed"); + grabber = get_theme_stylebox(SNAME("grabber_pressed")); } else if (highlight == HIGHLIGHT_RANGE) { - grabber = get_theme_stylebox("grabber_highlight"); + grabber = get_theme_stylebox(SNAME("grabber_highlight")); } else { - grabber = get_theme_stylebox("grabber"); + grabber = get_theme_stylebox(SNAME("grabber")); } Point2 ofs; @@ -389,7 +411,7 @@ void ScrollBar::_notification(int p_what) { } double ScrollBar::get_grabber_min_size() const { - Ref<StyleBox> grabber = get_theme_stylebox("grabber"); + Ref<StyleBox> grabber = get_theme_stylebox(SNAME("grabber")); Size2 gminsize = grabber->get_minimum_size() + grabber->get_center_size(); return (orientation == VERTICAL) ? gminsize.height : gminsize.width; } @@ -415,17 +437,17 @@ double ScrollBar::get_area_size() const { switch (orientation) { case VERTICAL: { double area = get_size().height; - area -= get_theme_stylebox("scroll")->get_minimum_size().height; - area -= get_theme_icon("increment")->get_height(); - area -= get_theme_icon("decrement")->get_height(); + area -= get_theme_stylebox(SNAME("scroll"))->get_minimum_size().height; + area -= get_theme_icon(SNAME("increment"))->get_height(); + area -= get_theme_icon(SNAME("decrement"))->get_height(); area -= get_grabber_min_size(); return area; } break; case HORIZONTAL: { double area = get_size().width; - area -= get_theme_stylebox("scroll")->get_minimum_size().width; - area -= get_theme_icon("increment")->get_width(); - area -= get_theme_icon("decrement")->get_width(); + area -= get_theme_stylebox(SNAME("scroll"))->get_minimum_size().width; + area -= get_theme_icon(SNAME("increment"))->get_width(); + area -= get_theme_icon(SNAME("decrement"))->get_width(); area -= get_grabber_min_size(); return area; } break; @@ -439,13 +461,13 @@ double ScrollBar::get_area_offset() const { double ofs = 0.0; if (orientation == VERTICAL) { - ofs += get_theme_stylebox("hscroll")->get_margin(SIDE_TOP); - ofs += get_theme_icon("decrement")->get_height(); + ofs += get_theme_stylebox(SNAME("hscroll"))->get_margin(SIDE_TOP); + ofs += get_theme_icon(SNAME("decrement"))->get_height(); } if (orientation == HORIZONTAL) { - ofs += get_theme_stylebox("hscroll")->get_margin(SIDE_LEFT); - ofs += get_theme_icon("decrement")->get_width(); + ofs += get_theme_stylebox(SNAME("hscroll"))->get_margin(SIDE_LEFT); + ofs += get_theme_icon(SNAME("decrement"))->get_width(); } return ofs; @@ -456,9 +478,9 @@ double ScrollBar::get_grabber_offset() const { } Size2 ScrollBar::get_minimum_size() const { - Ref<Texture2D> incr = get_theme_icon("increment"); - Ref<Texture2D> decr = get_theme_icon("decrement"); - Ref<StyleBox> bg = get_theme_stylebox("scroll"); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); + Ref<StyleBox> bg = get_theme_stylebox(SNAME("scroll")); Size2 minsize; if (orientation == VERTICAL) { @@ -597,7 +619,6 @@ bool ScrollBar::is_smooth_scroll_enabled() const { } void ScrollBar::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &ScrollBar::_gui_input); ClassDB::bind_method(D_METHOD("set_custom_step", "step"), &ScrollBar::set_custom_step); ClassDB::bind_method(D_METHOD("get_custom_step"), &ScrollBar::get_custom_step); diff --git a/scene/gui/scroll_bar.h b/scene/gui/scroll_bar.h index 24b3b33e82..574d17ee20 100644 --- a/scene/gui/scroll_bar.h +++ b/scene/gui/scroll_bar.h @@ -51,6 +51,9 @@ class ScrollBar : public Range { HighlightStatus highlight = HIGHLIGHT_NONE; + bool incr_active = false; + bool decr_active = false; + struct Drag { bool active = false; float pos_at_click = 0.0; @@ -86,7 +89,7 @@ class ScrollBar : public Range { void _drag_node_exit(); void _drag_node_input(const Ref<InputEvent> &p_input); - void _gui_input(Ref<InputEvent> p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; protected: void _notification(int p_what); diff --git a/scene/gui/scroll_container.cpp b/scene/gui/scroll_container.cpp index 46db4a3c2f..0c051f61e2 100644 --- a/scene/gui/scroll_container.cpp +++ b/scene/gui/scroll_container.cpp @@ -32,12 +32,8 @@ #include "core/os/os.h" #include "scene/main/window.h" -bool ScrollContainer::clips_input() const { - return true; -} - Size2 ScrollContainer::get_minimum_size() const { - Ref<StyleBox> sb = get_theme_stylebox("bg"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("bg")); Size2 min_size; for (int i = 0; i < get_child_count(); i++) { @@ -81,13 +77,13 @@ void ScrollContainer::_cancel_drag() { drag_from = Vector2(); if (beyond_deadzone) { - emit_signal("scroll_ended"); + emit_signal(SNAME("scroll_ended")); propagate_notification(NOTIFICATION_SCROLL_END); beyond_deadzone = false; } } -void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { +void ScrollContainer::gui_input(const Ref<InputEvent> &p_gui_input) { ERR_FAIL_COND(p_gui_input.is_null()); double prev_v_scroll = v_scroll->get_value(); @@ -177,7 +173,7 @@ void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { if (beyond_deadzone || (scroll_h && Math::abs(drag_accum.x) > deadzone) || (scroll_v && Math::abs(drag_accum.y) > deadzone)) { if (!beyond_deadzone) { propagate_notification(NOTIFICATION_SCROLL_BEGIN); - emit_signal("scroll_started"); + emit_signal(SNAME("scroll_started")); beyond_deadzone = true; // resetting drag_accum here ensures smooth scrolling after reaching deadzone @@ -232,38 +228,28 @@ void ScrollContainer::_update_scrollbar_position() { v_scroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); v_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); - h_scroll->raise(); - v_scroll->raise(); - _updating_scrollbars = false; } -void ScrollContainer::_ensure_focused_visible(Control *p_control) { - if (!follow_focus) { - return; +void ScrollContainer::_gui_focus_changed(Control *p_control) { + if (follow_focus && is_ancestor_of(p_control)) { + ensure_control_visible(p_control); } +} - if (is_a_parent_of(p_control)) { - Rect2 global_rect = get_global_rect(); - Rect2 other_rect = p_control->get_global_rect(); - float right_margin = 0.0; - if (v_scroll->is_visible()) { - right_margin += v_scroll->get_size().x; - } - float bottom_margin = 0.0; - if (h_scroll->is_visible()) { - bottom_margin += h_scroll->get_size().y; - } +void ScrollContainer::ensure_control_visible(Control *p_control) { + ERR_FAIL_COND_MSG(!is_ancestor_of(p_control), "Must be an ancestor of the control."); - float diff = MAX(MIN(other_rect.position.y, global_rect.position.y), other_rect.position.y + other_rect.size.y - global_rect.size.y + bottom_margin); - set_v_scroll(get_v_scroll() + (diff - global_rect.position.y)); - if (is_layout_rtl()) { - diff = MAX(MIN(other_rect.position.x, global_rect.position.x), other_rect.position.x + other_rect.size.x - global_rect.size.x); - } else { - diff = MAX(MIN(other_rect.position.x, global_rect.position.x), other_rect.position.x + other_rect.size.x - global_rect.size.x + right_margin); - } - set_h_scroll(get_h_scroll() + (diff - global_rect.position.x)); - } + Rect2 global_rect = get_global_rect(); + Rect2 other_rect = p_control->get_global_rect(); + float right_margin = v_scroll->is_visible() ? v_scroll->get_size().x : 0.0f; + float bottom_margin = h_scroll->is_visible() ? h_scroll->get_size().y : 0.0f; + + Vector2 diff = Vector2(MAX(MIN(other_rect.position.x, global_rect.position.x), other_rect.position.x + other_rect.size.x - global_rect.size.x + (!is_layout_rtl() ? right_margin : 0.0f)), + MAX(MIN(other_rect.position.y, global_rect.position.y), other_rect.position.y + other_rect.size.y - global_rect.size.y + bottom_margin)); + + set_h_scroll(get_h_scroll() + (diff.x - global_rect.position.x)); + set_v_scroll(get_v_scroll() + (diff.y - global_rect.position.y)); } void ScrollContainer::_update_dimensions() { @@ -271,7 +257,7 @@ void ScrollContainer::_update_dimensions() { Size2 size = get_size(); Point2 ofs; - Ref<StyleBox> sb = get_theme_stylebox("bg"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("bg")); size -= sb->get_minimum_size(); ofs += sb->get_offset(); bool rtl = is_layout_rtl(); @@ -330,11 +316,11 @@ void ScrollContainer::_update_dimensions() { void ScrollContainer::_notification(int p_what) { if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED || p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { _updating_scrollbars = true; - call_deferred("_update_scrollbar_position"); + call_deferred(SNAME("_update_scrollbar_position")); }; if (p_what == NOTIFICATION_READY) { - get_viewport()->connect("gui_focus_changed", callable_mp(this, &ScrollContainer::_ensure_focused_visible)); + get_viewport()->connect("gui_focus_changed", callable_mp(this, &ScrollContainer::_gui_focus_changed)); _update_dimensions(); } @@ -343,7 +329,7 @@ void ScrollContainer::_notification(int p_what) { }; if (p_what == NOTIFICATION_DRAW) { - Ref<StyleBox> sb = get_theme_stylebox("bg"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("bg")); draw_style_box(sb, Rect2(Vector2(), get_size())); update_scrollbars(); @@ -420,7 +406,7 @@ void ScrollContainer::_notification(int p_what) { void ScrollContainer::update_scrollbars() { Size2 size = get_size(); - Ref<StyleBox> sb = get_theme_stylebox("bg"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("bg")); size -= sb->get_minimum_size(); Size2 hmin; @@ -579,7 +565,6 @@ VScrollBar *ScrollContainer::get_v_scrollbar() { } void ScrollContainer::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &ScrollContainer::_gui_input); ClassDB::bind_method(D_METHOD("_update_scrollbar_position"), &ScrollContainer::_update_scrollbar_position); ClassDB::bind_method(D_METHOD("set_h_scroll", "value"), &ScrollContainer::set_h_scroll); @@ -608,6 +593,7 @@ void ScrollContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_h_scrollbar"), &ScrollContainer::get_h_scrollbar); ClassDB::bind_method(D_METHOD("get_v_scrollbar"), &ScrollContainer::get_v_scrollbar); + ClassDB::bind_method(D_METHOD("ensure_control_visible", "control"), &ScrollContainer::ensure_control_visible); ADD_SIGNAL(MethodInfo("scroll_started")); ADD_SIGNAL(MethodInfo("scroll_ended")); @@ -629,12 +615,12 @@ void ScrollContainer::_bind_methods() { ScrollContainer::ScrollContainer() { h_scroll = memnew(HScrollBar); h_scroll->set_name("_h_scroll"); - add_child(h_scroll); + add_child(h_scroll, false, INTERNAL_MODE_BACK); h_scroll->connect("value_changed", callable_mp(this, &ScrollContainer::_scroll_moved)); v_scroll = memnew(VScrollBar); v_scroll->set_name("_v_scroll"); - add_child(v_scroll); + add_child(v_scroll, false, INTERNAL_MODE_BACK); v_scroll->connect("value_changed", callable_mp(this, &ScrollContainer::_scroll_moved)); deadzone = GLOBAL_GET("gui/common/default_scroll_deadzone"); diff --git a/scene/gui/scroll_container.h b/scene/gui/scroll_container.h index f61df70b85..9f4ec558dc 100644 --- a/scene/gui/scroll_container.h +++ b/scene/gui/scroll_container.h @@ -68,7 +68,8 @@ class ScrollContainer : public Container { protected: Size2 get_minimum_size() const override; - void _gui_input(const Ref<InputEvent> &p_gui_input); + virtual void gui_input(const Ref<InputEvent> &p_gui_input) override; + void _gui_focus_changed(Control *p_control); void _update_dimensions(); void _notification(int p_what); @@ -77,7 +78,6 @@ protected: bool _updating_scrollbars = false; void _update_scrollbar_position(); - void _ensure_focused_visible(Control *p_node); public: void set_h_scroll(int p_pos); @@ -106,8 +106,7 @@ public: HScrollBar *get_h_scrollbar(); VScrollBar *get_v_scrollbar(); - - virtual bool clips_input() const override; + void ensure_control_visible(Control *p_control); TypedArray<String> get_configuration_warnings() const override; diff --git a/scene/gui/separator.cpp b/scene/gui/separator.cpp index 3cb8ccf135..1f3cb7aa24 100644 --- a/scene/gui/separator.cpp +++ b/scene/gui/separator.cpp @@ -33,9 +33,9 @@ Size2 Separator::get_minimum_size() const { Size2 ms(3, 3); if (orientation == VERTICAL) { - ms.x = get_theme_constant("separation"); + ms.x = get_theme_constant(SNAME("separation")); } else { // HORIZONTAL - ms.y = get_theme_constant("separation"); + ms.y = get_theme_constant(SNAME("separation")); } return ms; } @@ -44,7 +44,7 @@ void Separator::_notification(int p_what) { switch (p_what) { case NOTIFICATION_DRAW: { Size2i size = get_size(); - Ref<StyleBox> style = get_theme_stylebox("separator"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("separator")); Size2i ssize = style->get_minimum_size() + style->get_center_size(); if (orientation == VERTICAL) { diff --git a/scene/gui/shortcut.cpp b/scene/gui/shortcut.cpp deleted file mode 100644 index cbbcf9e069..0000000000 --- a/scene/gui/shortcut.cpp +++ /dev/null @@ -1,73 +0,0 @@ -/*************************************************************************/ -/* shortcut.cpp */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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 "shortcut.h" - -#include "core/os/keyboard.h" - -void Shortcut::set_shortcut(const Ref<InputEvent> &p_shortcut) { - shortcut = p_shortcut; - emit_changed(); -} - -Ref<InputEvent> Shortcut::get_shortcut() const { - return shortcut; -} - -bool Shortcut::is_shortcut(const Ref<InputEvent> &p_event) const { - return shortcut.is_valid() && shortcut->shortcut_match(p_event); -} - -String Shortcut::get_as_text() const { - if (shortcut.is_valid()) { - return shortcut->as_text(); - } else { - return "None"; - } -} - -bool Shortcut::is_valid() const { - return shortcut.is_valid(); -} - -void Shortcut::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_shortcut", "event"), &Shortcut::set_shortcut); - ClassDB::bind_method(D_METHOD("get_shortcut"), &Shortcut::get_shortcut); - - ClassDB::bind_method(D_METHOD("is_valid"), &Shortcut::is_valid); - - ClassDB::bind_method(D_METHOD("is_shortcut", "event"), &Shortcut::is_shortcut); - ClassDB::bind_method(D_METHOD("get_as_text"), &Shortcut::get_as_text); - - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shortcut", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"), "set_shortcut", "get_shortcut"); -} - -Shortcut::Shortcut() { -} diff --git a/scene/gui/shortcut.h b/scene/gui/shortcut.h deleted file mode 100644 index ea91f29b5d..0000000000 --- a/scene/gui/shortcut.h +++ /dev/null @@ -1,56 +0,0 @@ -/*************************************************************************/ -/* shortcut.h */ -/*************************************************************************/ -/* This file is part of: */ -/* GODOT ENGINE */ -/* https://godotengine.org */ -/*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ -/* */ -/* 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. */ -/*************************************************************************/ - -#ifndef SHORTCUT_H -#define SHORTCUT_H - -#include "core/input/input_event.h" -#include "core/io/resource.h" - -class Shortcut : public Resource { - GDCLASS(Shortcut, Resource); - - Ref<InputEvent> shortcut; - -protected: - static void _bind_methods(); - -public: - void set_shortcut(const Ref<InputEvent> &p_shortcut); - Ref<InputEvent> get_shortcut() const; - bool is_shortcut(const Ref<InputEvent> &p_event) const; - bool is_valid() const; - - String get_as_text() const; - - Shortcut(); -}; - -#endif // SHORTCUT_H diff --git a/scene/gui/slider.cpp b/scene/gui/slider.cpp index a407ef21cb..352f87954e 100644 --- a/scene/gui/slider.cpp +++ b/scene/gui/slider.cpp @@ -32,10 +32,10 @@ #include "core/os/keyboard.h" Size2 Slider::get_minimum_size() const { - Ref<StyleBox> style = get_theme_stylebox("slider"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("slider")); Size2i ss = style->get_minimum_size() + style->get_center_size(); - Ref<Texture2D> grabber = get_theme_icon("grabber"); + Ref<Texture2D> grabber = get_theme_icon(SNAME("grabber")); Size2i rs = grabber->get_size(); if (orientation == HORIZONTAL) { @@ -45,7 +45,7 @@ Size2 Slider::get_minimum_size() const { } } -void Slider::_gui_input(Ref<InputEvent> p_event) { +void Slider::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (!editable) { @@ -89,7 +89,7 @@ void Slider::_gui_input(Ref<InputEvent> p_event) { if (mm.is_valid()) { if (grab.active) { Size2i size = get_size(); - Ref<Texture2D> grabber = get_theme_icon("grabber"); + Ref<Texture2D> grabber = get_theme_icon(SNAME("grabber")); float motion = (orientation == VERTICAL ? mm->get_position().y : mm->get_position().x) - grab.pos; if (orientation == VERTICAL) { motion = -motion; @@ -161,18 +161,18 @@ void Slider::_notification(int p_what) { case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); Size2i size = get_size(); - Ref<StyleBox> style = get_theme_stylebox("slider"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("slider")); bool highlighted = mouse_inside || has_focus(); Ref<StyleBox> grabber_area = get_theme_stylebox(highlighted ? "grabber_area_highlight" : "grabber_area"); Ref<Texture2D> grabber = get_theme_icon(editable ? (highlighted ? "grabber_highlight" : "grabber") : "grabber_disabled"); - Ref<Texture2D> tick = get_theme_icon("tick"); + Ref<Texture2D> tick = get_theme_icon(SNAME("tick")); double ratio = Math::is_nan(get_as_ratio()) ? 0 : get_as_ratio(); if (orientation == VERTICAL) { int widget_width = style->get_minimum_size().width + style->get_center_size().width; float areasize = size.height - grabber->get_size().height; style->draw(ci, Rect2i(Point2i(size.width / 2 - widget_width / 2, 0), Size2i(widget_width, size.height))); - grabber_area->draw(ci, Rect2i(Point2i((size.width - widget_width) / 2, size.height - areasize * ratio - grabber->get_size().height / 2), Size2i(widget_width, areasize * ratio + grabber->get_size().width / 2))); + grabber_area->draw(ci, Rect2i(Point2i((size.width - widget_width) / 2, size.height - areasize * ratio - grabber->get_size().height / 2), Size2i(widget_width, areasize * ratio + grabber->get_size().height / 2))); if (ticks > 1) { int grabber_offset = (grabber->get_size().height / 2 - tick->get_height() / 2); @@ -253,7 +253,6 @@ bool Slider::is_scrollable() const { } void Slider::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &Slider::_gui_input); ClassDB::bind_method(D_METHOD("set_ticks", "count"), &Slider::set_ticks); ClassDB::bind_method(D_METHOD("get_ticks"), &Slider::get_ticks); diff --git a/scene/gui/slider.h b/scene/gui/slider.h index 65a4036cd1..46fa08bbf0 100644 --- a/scene/gui/slider.h +++ b/scene/gui/slider.h @@ -50,7 +50,7 @@ class Slider : public Range { bool scrollable = true; protected: - void _gui_input(Ref<InputEvent> p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; void _notification(int p_what); static void _bind_methods(); bool ticks_on_borders = false; diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp index 9dc2afdb2d..1074d0d8a0 100644 --- a/scene/gui/spin_box.cpp +++ b/scene/gui/spin_box.cpp @@ -50,9 +50,9 @@ void SpinBox::_value_changed(double) { line_edit->set_text(value); } -void SpinBox::_text_entered(const String &p_string) { +void SpinBox::_text_submitted(const String &p_string) { Ref<Expression> expr; - expr.instance(); + expr.instantiate(); String num = TS->parse_number(p_string); // Ignore the prefix and suffix in the expression @@ -99,7 +99,7 @@ void SpinBox::_release_mouse() { } } -void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { +void SpinBox::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (!is_editable()) { @@ -140,6 +140,8 @@ void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { accept_event(); } } break; + default: + break; } } @@ -168,11 +170,11 @@ void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { void SpinBox::_line_edit_focus_exit() { // discontinue because the focus_exit was caused by right-click context menu - if (line_edit->get_menu()->is_visible()) { + if (line_edit->is_menu_visible()) { return; } - _text_entered(line_edit->get_text()); + _text_submitted(line_edit->get_text()); } inline void SpinBox::_adjust_width_for_icon(const Ref<Texture2D> &icon) { @@ -186,7 +188,7 @@ inline void SpinBox::_adjust_width_for_icon(const Ref<Texture2D> &icon) { void SpinBox::_notification(int p_what) { if (p_what == NOTIFICATION_DRAW) { - Ref<Texture2D> updown = get_theme_icon("updown"); + Ref<Texture2D> updown = get_theme_icon(SNAME("updown")); _adjust_width_for_icon(updown); @@ -202,15 +204,15 @@ void SpinBox::_notification(int p_what) { } else if (p_what == NOTIFICATION_FOCUS_EXIT) { //_value_changed(0); } else if (p_what == NOTIFICATION_ENTER_TREE) { - _adjust_width_for_icon(get_theme_icon("updown")); + _adjust_width_for_icon(get_theme_icon(SNAME("updown"))); _value_changed(0); } else if (p_what == NOTIFICATION_EXIT_TREE) { _release_mouse(); } else if (p_what == NOTIFICATION_TRANSLATION_CHANGED) { _value_changed(0); } else if (p_what == NOTIFICATION_THEME_CHANGED) { - call_deferred("minimum_size_changed"); - get_line_edit()->call_deferred("minimum_size_changed"); + call_deferred(SNAME("minimum_size_changed")); + get_line_edit()->call_deferred(SNAME("minimum_size_changed")); } else if (p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { update(); } @@ -251,12 +253,12 @@ bool SpinBox::is_editable() const { } void SpinBox::apply() { - _text_entered(line_edit->get_text()); + _text_submitted(line_edit->get_text()); } void SpinBox::_bind_methods() { //ClassDB::bind_method(D_METHOD("_value_changed"),&SpinBox::_value_changed); - ClassDB::bind_method(D_METHOD("_gui_input"), &SpinBox::_gui_input); + ClassDB::bind_method(D_METHOD("set_align", "align"), &SpinBox::set_align); ClassDB::bind_method(D_METHOD("get_align"), &SpinBox::get_align); ClassDB::bind_method(D_METHOD("set_suffix", "suffix"), &SpinBox::set_suffix); @@ -276,18 +278,18 @@ void SpinBox::_bind_methods() { SpinBox::SpinBox() { line_edit = memnew(LineEdit); - add_child(line_edit); + add_child(line_edit, false, INTERNAL_MODE_FRONT); line_edit->set_anchors_and_offsets_preset(Control::PRESET_WIDE); line_edit->set_mouse_filter(MOUSE_FILTER_PASS); line_edit->set_align(LineEdit::ALIGN_LEFT); //connect("value_changed",this,"_value_changed"); - line_edit->connect("text_entered", callable_mp(this, &SpinBox::_text_entered), Vector<Variant>(), CONNECT_DEFERRED); + line_edit->connect("text_submitted", callable_mp(this, &SpinBox::_text_submitted), Vector<Variant>(), CONNECT_DEFERRED); line_edit->connect("focus_exited", callable_mp(this, &SpinBox::_line_edit_focus_exit), Vector<Variant>(), CONNECT_DEFERRED); line_edit->connect("gui_input", callable_mp(this, &SpinBox::_line_edit_input)); range_click_timer = memnew(Timer); range_click_timer->connect("timeout", callable_mp(this, &SpinBox::_range_click_timeout)); - add_child(range_click_timer); + add_child(range_click_timer, false, INTERNAL_MODE_FRONT); } diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h index e116adb64c..9ec3885f1f 100644 --- a/scene/gui/spin_box.h +++ b/scene/gui/spin_box.h @@ -45,7 +45,7 @@ class SpinBox : public Range { void _range_click_timeout(); void _release_mouse(); - void _text_entered(const String &p_string); + void _text_submitted(const String &p_string); virtual void _value_changed(double) override; String prefix; String suffix; @@ -65,7 +65,7 @@ class SpinBox : public Range { inline void _adjust_width_for_icon(const Ref<Texture2D> &icon); protected: - void _gui_input(const Ref<InputEvent> &p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; void _notification(int p_what); diff --git a/scene/gui/split_container.cpp b/scene/gui/split_container.cpp index df4cf9a740..4736a1ad37 100644 --- a/scene/gui/split_container.cpp +++ b/scene/gui/split_container.cpp @@ -38,7 +38,7 @@ Control *SplitContainer::_getch(int p_idx) const { for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); - if (!c || !c->is_visible_in_tree()) { + if (!c || !c->is_visible()) { continue; } if (c->is_set_as_top_level()) { @@ -76,8 +76,8 @@ void SplitContainer::_resort() { bool second_expanded = (vertical ? second->get_v_size_flags() : second->get_h_size_flags()) & SIZE_EXPAND; // Determine the separation between items - Ref<Texture2D> g = get_theme_icon("grabber"); - int sep = get_theme_constant("separation"); + Ref<Texture2D> g = get_theme_icon(SNAME("grabber")); + int sep = get_theme_constant(SNAME("separation")); sep = (dragger_visibility != DRAGGER_HIDDEN_COLLAPSED) ? MAX(sep, vertical ? g->get_height() : g->get_width()) : 0; // Compute the minimum size @@ -131,8 +131,8 @@ Size2 SplitContainer::get_minimum_size() const { /* Calculate MINIMUM SIZE */ Size2i minimum; - Ref<Texture2D> g = get_theme_icon("grabber"); - int sep = get_theme_constant("separation"); + Ref<Texture2D> g = get_theme_icon(SNAME("grabber")); + int sep = get_theme_constant(SNAME("separation")); sep = (dragger_visibility != DRAGGER_HIDDEN_COLLAPSED) ? MAX(sep, vertical ? g->get_height() : g->get_width()) : 0; for (int i = 0; i < 2; i++) { @@ -173,7 +173,7 @@ void SplitContainer::_notification(int p_what) { } break; case NOTIFICATION_MOUSE_EXIT: { mouse_inside = false; - if (get_theme_constant("autohide")) { + if (get_theme_constant(SNAME("autohide"))) { update(); } } break; @@ -182,7 +182,7 @@ void SplitContainer::_notification(int p_what) { return; } - if (collapsed || (!dragging && !mouse_inside && get_theme_constant("autohide"))) { + if (collapsed || (!dragging && !mouse_inside && get_theme_constant(SNAME("autohide")))) { return; } @@ -190,8 +190,8 @@ void SplitContainer::_notification(int p_what) { return; } - int sep = dragger_visibility != DRAGGER_HIDDEN_COLLAPSED ? get_theme_constant("separation") : 0; - Ref<Texture2D> tex = get_theme_icon("grabber"); + int sep = dragger_visibility != DRAGGER_HIDDEN_COLLAPSED ? get_theme_constant(SNAME("separation")) : 0; + Ref<Texture2D> tex = get_theme_icon(SNAME("grabber")); Size2 size = get_size(); if (vertical) { @@ -206,7 +206,7 @@ void SplitContainer::_notification(int p_what) { } } -void SplitContainer::_gui_input(const Ref<InputEvent> &p_event) { +void SplitContainer::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (collapsed || !_getch(0) || !_getch(1) || dragger_visibility != DRAGGER_VISIBLE) { @@ -218,7 +218,7 @@ void SplitContainer::_gui_input(const Ref<InputEvent> &p_event) { if (mb.is_valid()) { if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (mb->is_pressed()) { - int sep = get_theme_constant("separation"); + int sep = get_theme_constant(SNAME("separation")); if (vertical) { if (mb->get_position().y > middle_sep && mb->get_position().y < middle_sep + sep) { @@ -244,14 +244,14 @@ void SplitContainer::_gui_input(const Ref<InputEvent> &p_event) { if (mm.is_valid()) { bool mouse_inside_state = false; if (vertical) { - mouse_inside_state = mm->get_position().y > middle_sep && mm->get_position().y < middle_sep + get_theme_constant("separation"); + mouse_inside_state = mm->get_position().y > middle_sep && mm->get_position().y < middle_sep + get_theme_constant(SNAME("separation")); } else { - mouse_inside_state = mm->get_position().x > middle_sep && mm->get_position().x < middle_sep + get_theme_constant("separation"); + mouse_inside_state = mm->get_position().x > middle_sep && mm->get_position().x < middle_sep + get_theme_constant(SNAME("separation")); } if (mouse_inside != mouse_inside_state) { mouse_inside = mouse_inside_state; - if (get_theme_constant("autohide")) { + if (get_theme_constant(SNAME("autohide"))) { update(); } } @@ -267,7 +267,7 @@ void SplitContainer::_gui_input(const Ref<InputEvent> &p_event) { } should_clamp_split_offset = true; queue_sort(); - emit_signal("dragged", get_split_offset()); + emit_signal(SNAME("dragged"), get_split_offset()); } } @@ -277,7 +277,7 @@ Control::CursorShape SplitContainer::get_cursor_shape(const Point2 &p_pos) const } if (!collapsed && _getch(0) && _getch(1) && dragger_visibility == DRAGGER_VISIBLE) { - int sep = get_theme_constant("separation"); + int sep = get_theme_constant(SNAME("separation")); if (vertical) { if (p_pos.y > middle_sep && p_pos.y < middle_sep + sep) { @@ -337,8 +337,6 @@ bool SplitContainer::is_collapsed() const { } void SplitContainer::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &SplitContainer::_gui_input); - ClassDB::bind_method(D_METHOD("set_split_offset", "offset"), &SplitContainer::set_split_offset); ClassDB::bind_method(D_METHOD("get_split_offset"), &SplitContainer::get_split_offset); ClassDB::bind_method(D_METHOD("clamp_split_offset"), &SplitContainer::clamp_split_offset); diff --git a/scene/gui/split_container.h b/scene/gui/split_container.h index 6cb94d6ecf..47fd30a122 100644 --- a/scene/gui/split_container.h +++ b/scene/gui/split_container.h @@ -60,7 +60,7 @@ private: void _resort(); protected: - void _gui_input(const Ref<InputEvent> &p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; void _notification(int p_what); static void _bind_methods(); diff --git a/scene/gui/subviewport_container.cpp b/scene/gui/subviewport_container.cpp index bfc7e29f9c..53ea32e1b7 100644 --- a/scene/gui/subviewport_container.cpp +++ b/scene/gui/subviewport_container.cpp @@ -139,7 +139,7 @@ void SubViewportContainer::_notification(int p_what) { } } -void SubViewportContainer::_input(const Ref<InputEvent> &p_event) { +void SubViewportContainer::input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (Engine::get_singleton()->is_editor_hint()) { @@ -162,11 +162,11 @@ void SubViewportContainer::_input(const Ref<InputEvent> &p_event) { continue; } - c->input(ev); + c->push_input(ev); } } -void SubViewportContainer::_unhandled_input(const Ref<InputEvent> &p_event) { +void SubViewportContainer::unhandled_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); if (Engine::get_singleton()->is_editor_hint()) { @@ -189,13 +189,11 @@ void SubViewportContainer::_unhandled_input(const Ref<InputEvent> &p_event) { continue; } - c->unhandled_input(ev); + c->push_unhandled_input(ev); } } void SubViewportContainer::_bind_methods() { - ClassDB::bind_method(D_METHOD("_unhandled_input", "event"), &SubViewportContainer::_unhandled_input); - ClassDB::bind_method(D_METHOD("_input", "event"), &SubViewportContainer::_input); ClassDB::bind_method(D_METHOD("set_stretch", "enable"), &SubViewportContainer::set_stretch); ClassDB::bind_method(D_METHOD("is_stretch_enabled"), &SubViewportContainer::is_stretch_enabled); diff --git a/scene/gui/subviewport_container.h b/scene/gui/subviewport_container.h index 77cf4c16b3..7853f1590e 100644 --- a/scene/gui/subviewport_container.h +++ b/scene/gui/subviewport_container.h @@ -47,8 +47,8 @@ public: void set_stretch(bool p_enable); bool is_stretch_enabled() const; - void _input(const Ref<InputEvent> &p_event); - void _unhandled_input(const Ref<InputEvent> &p_event); + virtual void input(const Ref<InputEvent> &p_event) override; + virtual void unhandled_input(const Ref<InputEvent> &p_event) override; void set_stretch_shrink(int p_shrink); int get_stretch_shrink() const; diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index acf0641005..137ce7e96f 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -43,9 +43,9 @@ int TabContainer::_get_top_margin() const { } // Respect the minimum tab height. - Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); - Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); + Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); + Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); + Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); int tab_height = MAX(MAX(tab_unselected->get_minimum_size().height, tab_selected->get_minimum_size().height), tab_disabled->get_minimum_size().height); @@ -71,7 +71,7 @@ int TabContainer::_get_top_margin() const { return tab_height + content_height; } -void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { +void TabContainer::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventMouseButton> mb = p_event; @@ -88,11 +88,11 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { } // Handle menu button. - Ref<Texture2D> menu = get_theme_icon("menu"); + Ref<Texture2D> menu = get_theme_icon(SNAME("menu")); if (is_layout_rtl()) { if (popup && pos.x < menu->get_width()) { - emit_signal("pre_popup_pressed"); + emit_signal(SNAME("pre_popup_pressed")); Vector2 popup_pos = get_screen_position(); popup_pos.y += menu->get_height(); @@ -103,7 +103,7 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { } } else { if (popup && pos.x > size.width - menu->get_width()) { - emit_signal("pre_popup_pressed"); + emit_signal(SNAME("pre_popup_pressed")); Vector2 popup_pos = get_screen_position(); popup_pos.x += size.width - popup->get_size().width; @@ -129,8 +129,8 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { popup_ofs = menu->get_width(); } - Ref<Texture2D> increment = get_theme_icon("increment"); - Ref<Texture2D> decrement = get_theme_icon("decrement"); + Ref<Texture2D> increment = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decrement = get_theme_icon(SNAME("decrement")); if (is_layout_rtl()) { if (pos.x < popup_ofs + decrement->get_width()) { if (last_tab_cache < tabs.size() - 1) { @@ -203,7 +203,7 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { return; } - Ref<Texture2D> menu = get_theme_icon("menu"); + Ref<Texture2D> menu = get_theme_icon(SNAME("menu")); if (popup) { if (is_layout_rtl()) { if (pos.x <= menu->get_width()) { @@ -248,8 +248,8 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { popup_ofs = menu->get_width(); } - Ref<Texture2D> increment = get_theme_icon("increment"); - Ref<Texture2D> decrement = get_theme_icon("decrement"); + Ref<Texture2D> increment = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decrement = get_theme_icon(SNAME("decrement")); if (is_layout_rtl()) { if (pos.x <= popup_ofs + decrement->get_width()) { @@ -289,10 +289,10 @@ void TabContainer::_notification(int p_what) { switch (p_what) { case NOTIFICATION_RESIZED: { Vector<Control *> tabs = _get_tabs(); - int side_margin = get_theme_constant("side_margin"); - Ref<Texture2D> menu = get_theme_icon("menu"); - Ref<Texture2D> increment = get_theme_icon("increment"); - Ref<Texture2D> decrement = get_theme_icon("decrement"); + int side_margin = get_theme_constant(SNAME("side_margin")); + Ref<Texture2D> menu = get_theme_icon(SNAME("menu")); + Ref<Texture2D> increment = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decrement = get_theme_icon(SNAME("decrement")); int header_width = get_size().width - side_margin * 2; // Find the width of the header area. @@ -332,26 +332,26 @@ void TabContainer::_notification(int p_what) { bool rtl = is_layout_rtl(); // Draw only the tab area if the header is hidden. - Ref<StyleBox> panel = get_theme_stylebox("panel"); + Ref<StyleBox> panel = get_theme_stylebox(SNAME("panel")); if (!tabs_visible) { panel->draw(canvas, Rect2(0, 0, size.width, size.height)); return; } Vector<Control *> tabs = _get_tabs(); - Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); - Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<Texture2D> increment = get_theme_icon("increment"); - Ref<Texture2D> increment_hl = get_theme_icon("increment_highlight"); - Ref<Texture2D> decrement = get_theme_icon("decrement"); - Ref<Texture2D> decrement_hl = get_theme_icon("decrement_highlight"); - Ref<Texture2D> menu = get_theme_icon("menu"); - Ref<Texture2D> menu_hl = get_theme_icon("menu_highlight"); - Color font_selected_color = get_theme_color("font_selected_color"); - Color font_unselected_color = get_theme_color("font_unselected_color"); - Color font_disabled_color = get_theme_color("font_disabled_color"); - int side_margin = get_theme_constant("side_margin"); + Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); + Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); + Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); + Ref<Texture2D> increment = get_theme_icon(SNAME("increment")); + Ref<Texture2D> increment_hl = get_theme_icon(SNAME("increment_highlight")); + Ref<Texture2D> decrement = get_theme_icon(SNAME("decrement")); + Ref<Texture2D> decrement_hl = get_theme_icon(SNAME("decrement_highlight")); + Ref<Texture2D> menu = get_theme_icon(SNAME("menu")); + Ref<Texture2D> menu_hl = get_theme_icon(SNAME("menu_highlight")); + Color font_selected_color = get_theme_color(SNAME("font_selected_color")); + Color font_unselected_color = get_theme_color(SNAME("font_unselected_color")); + Color font_disabled_color = get_theme_color(SNAME("font_disabled_color")); + int side_margin = get_theme_constant(SNAME("side_margin")); // Find out start and width of the header area. int header_x = side_margin; @@ -434,14 +434,14 @@ void TabContainer::_notification(int p_what) { } int tab_width = tab_widths[i]; - if (get_tab_disabled(index)) { + if (index == current) { + x_current = x; + } else if (get_tab_disabled(index)) { if (rtl) { _draw_tab(tab_disabled, font_disabled_color, index, size.width - (tabs_ofs_cache + x) - tab_width); } else { _draw_tab(tab_disabled, font_disabled_color, index, tabs_ofs_cache + x); } - } else if (index == current) { - x_current = x; } else { if (rtl) { _draw_tab(tab_unselected, font_unselected_color, index, size.width - (tabs_ofs_cache + x) - tab_width); @@ -459,12 +459,13 @@ void TabContainer::_notification(int p_what) { panel->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height)); } - // Draw selected tab in front. only draw selected tab when it's in visible range. + // Draw selected tab in front. Only draw selected tab when it's in visible range. if (tabs.size() > 0 && current - first_tab_cache < tab_widths.size() && current >= first_tab_cache) { + Ref<StyleBox> current_style_box = get_tab_disabled(current) ? tab_disabled : tab_selected; if (rtl) { - _draw_tab(tab_selected, font_selected_color, current, size.width - (tabs_ofs_cache + x_current) - tab_widths[current]); + _draw_tab(current_style_box, font_selected_color, current, size.width - (tabs_ofs_cache + x_current) - tab_widths[current]); } else { - _draw_tab(tab_selected, font_selected_color, current, tabs_ofs_cache + x_current); + _draw_tab(current_style_box, font_selected_color, current, tabs_ofs_cache + x_current); } } @@ -529,18 +530,18 @@ void TabContainer::_notification(int p_what) { text_buf.write[i]->clear(); } _theme_changing = true; - call_deferred("_on_theme_changed"); // Wait until all changed theme. + call_deferred(SNAME("_on_theme_changed")); // Wait until all changed theme. } break; } } void TabContainer::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x) { - Vector<Control *> tabs = _get_tabs(); + Control *control = get_tab_control(p_index); RID canvas = get_canvas_item(); - Ref<Font> font = get_theme_font("font"); - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); - int icon_text_distance = get_theme_constant("icon_separation"); + Ref<Font> font = get_theme_font(SNAME("font")); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); + int icon_text_distance = get_theme_constant(SNAME("icon_separation")); int tab_width = _get_tab_width(p_index); int header_height = _get_top_margin(); @@ -549,8 +550,7 @@ void TabContainer::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, in p_tab_style->draw(canvas, tab_rect); // Draw the tab contents. - Control *control = Object::cast_to<Control>(tabs[p_index]); - String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(tr(control->get_name())); + String text = control->has_meta("_tab_name") ? String(atr(String(control->get_meta("_tab_name")))) : String(atr(control->get_name())); int x_content = tab_rect.position.x + p_tab_style->get_margin(SIDE_LEFT); int top_margin = p_tab_style->get_margin(SIDE_TOP); @@ -580,13 +580,14 @@ void TabContainer::_refresh_texts() { text_buf.clear(); Vector<Control *> tabs = _get_tabs(); bool rtl = is_layout_rtl(); - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); for (int i = 0; i < tabs.size(); i++) { Control *control = Object::cast_to<Control>(tabs[i]); - String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(tr(control->get_name())); + String text = control->has_meta("_tab_name") ? String(atr(String(control->get_meta("_tab_name")))) : String(atr(control->get_name())); + Ref<TextLine> name; - name.instance(); + name.instantiate(); name->set_direction(rtl ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); name->add_string(text, font, font_size, Dictionary(), TranslationServer::get_singleton()->get_tool_locale()); text_buf.push_back(name); @@ -609,7 +610,7 @@ void TabContainer::_on_theme_changed() { } void TabContainer::_repaint() { - Ref<StyleBox> sb = get_theme_stylebox("panel"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("panel")); Vector<Control *> tabs = _get_tabs(); for (int i = 0; i < tabs.size(); i++) { Control *c = tabs[i]; @@ -619,10 +620,10 @@ void TabContainer::_repaint() { if (tabs_visible) { c->set_offset(SIDE_TOP, _get_top_margin()); } - c->set_offset(Side(SIDE_TOP), c->get_offset(Side(SIDE_TOP)) + sb->get_margin(Side(SIDE_TOP))); - c->set_offset(Side(SIDE_LEFT), c->get_offset(Side(SIDE_LEFT)) + sb->get_margin(Side(SIDE_LEFT))); - c->set_offset(Side(SIDE_RIGHT), c->get_offset(Side(SIDE_RIGHT)) - sb->get_margin(Side(SIDE_RIGHT))); - c->set_offset(Side(SIDE_BOTTOM), c->get_offset(Side(SIDE_BOTTOM)) - sb->get_margin(Side(SIDE_BOTTOM))); + c->set_offset(SIDE_TOP, c->get_offset(SIDE_TOP) + sb->get_margin(SIDE_TOP)); + c->set_offset(SIDE_LEFT, c->get_offset(SIDE_LEFT) + sb->get_margin(SIDE_LEFT)); + c->set_offset(SIDE_RIGHT, c->get_offset(SIDE_RIGHT) - sb->get_margin(SIDE_RIGHT)); + c->set_offset(SIDE_BOTTOM, c->get_offset(SIDE_BOTTOM) - sb->get_margin(SIDE_BOTTOM)); } else { c->hide(); @@ -640,15 +641,15 @@ void TabContainer::_on_mouse_exited() { int TabContainer::_get_tab_width(int p_index) const { ERR_FAIL_INDEX_V(p_index, get_tab_count(), 0); - Control *control = Object::cast_to<Control>(_get_tabs()[p_index]); - if (!control || control->is_set_as_top_level() || get_tab_hidden(p_index)) { + Control *control = get_tab_control(p_index); + if (!control || get_tab_hidden(p_index)) { return 0; } // Get the width of the text displayed on the tab. - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); - String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(tr(control->get_name())); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); + String text = control->has_meta("_tab_name") ? String(atr(String(control->get_meta("_tab_name")))) : String(atr(control->get_name())); int width = font->get_string_size(text, font_size).width; // Add space for a tab icon. @@ -657,15 +658,15 @@ int TabContainer::_get_tab_width(int p_index) const { if (icon.is_valid()) { width += icon->get_width(); if (text != "") { - width += get_theme_constant("icon_separation"); + width += get_theme_constant(SNAME("icon_separation")); } } } // Respect a minimum size. - Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); - Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); + Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); + Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); + Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); if (get_tab_disabled(p_index)) { width += tab_disabled->get_minimum_size().width; } else if (p_index == current) { @@ -681,7 +682,7 @@ Vector<Control *> TabContainer::_get_tabs() const { Vector<Control *> controls; for (int i = 0; i < get_child_count(); i++) { Control *control = Object::cast_to<Control>(get_child(i)); - if (!control || control->is_top_level_control()) { + if (!control || control->is_set_as_top_level()) { continue; } @@ -699,10 +700,7 @@ void TabContainer::add_child_notify(Node *p_child) { Container::add_child_notify(p_child); Control *c = Object::cast_to<Control>(p_child); - if (!c) { - return; - } - if (c->is_set_as_top_level()) { + if (!c || c->is_set_as_top_level()) { return; } @@ -715,7 +713,7 @@ void TabContainer::add_child_notify(Node *p_child) { c->hide(); } else { c->show(); - //call_deferred("set_current_tab",0); + //call_deferred(SNAME("set_current_tab"),0); first = true; current = 0; previous = 0; @@ -724,22 +722,27 @@ void TabContainer::add_child_notify(Node *p_child) { if (tabs_visible) { c->set_offset(SIDE_TOP, _get_top_margin()); } - Ref<StyleBox> sb = get_theme_stylebox("panel"); - c->set_offset(Side(SIDE_TOP), c->get_offset(Side(SIDE_TOP)) + sb->get_margin(Side(SIDE_TOP))); - c->set_offset(Side(SIDE_LEFT), c->get_offset(Side(SIDE_LEFT)) + sb->get_margin(Side(SIDE_LEFT))); - c->set_offset(Side(SIDE_RIGHT), c->get_offset(Side(SIDE_RIGHT)) - sb->get_margin(Side(SIDE_RIGHT))); - c->set_offset(Side(SIDE_BOTTOM), c->get_offset(Side(SIDE_BOTTOM)) - sb->get_margin(Side(SIDE_BOTTOM))); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("panel")); + c->set_offset(SIDE_TOP, c->get_offset(SIDE_TOP) + sb->get_margin(SIDE_TOP)); + c->set_offset(SIDE_LEFT, c->get_offset(SIDE_LEFT) + sb->get_margin(SIDE_LEFT)); + c->set_offset(SIDE_RIGHT, c->get_offset(SIDE_RIGHT) - sb->get_margin(SIDE_RIGHT)); + c->set_offset(SIDE_BOTTOM, c->get_offset(SIDE_BOTTOM) - sb->get_margin(SIDE_BOTTOM)); update(); p_child->connect("renamed", callable_mp(this, &TabContainer::_child_renamed_callback)); if (first && is_inside_tree()) { - emit_signal("tab_changed", current); + emit_signal(SNAME("tab_changed"), current); } } void TabContainer::move_child_notify(Node *p_child) { Container::move_child_notify(p_child); - call_deferred("_update_current_tab"); - _refresh_texts(); + + Control *c = Object::cast_to<Control>(p_child); + if (!c || c->is_set_as_top_level()) { + return; + } + + _update_current_tab(); update(); } @@ -756,11 +759,11 @@ void TabContainer::set_current_tab(int p_current) { _repaint(); if (pending_previous == current) { - emit_signal("tab_selected", current); + emit_signal(SNAME("tab_selected"), current); } else { previous = pending_previous; - emit_signal("tab_selected", current); - emit_signal("tab_changed", current); + emit_signal(SNAME("tab_selected"), current); + emit_signal(SNAME("tab_changed"), current); } update(); @@ -784,22 +787,19 @@ Control *TabContainer::get_tab_control(int p_idx) const { } Control *TabContainer::get_current_tab_control() const { - Vector<Control *> tabs = _get_tabs(); - if (current >= 0 && current < tabs.size()) { - return tabs[current]; - } else { - return nullptr; - } + return get_tab_control(current); } void TabContainer::remove_child_notify(Node *p_child) { Container::remove_child_notify(p_child); - if (!Object::cast_to<Control>(p_child)) { + Control *c = Object::cast_to<Control>(p_child); + if (!c || c->is_set_as_top_level()) { return; } - call_deferred("_update_current_tab"); + // Defer the call because tab is not yet removed (remove_child_notify is called right before p_child is actually removed). + call_deferred(SNAME("_update_current_tab")); p_child->disconnect("renamed", callable_mp(this, &TabContainer::_child_renamed_callback)); @@ -807,10 +807,9 @@ void TabContainer::remove_child_notify(Node *p_child) { } void TabContainer::_update_current_tab() { - Vector<Control *> tabs = _get_tabs(); _refresh_texts(); - int tc = tabs.size(); + int tc = get_tab_count(); if (current >= tc) { current = tc - 1; } @@ -898,7 +897,7 @@ void TabContainer::drop_data(const Point2 &p_point, const Variant &p_data) { if (hover_now < 0) { hover_now = get_tab_count() - 1; } - move_child(get_tab_control(tab_from_id), hover_now); + move_child(get_tab_control(tab_from_id), get_tab_control(hover_now)->get_index()); set_current_tab(hover_now); } else if (get_tabs_rearrange_group() != -1) { // drag and drop between TabContainers @@ -907,13 +906,13 @@ void TabContainer::drop_data(const Point2 &p_point, const Variant &p_data) { if (from_tabc && from_tabc->get_tabs_rearrange_group() == get_tabs_rearrange_group()) { Control *moving_tabc = from_tabc->get_tab_control(tab_from_id); from_tabc->remove_child(moving_tabc); - add_child(moving_tabc); + add_child(moving_tabc, false, INTERNAL_MODE_FRONT); if (hover_now < 0) { hover_now = get_tab_count() - 1; } - move_child(moving_tabc, hover_now); + move_child(moving_tabc, get_tab_control(hover_now)->get_index()); set_current_tab(hover_now); - emit_signal("tab_changed", hover_now); + emit_signal(SNAME("tab_changed"), hover_now); } } } @@ -944,12 +943,12 @@ int TabContainer::get_tab_idx_at_point(const Point2 &p_point) const { Popup *popup = get_popup(); if (popup) { - Ref<Texture2D> menu = get_theme_icon("menu"); + Ref<Texture2D> menu = get_theme_icon(SNAME("menu")); button_ofs += menu->get_width(); } if (buttons_visible_cache) { - Ref<Texture2D> increment = get_theme_icon("increment"); - Ref<Texture2D> decrement = get_theme_icon("decrement"); + Ref<Texture2D> increment = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decrement = get_theme_icon(SNAME("decrement")); button_ofs += increment->get_width() + decrement->get_width(); } if (px > size.width - button_ofs) { @@ -1018,12 +1017,8 @@ bool TabContainer::is_all_tabs_in_front() const { return all_tabs_in_front; } -Control *TabContainer::_get_tab(int p_idx) const { - return get_tab_control(p_idx); -} - void TabContainer::set_tab_title(int p_tab, const String &p_title) { - Control *child = _get_tab(p_tab); + Control *child = get_tab_control(p_tab); ERR_FAIL_COND(!child); child->set_meta("_tab_name", p_title); _refresh_texts(); @@ -1031,7 +1026,7 @@ void TabContainer::set_tab_title(int p_tab, const String &p_title) { } String TabContainer::get_tab_title(int p_tab) const { - Control *child = _get_tab(p_tab); + Control *child = get_tab_control(p_tab); ERR_FAIL_COND_V(!child, ""); if (child->has_meta("_tab_name")) { return child->get_meta("_tab_name"); @@ -1041,14 +1036,14 @@ String TabContainer::get_tab_title(int p_tab) const { } void TabContainer::set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon) { - Control *child = _get_tab(p_tab); + Control *child = get_tab_control(p_tab); ERR_FAIL_COND(!child); child->set_meta("_tab_icon", p_icon); update(); } Ref<Texture2D> TabContainer::get_tab_icon(int p_tab) const { - Control *child = _get_tab(p_tab); + Control *child = get_tab_control(p_tab); ERR_FAIL_COND_V(!child, Ref<Texture2D>()); if (child->has_meta("_tab_icon")) { return child->get_meta("_tab_icon"); @@ -1058,14 +1053,14 @@ Ref<Texture2D> TabContainer::get_tab_icon(int p_tab) const { } void TabContainer::set_tab_disabled(int p_tab, bool p_disabled) { - Control *child = _get_tab(p_tab); + Control *child = get_tab_control(p_tab); ERR_FAIL_COND(!child); child->set_meta("_tab_disabled", p_disabled); update(); } bool TabContainer::get_tab_disabled(int p_tab) const { - Control *child = _get_tab(p_tab); + Control *child = get_tab_control(p_tab); ERR_FAIL_COND_V(!child, false); if (child->has_meta("_tab_disabled")) { return child->get_meta("_tab_disabled"); @@ -1075,7 +1070,7 @@ bool TabContainer::get_tab_disabled(int p_tab) const { } void TabContainer::set_tab_hidden(int p_tab, bool p_hidden) { - Control *child = _get_tab(p_tab); + Control *child = get_tab_control(p_tab); ERR_FAIL_COND(!child); child->set_meta("_tab_hidden", p_hidden); update(); @@ -1094,7 +1089,7 @@ void TabContainer::set_tab_hidden(int p_tab, bool p_hidden) { } bool TabContainer::get_tab_hidden(int p_tab) const { - Control *child = _get_tab(p_tab); + Control *child = get_tab_control(p_tab); ERR_FAIL_COND_V(!child, false); if (child->has_meta("_tab_hidden")) { return child->get_meta("_tab_hidden"); @@ -1136,17 +1131,17 @@ Size2 TabContainer::get_minimum_size() const { ms.y = MAX(ms.y, cms.y); } - Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); - Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<Font> font = get_theme_font("font"); + Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); + Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); + Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); + Ref<Font> font = get_theme_font(SNAME("font")); if (tabs_visible) { ms.y += MAX(MAX(tab_unselected->get_minimum_size().y, tab_selected->get_minimum_size().y), tab_disabled->get_minimum_size().y); ms.y += _get_top_margin(); } - Ref<StyleBox> sb = get_theme_stylebox("panel"); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("panel")); ms += sb->get_minimum_size(); return ms; @@ -1199,7 +1194,6 @@ bool TabContainer::get_use_hidden_tabs_for_min_size() const { } void TabContainer::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &TabContainer::_gui_input); ClassDB::bind_method(D_METHOD("get_tab_count"), &TabContainer::get_tab_count); ClassDB::bind_method(D_METHOD("set_current_tab", "tab_idx"), &TabContainer::set_current_tab); ClassDB::bind_method(D_METHOD("get_current_tab"), &TabContainer::get_current_tab); @@ -1218,6 +1212,7 @@ void TabContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &TabContainer::get_tab_icon); ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &TabContainer::set_tab_disabled); ClassDB::bind_method(D_METHOD("get_tab_disabled", "tab_idx"), &TabContainer::get_tab_disabled); + ClassDB::bind_method(D_METHOD("get_tab_idx_at_point", "point"), &TabContainer::get_tab_idx_at_point); ClassDB::bind_method(D_METHOD("set_popup", "popup"), &TabContainer::set_popup); ClassDB::bind_method(D_METHOD("get_popup"), &TabContainer::get_popup); ClassDB::bind_method(D_METHOD("set_drag_to_rearrange_enabled", "enabled"), &TabContainer::set_drag_to_rearrange_enabled); diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h index 4ed5255729..fe96df25e8 100644 --- a/scene/gui/tab_container.h +++ b/scene/gui/tab_container.h @@ -57,7 +57,6 @@ private: bool menu_hovered = false; int highlight_arrow = -1; TabAlign align = ALIGN_CENTER; - Control *_get_tab(int p_idx) const; int _get_top_margin() const; mutable ObjectID popup_obj_id; bool drag_to_rearrange_enabled = false; @@ -77,7 +76,7 @@ private: protected: void _child_renamed_callback(); - void _gui_input(const Ref<InputEvent> &p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; void _notification(int p_what); virtual void add_child_notify(Node *p_child) override; virtual void move_child_notify(Node *p_child) override; diff --git a/scene/gui/tabs.cpp b/scene/gui/tabs.cpp index 471b26be75..3ca2d1c1e9 100644 --- a/scene/gui/tabs.cpp +++ b/scene/gui/tabs.cpp @@ -38,9 +38,9 @@ #include "scene/gui/texture_rect.h" Size2 Tabs::get_minimum_size() const { - Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); - Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); + Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); + Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); + Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); int y_margin = MAX(MAX(tab_unselected->get_minimum_size().height, tab_selected->get_minimum_size().height), tab_disabled->get_minimum_size().height); @@ -51,7 +51,7 @@ Size2 Tabs::get_minimum_size() const { if (tex.is_valid()) { ms.height = MAX(ms.height, tex->get_size().height); if (tabs[i].text != "") { - ms.width += get_theme_constant("hseparation"); + ms.width += get_theme_constant(SNAME("hseparation")); } } @@ -69,15 +69,15 @@ Size2 Tabs::get_minimum_size() const { if (tabs[i].right_button.is_valid()) { Ref<Texture2D> rb = tabs[i].right_button; Size2 bms = rb->get_size(); - bms.width += get_theme_constant("hseparation"); + bms.width += get_theme_constant(SNAME("hseparation")); ms.width += bms.width; ms.height = MAX(bms.height + tab_unselected->get_minimum_size().height, ms.height); } if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current)) { - Ref<Texture2D> cb = get_theme_icon("close"); + Ref<Texture2D> cb = get_theme_icon(SNAME("close")); Size2 bms = cb->get_size(); - bms.width += get_theme_constant("hseparation"); + bms.width += get_theme_constant(SNAME("hseparation")); ms.width += bms.width; ms.height = MAX(bms.height + tab_unselected->get_minimum_size().height, ms.height); } @@ -90,7 +90,7 @@ Size2 Tabs::get_minimum_size() const { return ms; } -void Tabs::_gui_input(const Ref<InputEvent> &p_event) { +void Tabs::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventMouseMotion> mm = p_event; @@ -100,8 +100,8 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { highlight_arrow = -1; if (buttons_visible) { - Ref<Texture2D> incr = get_theme_icon("increment"); - Ref<Texture2D> decr = get_theme_icon("decrement"); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); if (is_layout_rtl()) { if (pos.x < decr->get_width()) { @@ -148,7 +148,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { if (rb_pressing && !mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (rb_hover != -1) { //pressed - emit_signal("right_button_pressed", rb_hover); + emit_signal(SNAME("right_button_pressed"), rb_hover); } rb_pressing = false; @@ -158,7 +158,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (cb_hover != -1) { //pressed - emit_signal("tab_closed", cb_hover); + emit_signal(SNAME("tab_closed"), cb_hover); } cb_pressing = false; @@ -170,8 +170,8 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { Point2 pos(mb->get_position().x, mb->get_position().y); if (buttons_visible) { - Ref<Texture2D> incr = get_theme_icon("increment"); - Ref<Texture2D> decr = get_theme_icon("decrement"); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); if (is_layout_rtl()) { if (pos.x < decr->get_width()) { @@ -229,17 +229,17 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { if (found != -1) { set_current_tab(found); - emit_signal("tab_clicked", found); + emit_signal(SNAME("tab_clicked"), found); } } } } void Tabs::_shape(int p_tab) { - Ref<Font> font = get_theme_font("font"); - int font_size = get_theme_font_size("font_size"); + Ref<Font> font = get_theme_font(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); - tabs.write[p_tab].xl_text = tr(tabs[p_tab].text); + tabs.write[p_tab].xl_text = atr(tabs[p_tab].text); tabs.write[p_tab].text_buf->clear(); if (tabs[p_tab].text_direction == Control::TEXT_DIRECTION_INHERITED) { tabs.write[p_tab].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); @@ -256,6 +256,7 @@ void Tabs::_notification(int p_what) { _update_cache(); update(); } break; + case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { for (int i = 0; i < tabs.size(); ++i) { _shape(i); @@ -273,15 +274,15 @@ void Tabs::_notification(int p_what) { _update_cache(); RID ci = get_canvas_item(); - Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); - Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Color font_selected_color = get_theme_color("font_selected_color"); - Color font_unselected_color = get_theme_color("font_unselected_color"); - Color font_disabled_color = get_theme_color("font_disabled_color"); - Ref<Texture2D> close = get_theme_icon("close"); - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); + Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); + Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); + Color font_selected_color = get_theme_color(SNAME("font_selected_color")); + Color font_unselected_color = get_theme_color(SNAME("font_unselected_color")); + Color font_disabled_color = get_theme_color(SNAME("font_disabled_color")); + Ref<Texture2D> close = get_theme_icon(SNAME("close")); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); Vector2 size = get_size(); bool rtl = is_layout_rtl(); @@ -305,10 +306,10 @@ void Tabs::_notification(int p_what) { w = 0; } - Ref<Texture2D> incr = get_theme_icon("increment"); - Ref<Texture2D> decr = get_theme_icon("decrement"); - Ref<Texture2D> incr_hl = get_theme_icon("increment_highlight"); - Ref<Texture2D> decr_hl = get_theme_icon("decrement_highlight"); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); + Ref<Texture2D> incr_hl = get_theme_icon(SNAME("increment_highlight")); + Ref<Texture2D> decr_hl = get_theme_icon(SNAME("decrement_highlight")); int limit = get_size().width; int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); @@ -362,7 +363,7 @@ void Tabs::_notification(int p_what) { icon->draw(ci, Point2i(w, sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - icon->get_height()) / 2)); } if (tabs[i].text != "") { - w += icon->get_width() + get_theme_constant("hseparation"); + w += icon->get_width() + get_theme_constant(SNAME("hseparation")); } } @@ -383,10 +384,10 @@ void Tabs::_notification(int p_what) { w += tabs[i].size_text; if (tabs[i].right_button.is_valid()) { - Ref<StyleBox> style = get_theme_stylebox("button"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("button")); Ref<Texture2D> rb = tabs[i].right_button; - w += get_theme_constant("hseparation"); + w += get_theme_constant(SNAME("hseparation")); Rect2 rb_rect; rb_rect.size = style->get_minimum_size() + rb->get_size(); @@ -399,7 +400,7 @@ void Tabs::_notification(int p_what) { if (rb_hover == i) { if (rb_pressing) { - get_theme_stylebox("button_pressed")->draw(ci, rb_rect); + get_theme_stylebox(SNAME("button_pressed"))->draw(ci, rb_rect); } else { style->draw(ci, rb_rect); } @@ -415,10 +416,10 @@ void Tabs::_notification(int p_what) { } if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current)) { - Ref<StyleBox> style = get_theme_stylebox("button"); + Ref<StyleBox> style = get_theme_stylebox(SNAME("button")); Ref<Texture2D> cb = close; - w += get_theme_constant("hseparation"); + w += get_theme_constant(SNAME("hseparation")); Rect2 cb_rect; cb_rect.size = style->get_minimum_size() + cb->get_size(); @@ -431,7 +432,7 @@ void Tabs::_notification(int p_what) { if (!tabs[i].disabled && cb_hover == i) { if (cb_pressing) { - get_theme_stylebox("button_pressed")->draw(ci, cb_rect); + get_theme_stylebox(SNAME("button_pressed"))->draw(ci, cb_rect); } else { style->draw(ci, cb_rect); } @@ -502,7 +503,7 @@ void Tabs::set_current_tab(int p_current) { _update_cache(); update(); - emit_signal("tab_changed", p_current); + emit_signal(SNAME("tab_changed"), p_current); } int Tabs::get_current_tab() const { @@ -528,7 +529,7 @@ bool Tabs::get_offset_buttons_visible() const { void Tabs::set_tab_title(int p_tab, const String &p_title) { ERR_FAIL_INDEX(p_tab, tabs.size()); tabs.write[p_tab].text = p_title; - tabs.write[p_tab].xl_text = tr(p_title); + tabs.write[p_tab].xl_text = atr(p_title); _shape(p_tab); update(); minimum_size_changed(); @@ -658,7 +659,7 @@ void Tabs::_update_hover() { } if (hover != hover_now) { hover = hover_now; - emit_signal("tab_hovered", hover); + emit_signal(SNAME("tab_hovered"), hover); } if (hover_buttons == -1) { // no hover @@ -668,11 +669,11 @@ void Tabs::_update_hover() { } void Tabs::_update_cache() { - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); - Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); - Ref<Texture2D> incr = get_theme_icon("increment"); - Ref<Texture2D> decr = get_theme_icon("decrement"); + Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); + Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); + Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); int w = 0; @@ -711,12 +712,12 @@ void Tabs::_update_cache() { slen = m_width - (sb->get_margin(SIDE_LEFT) + sb->get_margin(SIDE_RIGHT)); if (tabs[i].icon.is_valid()) { slen -= tabs[i].icon->get_width(); - slen -= get_theme_constant("hseparation"); + slen -= get_theme_constant(SNAME("hseparation")); } if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current)) { - Ref<Texture2D> cb = get_theme_icon("close"); + Ref<Texture2D> cb = get_theme_icon(SNAME("close")); slen -= cb->get_width(); - slen -= get_theme_constant("hseparation"); + slen -= get_theme_constant(SNAME("hseparation")); } slen = MAX(slen, 1); lsize = m_width; @@ -741,10 +742,10 @@ void Tabs::_on_mouse_exited() { void Tabs::add_tab(const String &p_str, const Ref<Texture2D> &p_icon) { Tab t; t.text = p_str; - t.xl_text = tr(p_str); - t.text_buf.instance(); + t.xl_text = atr(p_str); + t.text_buf.instantiate(); t.text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); - t.text_buf->add_string(t.xl_text, get_theme_font("font"), get_theme_font_size("font_size"), Dictionary(), TranslationServer::get_singleton()->get_tool_locale()); + t.text_buf->add_string(t.xl_text, get_theme_font(SNAME("font")), get_theme_font_size(SNAME("font_size")), Dictionary(), TranslationServer::get_singleton()->get_tool_locale()); t.icon = p_icon; t.disabled = false; t.ofs_cache = 0; @@ -752,7 +753,7 @@ void Tabs::add_tab(const String &p_str, const Ref<Texture2D> &p_icon) { tabs.push_back(t); _update_cache(); - call_deferred("_update_hover"); + call_deferred(SNAME("_update_hover")); update(); minimum_size_changed(); } @@ -761,7 +762,7 @@ void Tabs::clear_tabs() { tabs.clear(); current = 0; previous = 0; - call_deferred("_update_hover"); + call_deferred(SNAME("_update_hover")); update(); } @@ -772,7 +773,7 @@ void Tabs::remove_tab(int p_idx) { current--; } _update_cache(); - call_deferred("_update_hover"); + call_deferred(SNAME("_update_hover")); update(); minimum_size_changed(); @@ -869,7 +870,7 @@ void Tabs::drop_data(const Point2 &p_point, const Variant &p_data) { hover_now = get_tab_count() - 1; } move_tab(tab_from_id, hover_now); - emit_signal("reposition_active_tab_request", hover_now); + emit_signal(SNAME("reposition_active_tab_request"), hover_now); set_current_tab(hover_now); } else if (get_tabs_rearrange_group() != -1) { // drag and drop between Tabs @@ -886,7 +887,7 @@ void Tabs::drop_data(const Point2 &p_point, const Variant &p_data) { tabs.insert(hover_now, moving_tab); from_tabs->remove_tab(tab_from_id); set_current_tab(hover_now); - emit_signal("tab_changed", hover_now); + emit_signal(SNAME("tab_changed"), hover_now); _update_cache(); } } @@ -948,9 +949,9 @@ void Tabs::move_tab(int from, int to) { int Tabs::get_tab_width(int p_idx) const { ERR_FAIL_INDEX_V(p_idx, tabs.size(), 0); - Ref<StyleBox> tab_unselected = get_theme_stylebox("tab_unselected"); - Ref<StyleBox> tab_selected = get_theme_stylebox("tab_selected"); - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); + Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); + Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); + Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); int x = 0; @@ -958,7 +959,7 @@ int Tabs::get_tab_width(int p_idx) const { if (tex.is_valid()) { x += tex->get_width(); if (tabs[p_idx].text != "") { - x += get_theme_constant("hseparation"); + x += get_theme_constant(SNAME("hseparation")); } } @@ -975,13 +976,13 @@ int Tabs::get_tab_width(int p_idx) const { if (tabs[p_idx].right_button.is_valid()) { Ref<Texture2D> rb = tabs[p_idx].right_button; x += rb->get_width(); - x += get_theme_constant("hseparation"); + x += get_theme_constant(SNAME("hseparation")); } if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && p_idx == current)) { - Ref<Texture2D> cb = get_theme_icon("close"); + Ref<Texture2D> cb = get_theme_icon(SNAME("close")); x += cb->get_width(); - x += get_theme_constant("hseparation"); + x += get_theme_constant(SNAME("hseparation")); } return x; @@ -992,8 +993,8 @@ void Tabs::_ensure_no_over_offset() { return; } - Ref<Texture2D> incr = get_theme_icon("increment"); - Ref<Texture2D> decr = get_theme_icon("decrement"); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); int limit = get_size().width; int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); @@ -1033,8 +1034,8 @@ void Tabs::ensure_tab_visible(int p_idx) { } int prev_offset = offset; - Ref<Texture2D> incr = get_theme_icon("increment"); - Ref<Texture2D> decr = get_theme_icon("decrement"); + Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); + Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); int limit = get_size().width; int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); @@ -1106,7 +1107,6 @@ bool Tabs::get_select_with_rmb() const { } void Tabs::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &Tabs::_gui_input); ClassDB::bind_method(D_METHOD("_update_hover"), &Tabs::_update_hover); ClassDB::bind_method(D_METHOD("get_tab_count"), &Tabs::get_tab_count); ClassDB::bind_method(D_METHOD("set_current_tab", "tab_idx"), &Tabs::set_current_tab); diff --git a/scene/gui/tabs.h b/scene/gui/tabs.h index 61c9a5d96a..b044453803 100644 --- a/scene/gui/tabs.h +++ b/scene/gui/tabs.h @@ -112,7 +112,7 @@ private: void _shape(int p_tab); protected: - void _gui_input(const Ref<InputEvent> &p_event); + virtual void gui_input(const Ref<InputEvent> &p_event) override; void _notification(int p_what); static void _bind_methods(); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 4b199d1441..06dfc31621 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -37,6 +37,7 @@ #include "core/object/script_language.h" #include "core/os/keyboard.h" #include "core/os/os.h" +#include "core/string/string_builder.h" #include "core/string/translation.h" #include "scene/main/window.h" @@ -45,12 +46,6 @@ #include "editor/editor_scale.h" #endif -#define TAB_PIXELS - -inline bool _is_symbol(char32_t c) { - return is_symbol(c); -} - static bool _is_text_char(char32_t c) { return !is_symbol(c); } @@ -63,89 +58,73 @@ static bool _is_char(char32_t c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; } -static bool _is_pair_right_symbol(char32_t c) { - return c == '"' || - c == '\'' || - c == ')' || - c == ']' || - c == '}'; -} - -static bool _is_pair_left_symbol(char32_t c) { - return c == '"' || - c == '\'' || - c == '(' || - c == '[' || - c == '{'; -} - -static bool _is_pair_symbol(char32_t c) { - return _is_pair_left_symbol(c) || _is_pair_right_symbol(c); -} - -static char32_t _get_right_pair_symbol(char32_t c) { - if (c == '"') { - return '"'; - } - if (c == '\'') { - return '\''; - } - if (c == '(') { - return ')'; - } - if (c == '[') { - return ']'; - } - if (c == '{') { - return '}'; - } - return 0; -} - -static int _find_first_non_whitespace_column_of_line(const String &line) { - int left = 0; - while (left < line.length() && _is_whitespace(line[left])) { - left++; - } - return left; -} - +/////////////////////////////////////////////////////////////////////////////// +/// TEXT /// /////////////////////////////////////////////////////////////////////////////// void TextEdit::Text::set_font(const Ref<Font> &p_font) { + if (font == p_font) { + return; + } font = p_font; + is_dirty = true; } void TextEdit::Text::set_font_size(int p_font_size) { + if (font_size == p_font_size) { + return; + } font_size = p_font_size; + is_dirty = true; +} + +void TextEdit::Text::set_tab_size(int p_tab_size) { + if (tab_size == p_tab_size) { + return; + } + tab_size = p_tab_size; + tab_size_dirty = true; } -void TextEdit::Text::set_indent_size(int p_indent_size) { - indent_size = p_indent_size; +int TextEdit::Text::get_tab_size() const { + return tab_size; } void TextEdit::Text::set_font_features(const Dictionary &p_features) { + if (opentype_features.hash() == p_features.hash()) { + return; + } opentype_features = p_features; + is_dirty = true; } -void TextEdit::Text::set_direction_and_language(TextServer::Direction p_direction, String p_language) { +void TextEdit::Text::set_direction_and_language(TextServer::Direction p_direction, const String &p_language) { + if (direction == p_direction && language == p_language) { + return; + } direction = p_direction; language = p_language; + is_dirty = true; } void TextEdit::Text::set_draw_control_chars(bool p_draw_control_chars) { + if (draw_control_chars == p_draw_control_chars) { + return; + } draw_control_chars = p_draw_control_chars; + is_dirty = true; } -int TextEdit::Text::get_line_width(int p_line) const { +int TextEdit::Text::get_line_width(int p_line, int p_wrap_index) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); + if (p_wrap_index != -1) { + return text[p_line].data_buf->get_line_width(p_wrap_index); + } return text[p_line].data_buf->get_size().x; } -int TextEdit::Text::get_line_height(int p_line, int p_wrap_index) const { - ERR_FAIL_INDEX_V(p_line, text.size(), 0); - - return text[p_line].data_buf->get_line_size(p_wrap_index).y; +int TextEdit::Text::get_line_height() const { + return line_height; } void TextEdit::Text::set_width(float p_width) { @@ -177,6 +156,36 @@ _FORCE_INLINE_ const String &TextEdit::Text::operator[](int p_line) const { return text[p_line].data; } +void TextEdit::Text::_calculate_line_height() { + int height = 0; + for (int i = 0; i < text.size(); i++) { + // Found another line with the same height...nothing to update. + if (text[i].height == line_height) { + height = line_height; + break; + } + height = MAX(height, text[i].height); + } + line_height = height; +} + +void TextEdit::Text::_calculate_max_line_width() { + int width = 0; + for (int i = 0; i < text.size(); i++) { + if (is_hidden(i)) { + continue; + } + + // Found another line with the same width...nothing to update. + if (text[i].width == max_width) { + width = max_width; + break; + } + width = MAX(width, text[i].width); + } + max_width = width; +} + void TextEdit::Text::invalidate_cache(int p_line, int p_column, const String &p_ime_text, const Vector<Vector2i> &p_bidi_override) { ERR_FAIL_INDEX(p_line, text.size()); @@ -201,28 +210,70 @@ void TextEdit::Text::invalidate_cache(int p_line, int p_column, const String &p_ } // Apply tab align. - if (indent_size > 0) { + if (tab_size > 0) { Vector<float> tabs; - tabs.push_back(font->get_char_size(' ', 0, font_size).width * indent_size); + tabs.push_back(font->get_char_size(' ', 0, font_size).width * tab_size); text.write[p_line].data_buf->tab_align(tabs); } + + // Update height. + const int old_height = text.write[p_line].height; + const int wrap_amount = get_line_wrap_amount(p_line); + int height = font->get_height(font_size); + for (int i = 0; i <= wrap_amount; i++) { + height = MAX(height, text[p_line].data_buf->get_line_size(i).y); + } + text.write[p_line].height = height; + + // If this line has shrunk, this may no longer the the tallest line. + if (old_height == line_height && height < line_height) { + _calculate_line_height(); + } else { + line_height = MAX(height, line_height); + } + + // Update width. + const int old_width = text.write[p_line].width; + int width = get_line_width(p_line); + text.write[p_line].width = width; + + // If this line has shrunk, this may no longer the the longest line. + if (old_width == max_width && width < max_width) { + _calculate_max_line_width(); + } else if (!is_hidden(p_line)) { + max_width = MAX(width, max_width); + } } void TextEdit::Text::invalidate_all_lines() { for (int i = 0; i < text.size(); i++) { text.write[i].data_buf->set_width(width); - if (indent_size > 0) { - Vector<float> tabs; - tabs.push_back(font->get_char_size(' ', 0, font_size).width * indent_size); - text.write[i].data_buf->tab_align(tabs); + if (tab_size_dirty) { + if (tab_size > 0) { + Vector<float> tabs; + tabs.push_back(font->get_char_size(' ', 0, font_size).width * tab_size); + text.write[i].data_buf->tab_align(tabs); + } + // Tabs have changes, force width update. + text.write[i].width = get_line_width(i); } } + + if (tab_size_dirty) { + _calculate_max_line_width(); + tab_size_dirty = false; + } } void TextEdit::Text::invalidate_all() { + if (!is_dirty) { + return; + } + for (int i = 0; i < text.size(); i++) { invalidate_cache(i); } + is_dirty = false; } void TextEdit::Text::clear() { @@ -230,16 +281,8 @@ void TextEdit::Text::clear() { insert(0, "", Vector<Vector2i>()); } -int TextEdit::Text::get_max_width(bool p_exclude_hidden) const { - // Quite some work, but should be fast enough. - - int max = 0; - for (int i = 0; i < text.size(); i++) { - if (!p_exclude_hidden || !is_hidden(i)) { - max = MAX(max, get_line_width(i)); - } - } - return max; +int TextEdit::Text::get_max_width() const { + return max_width; } void TextEdit::Text::set(int p_line, const String &p_text, const Vector<Vector2i> &p_bidi_override) { @@ -262,7 +305,20 @@ void TextEdit::Text::insert(int p_at, const String &p_text, const Vector<Vector2 } void TextEdit::Text::remove(int p_at) { + int height = text[p_at].height; + int width = text[p_at].width; + text.remove(p_at); + + // If this is the tallest line, we need to get the next tallest. + if (height == line_height) { + _calculate_line_height(); + } + + // If this is the longest line, we need to get the next longest. + if (width == max_width) { + _calculate_max_line_width(); + } } void TextEdit::Text::add_gutter(int p_at) { @@ -289,269 +345,37 @@ void TextEdit::Text::move_gutters(int p_from_line, int p_to_line) { text.write[p_from_line].gutters.resize(gutter_count); } -//////////////////////////////////////////////////////////////////////////////// - -void TextEdit::_update_scrollbars() { - Size2 size = get_size(); - Size2 hmin = h_scroll->get_combined_minimum_size(); - Size2 vmin = v_scroll->get_combined_minimum_size(); - - v_scroll->set_begin(Point2(size.width - vmin.width, cache.style_normal->get_margin(SIDE_TOP))); - v_scroll->set_end(Point2(size.width, size.height - cache.style_normal->get_margin(SIDE_TOP) - cache.style_normal->get_margin(SIDE_BOTTOM))); - - h_scroll->set_begin(Point2(0, size.height - hmin.height)); - h_scroll->set_end(Point2(size.width - vmin.width, size.height)); - - int visible_rows = get_visible_rows(); - int total_rows = get_total_visible_rows(); - if (scroll_past_end_of_file_enabled) { - total_rows += visible_rows - 1; - } - - int visible_width = size.width - cache.style_normal->get_minimum_size().width; - int total_width = text.get_max_width(true) + vmin.x + gutters_width + gutter_padding; - - if (draw_minimap) { - total_width += cache.minimap_width; - } - - updating_scrolls = true; - - if (total_rows > visible_rows) { - v_scroll->show(); - v_scroll->set_max(total_rows + get_visible_rows_offset()); - v_scroll->set_page(visible_rows + get_visible_rows_offset()); - if (smooth_scroll_enabled) { - v_scroll->set_step(0.25); - } else { - v_scroll->set_step(1); - } - set_v_scroll(get_v_scroll()); - - } else { - cursor.line_ofs = 0; - cursor.wrap_ofs = 0; - v_scroll->set_value(0); - v_scroll->hide(); - } - - if (total_width > visible_width && !is_wrap_enabled()) { - h_scroll->show(); - h_scroll->set_max(total_width); - h_scroll->set_page(visible_width); - if (cursor.x_ofs > (total_width - visible_width)) { - cursor.x_ofs = (total_width - visible_width); - } - if (fabs(h_scroll->get_value() - (double)cursor.x_ofs) >= 1) { - h_scroll->set_value(cursor.x_ofs); - } - - } else { - cursor.x_ofs = 0; - h_scroll->set_value(0); - h_scroll->hide(); - } - - updating_scrolls = false; -} - -void TextEdit::_click_selection_held() { - // Warning: is_mouse_button_pressed(MOUSE_BUTTON_LEFT) returns false for double+ clicks, so this doesn't work for MODE_WORD - // and MODE_LINE. However, moving the mouse triggers _gui_input, which calls these functions too, so that's not a huge problem. - // I'm unsure if there's an actual fix that doesn't have a ton of side effects. - if (Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT) && selection.selecting_mode != SelectionMode::SELECTION_MODE_NONE) { - switch (selection.selecting_mode) { - case SelectionMode::SELECTION_MODE_POINTER: { - _update_selection_mode_pointer(); - } break; - case SelectionMode::SELECTION_MODE_WORD: { - _update_selection_mode_word(); - } break; - case SelectionMode::SELECTION_MODE_LINE: { - _update_selection_mode_line(); - } break; - default: { - break; - } - } - } else { - click_select_held->stop(); - } -} - -Point2 TextEdit::_get_local_mouse_pos() const { - Point2 mp = get_local_mouse_position(); - if (is_layout_rtl()) { - mp.x = get_size().width - mp.x; - } - return mp; -} - -void TextEdit::_update_selection_mode_pointer() { - dragging_selection = true; - Point2 mp = _get_local_mouse_pos(); - - int row, col; - _get_mouse_pos(Point2i(mp.x, mp.y), row, col); - - select(selection.selecting_line, selection.selecting_column, row, col); - - cursor_set_line(row, false); - cursor_set_column(col); - update(); - - click_select_held->start(); -} - -void TextEdit::_update_selection_mode_word() { - dragging_selection = true; - Point2 mp = _get_local_mouse_pos(); - - int row, col; - _get_mouse_pos(Point2i(mp.x, mp.y), row, col); - - String line = text[row]; - int cursor_pos = CLAMP(col, 0, line.length()); - int beg = cursor_pos; - int end = beg; - Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(row)->get_rid()); - for (int i = 0; i < words.size(); i++) { - if (words[i].x < cursor_pos && words[i].y > cursor_pos) { - beg = words[i].x; - end = words[i].y; - break; - } - } - - // Initial selection. - if (!selection.active) { - select(row, beg, row, end); - selection.selecting_column = beg; - selection.selected_word_beg = beg; - selection.selected_word_end = end; - selection.selected_word_origin = beg; - cursor_set_line(selection.to_line, false); - cursor_set_column(selection.to_column); - } else { - if ((col <= selection.selected_word_origin && row == selection.selecting_line) || row < selection.selecting_line) { - selection.selecting_column = selection.selected_word_end; - select(row, beg, selection.selecting_line, selection.selected_word_end); - cursor_set_line(selection.from_line, false); - cursor_set_column(selection.from_column); - } else { - selection.selecting_column = selection.selected_word_beg; - select(selection.selecting_line, selection.selected_word_beg, row, end); - cursor_set_line(selection.to_line, false); - cursor_set_column(selection.to_column); - } - } - - update(); - - click_select_held->start(); -} - -void TextEdit::_update_selection_mode_line() { - dragging_selection = true; - Point2 mp = _get_local_mouse_pos(); - - int row, col; - _get_mouse_pos(Point2i(mp.x, mp.y), row, col); - - col = 0; - if (row < selection.selecting_line) { - // Cursor is above us. - cursor_set_line(row - 1, false); - selection.selecting_column = text[selection.selecting_line].length(); - } else { - // Cursor is below us. - cursor_set_line(row + 1, false); - selection.selecting_column = 0; - col = text[row].length(); - } - cursor_set_column(0); - - select(selection.selecting_line, selection.selecting_column, row, col); - update(); - - click_select_held->start(); -} - -void TextEdit::_update_minimap_click() { - Point2 mp = _get_local_mouse_pos(); - - int xmargin_end = get_size().width - cache.style_normal->get_margin(SIDE_RIGHT); - if (!dragging_minimap && (mp.x < xmargin_end - minimap_width || mp.y > xmargin_end)) { - minimap_clicked = false; - return; - } - minimap_clicked = true; - dragging_minimap = true; - - int row; - _get_minimap_mouse_row(Point2i(mp.x, mp.y), row); - - if (row >= get_first_visible_line() && (row < get_last_full_visible_line() || row >= (text.size() - 1))) { - minimap_scroll_ratio = v_scroll->get_as_ratio(); - minimap_scroll_click_pos = mp.y; - can_drag_minimap = true; - return; - } - - int wi; - int first_line = row - num_lines_from_rows(row, 0, -get_visible_rows() / 2, wi) + 1; - double delta = get_scroll_pos_for_line(first_line, wi) - get_v_scroll(); - if (delta < 0) { - _scroll_up(-delta); - } else { - _scroll_down(delta); - } -} - -void TextEdit::_update_minimap_drag() { - if (!can_drag_minimap) { - return; - } - - int control_height = _get_control_height(); - int scroll_height = v_scroll->get_max() * (minimap_char_size.y + minimap_line_spacing); - if (control_height > scroll_height) { - control_height = scroll_height; - } - - Point2 mp = _get_local_mouse_pos(); - - double diff = (mp.y - minimap_scroll_click_pos) / control_height; - v_scroll->set_as_ratio(minimap_scroll_ratio + diff); -} +/////////////////////////////////////////////////////////////////////////////// +/// TEXT EDIT /// +/////////////////////////////////////////////////////////////////////////////// void TextEdit::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { _update_caches(); - if (cursor_changed_dirty) { - MessageQueue::get_singleton()->push_call(this, "_cursor_changed_emit"); + if (caret_pos_dirty) { + MessageQueue::get_singleton()->push_call(this, "_emit_caret_changed"); } if (text_changed_dirty) { MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); } - _update_wrap_at(true); + _update_wrap_at_column(true); } break; case NOTIFICATION_RESIZED: { _update_scrollbars(); - _update_wrap_at(); + _update_wrap_at_column(); } break; case NOTIFICATION_VISIBILITY_CHANGED: { if (is_visible()) { - call_deferred("_update_scrollbars"); - call_deferred("_update_wrap_at"); + call_deferred(SNAME("_update_scrollbars")); + call_deferred(SNAME("_update_wrap_at_column")); } } break; case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_THEME_CHANGED: { _update_caches(); - _update_wrap_at(true); + _update_wrap_at_column(true); } break; case NOTIFICATION_WM_WINDOW_FOCUS_IN: { window_has_focus = true; @@ -586,8 +410,8 @@ void TextEdit::_notification(int p_what) { } break; case NOTIFICATION_DRAW: { if (first_draw) { - // Size may not be the final one, so attempts to ensure cursor was visible may have failed. - adjust_viewport_to_cursor(); + // Size may not be the final one, so attempts to ensure caret was visible may have failed. + adjust_viewport_to_caret(); first_draw = false; } @@ -600,61 +424,36 @@ void TextEdit::_notification(int p_what) { Size2 size = get_size(); bool rtl = is_layout_rtl(); - if ((!has_focus() && !menu->has_focus()) || !window_has_focus) { + if ((!has_focus() && !(menu && menu->has_focus())) || !window_has_focus) { draw_caret = false; } - cache.minimap_width = 0; - if (draw_minimap) { - cache.minimap_width = minimap_width; - } - _update_scrollbars(); RID ci = get_canvas_item(); RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), true); - int xmargin_beg = cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding; + int xmargin_beg = style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding; - int xmargin_end = size.width - cache.style_normal->get_margin(SIDE_RIGHT) - cache.minimap_width; + int xmargin_end = size.width - style_normal->get_margin(SIDE_RIGHT); + if (draw_minimap) { + xmargin_end -= minimap_width; + } // Let's do it easy for now. - cache.style_normal->draw(ci, Rect2(Point2(), size)); - if (readonly) { - cache.style_readonly->draw(ci, Rect2(Point2(), size)); + style_normal->draw(ci, Rect2(Point2(), size)); + if (!editable) { + style_readonly->draw(ci, Rect2(Point2(), size)); draw_caret = false; } if (has_focus()) { - cache.style_focus->draw(ci, Rect2(Point2(), size)); + style_focus->draw(ci, Rect2(Point2(), size)); } - int visible_rows = get_visible_rows() + 1; + int visible_rows = get_visible_line_count() + 1; - Color color = readonly ? cache.font_readonly_color : cache.font_color; + Color color = !editable ? font_readonly_color : font_color; - if (cache.background_color.a > 0.01) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(), get_size()), cache.background_color); - } - - if (line_length_guidelines) { - const int hard_x = xmargin_beg + (int)cache.font->get_char_size('0', 0, cache.font_size).width * line_length_guideline_hard_col - cursor.x_ofs; - if (hard_x > xmargin_beg && hard_x < xmargin_end) { - if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(size.width - hard_x, 0), Point2(size.width - hard_x, size.height), cache.line_length_guideline_color); - } else { - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(hard_x, 0), Point2(hard_x, size.height), cache.line_length_guideline_color); - } - } - - // Draw a "Soft" line length guideline, less visible than the hard line length guideline. - // It's usually set to a lower column compared to the hard line length guideline. - // Only drawn if its column differs from the hard line length guideline. - const int soft_x = xmargin_beg + (int)cache.font->get_char_size('0', 0, cache.font_size).width * line_length_guideline_soft_col - cursor.x_ofs; - if (hard_x != soft_x && soft_x > xmargin_beg && soft_x < xmargin_end) { - if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(size.width - soft_x, 0), Point2(size.width - soft_x, size.height), cache.line_length_guideline_color * Color(1, 1, 1, 0.5)); - } else { - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2(soft_x, 0), Point2(soft_x, size.height), cache.line_length_guideline_color * Color(1, 1, 1, 0.5)); - } - } + if (background_color.a > 0.01) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(), get_size()), background_color); } int brace_open_match_line = -1; @@ -666,10 +465,10 @@ void TextEdit::_notification(int p_what) { bool brace_close_matching = false; bool brace_close_mismatch = false; - if (brace_matching_enabled && cursor.line >= 0 && cursor.line < text.size() && cursor.column >= 0) { - if (cursor.column < text[cursor.line].length()) { + if (highlight_matching_braces_enabled && caret.line >= 0 && caret.line < text.size() && caret.column >= 0) { + if (caret.column < text[caret.line].length()) { // Check for open. - char32_t c = text[cursor.line][cursor.column]; + char32_t c = text[caret.line][caret.column]; char32_t closec = 0; if (c == '[') { @@ -683,8 +482,8 @@ void TextEdit::_notification(int p_what) { if (closec != 0) { int stack = 1; - for (int i = cursor.line; i < text.size(); i++) { - int from = i == cursor.line ? cursor.column + 1 : 0; + for (int i = caret.line; i < text.size(); i++) { + int from = i == caret.line ? caret.column + 1 : 0; for (int j = from; j < text[i].length(); j++) { char32_t cc = text[i][j]; // Ignore any brackets inside a string. @@ -734,8 +533,8 @@ void TextEdit::_notification(int p_what) { } } - if (cursor.column > 0) { - char32_t c = text[cursor.line][cursor.column - 1]; + if (caret.column > 0) { + char32_t c = text[caret.line][caret.column - 1]; char32_t closec = 0; if (c == ']') { @@ -749,8 +548,8 @@ void TextEdit::_notification(int p_what) { if (closec != 0) { int stack = 1; - for (int i = cursor.line; i >= 0; i--) { - int from = i == cursor.line ? cursor.column - 2 : text[i].length() - 1; + for (int i = caret.line; i >= 0; i--) { + int from = i == caret.line ? caret.column - 2 : text[i].length() - 1; for (int j = from; j >= 0; j--) { char32_t cc = text[i][j]; // Ignore any brackets inside a string. @@ -801,28 +600,23 @@ void TextEdit::_notification(int p_what) { } } - bool is_cursor_line_visible = false; - Point2 cursor_pos; - // Get the highlighted words. - String highlighted_text = get_selection_text(); + String highlighted_text = get_selected_text(); // Check if highlighted words contain only whitespaces (tabs or spaces). bool only_whitespaces_highlighted = highlighted_text.strip_edges() == String(); - int cursor_wrap_index = get_cursor_wrap_index(); - - //FontDrawer drawer(cache.font, Color(1, 1, 1)); + const int caret_wrap_index = get_caret_wrap_index(); int first_visible_line = get_first_visible_line() - 1; int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0); - draw_amount += times_line_wraps(first_visible_line + 1); + draw_amount += get_line_wrap_count(first_visible_line + 1); // minimap if (draw_minimap) { - int minimap_visible_lines = _get_minimap_visible_rows(); + int minimap_visible_lines = get_minimap_visible_lines(); int minimap_line_height = (minimap_char_size.y + minimap_line_spacing); - int minimap_tab_size = minimap_char_size.x * indent_size; + int minimap_tab_size = minimap_char_size.x * text.get_tab_size(); // calculate viewport size and y offset int viewport_height = (draw_amount - 1) * minimap_line_height; @@ -831,21 +625,32 @@ void TextEdit::_notification(int p_what) { // calculate the first line. int num_lines_before = round((viewport_offset_y) / minimap_line_height); - int wi; int minimap_line = (v_scroll->get_max() <= minimap_visible_lines) ? -1 : first_visible_line; if (minimap_line >= 0) { - minimap_line -= num_lines_from_rows(first_visible_line, 0, -num_lines_before, wi); + minimap_line -= get_next_visible_line_index_offset_from(first_visible_line, 0, -num_lines_before).x; minimap_line -= (minimap_line > 0 && smooth_scroll_enabled ? 1 : 0); } - int minimap_draw_amount = minimap_visible_lines + times_line_wraps(minimap_line + 1); + int minimap_draw_amount = minimap_visible_lines + get_line_wrap_count(minimap_line + 1); + + // Draw the minimap. + + // Add visual feedback when dragging or hovering the the visible area rectangle. + float viewport_alpha; + if (dragging_minimap) { + viewport_alpha = 0.25; + } else if (hovering_minimap) { + viewport_alpha = 0.175; + } else { + viewport_alpha = 0.1; + } - // draw the minimap - Color viewport_color = (cache.background_color.get_v() < 0.5) ? Color(1, 1, 1, 0.1) : Color(0, 0, 0, 0.1); + const Color viewport_color = (background_color.get_v() < 0.5) ? Color(1, 1, 1, viewport_alpha) : Color(0, 0, 0, viewport_alpha); if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - cache.minimap_width, viewport_offset_y, cache.minimap_width, viewport_height), viewport_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - minimap_width, viewport_offset_y, minimap_width, viewport_height), viewport_color); } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), viewport_offset_y, cache.minimap_width, viewport_height), viewport_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), viewport_offset_y, minimap_width, viewport_height), viewport_color); } + for (int i = 0; i < minimap_draw_amount; i++) { minimap_line++; @@ -853,7 +658,7 @@ void TextEdit::_notification(int p_what) { break; } - while (is_line_hidden(minimap_line)) { + while (_is_line_hidden(minimap_line)) { minimap_line++; if (minimap_line < 0 || minimap_line >= (int)text.size()) { break; @@ -868,13 +673,13 @@ void TextEdit::_notification(int p_what) { Color line_background_color = text.get_line_background_color(minimap_line); line_background_color.a *= 0.6; - Color current_color = cache.font_color; - if (readonly) { - current_color = cache.font_readonly_color; + Color current_color = font_color; + if (!editable) { + current_color = font_readonly_color; } - Vector<String> wrap_rows = get_wrap_rows_text(minimap_line); - int line_wrap_amount = times_line_wraps(minimap_line); + Vector<String> wrap_rows = get_line_wrapped_text(minimap_line); + int line_wrap_amount = get_line_wrap_count(minimap_line); int last_wrap_column = 0; for (int line_wrap_index = 0; line_wrap_index < line_wrap_amount + 1; line_wrap_index++) { @@ -887,7 +692,7 @@ void TextEdit::_notification(int p_what) { const String &str = wrap_rows[line_wrap_index]; int indent_px = line_wrap_index != 0 ? get_indent_level(minimap_line) : 0; - if (indent_px >= wrap_at) { + if (indent_px >= wrap_at_column) { indent_px = 0; } indent_px = minimap_char_size.x * indent_px; @@ -896,17 +701,17 @@ void TextEdit::_notification(int p_what) { last_wrap_column += wrap_rows[line_wrap_index - 1].length(); } - if (minimap_line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { + if (minimap_line == caret.line && caret_wrap_index == line_wrap_index && highlight_current_line) { if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - cache.minimap_width, i * 3, cache.minimap_width, 2), cache.current_line_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - minimap_width, i * 3, minimap_width, 2), current_line_color); } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), i * 3, cache.minimap_width, 2), cache.current_line_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), i * 3, minimap_width, 2), current_line_color); } } else if (line_background_color != Color(0, 0, 0, 0)) { if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - cache.minimap_width, i * 3, cache.minimap_width, 2), line_background_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - (xmargin_end + 2) - minimap_width, i * 3, minimap_width, 2), line_background_color); } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), i * 3, cache.minimap_width, 2), line_background_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), i * 3, minimap_width, 2), line_background_color); } } @@ -914,10 +719,11 @@ void TextEdit::_notification(int p_what) { int characters = 0; int tabs = 0; for (int j = 0; j < str.length(); j++) { - if (color_map.has(last_wrap_column + j)) { - current_color = color_map[last_wrap_column + j].get("color"); - if (readonly) { - current_color.a = cache.font_readonly_color.a; + const Variant *color_data = color_map.getptr(last_wrap_column + j); + if (color_data != nullptr) { + current_color = (color_data->operator Dictionary()).get("color", font_color); + if (!editable) { + current_color.a = font_readonly_color.a; } } color = current_color; @@ -927,7 +733,7 @@ void TextEdit::_notification(int p_what) { } int xpos = indent_px + ((xmargin_end + minimap_char_size.x) + (minimap_char_size.x * j)) + tabs; - bool out_of_bounds = (xpos >= xmargin_end + cache.minimap_width); + bool out_of_bounds = (xpos >= xmargin_end + minimap_width); bool is_whitespace = _is_whitespace(str[j]); if (!is_whitespace) { @@ -978,16 +784,17 @@ void TextEdit::_notification(int p_what) { int top_limit_y = 0; int bottom_limit_y = get_size().height; - if (readonly) { - top_limit_y += cache.style_readonly->get_margin(SIDE_TOP); - bottom_limit_y -= cache.style_readonly->get_margin(SIDE_BOTTOM); + if (!editable) { + top_limit_y += style_readonly->get_margin(SIDE_TOP); + bottom_limit_y -= style_readonly->get_margin(SIDE_BOTTOM); } else { - top_limit_y += cache.style_normal->get_margin(SIDE_TOP); - bottom_limit_y -= cache.style_normal->get_margin(SIDE_BOTTOM); + top_limit_y += style_normal->get_margin(SIDE_TOP); + bottom_limit_y -= style_normal->get_margin(SIDE_BOTTOM); } // draw main text - int row_height = get_row_height(); + caret.visible = false; + int row_height = get_line_height(); int line = first_visible_line; for (int i = 0; i < draw_amount; i++) { line++; @@ -996,7 +803,7 @@ void TextEdit::_notification(int p_what) { continue; } - while (is_line_hidden(line)) { + while (_is_line_hidden(line)) { line++; if (line < 0 || line >= (int)text.size()) { break; @@ -1010,12 +817,12 @@ void TextEdit::_notification(int p_what) { Dictionary color_map = _get_line_syntax_highlighting(line); // Ensure we at least use the font color. - Color current_color = readonly ? cache.font_readonly_color : cache.font_color; + Color current_color = !editable ? font_readonly_color : font_color; const Ref<TextParagraph> ldata = text.get_line_data(line); - Vector<String> wrap_rows = get_wrap_rows_text(line); - int line_wrap_amount = times_line_wraps(line); + Vector<String> wrap_rows = get_line_wrapped_text(line); + int line_wrap_amount = get_line_wrap_count(line); for (int line_wrap_index = 0; line_wrap_index <= line_wrap_amount; line_wrap_index++) { if (line_wrap_index != 0) { @@ -1026,21 +833,21 @@ void TextEdit::_notification(int p_what) { } const String &str = wrap_rows[line_wrap_index]; - int char_margin = xmargin_beg - cursor.x_ofs; + int char_margin = xmargin_beg - caret.x_ofs; int ofs_x = 0; int ofs_y = 0; - if (readonly) { - ofs_x = cache.style_readonly->get_offset().x / 2; - ofs_x -= cache.style_normal->get_offset().x / 2; - ofs_y = cache.style_readonly->get_offset().y / 2; + if (!editable) { + ofs_x = style_readonly->get_offset().x / 2; + ofs_x -= style_normal->get_offset().x / 2; + ofs_y = style_readonly->get_offset().y / 2; } else { - ofs_y = cache.style_normal->get_offset().y / 2; + ofs_y = style_normal->get_offset().y / 2; } - ofs_y += i * row_height + cache.line_spacing / 2; - ofs_y -= cursor.wrap_ofs * row_height; - ofs_y -= get_v_scroll_offset() * row_height; + ofs_y += i * row_height + line_spacing / 2; + ofs_y -= caret.wrap_ofs * row_height; + ofs_y -= _get_v_scroll_offset() * row_height; bool clipped = false; if (ofs_y + row_height < top_limit_y) { @@ -1065,30 +872,30 @@ void TextEdit::_notification(int p_what) { if (str.length() == 0) { // Draw line background if empty as we won't loop at all. - if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { + if (line == caret.line && caret_wrap_index == line_wrap_index && highlight_current_line) { if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), cache.current_line_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), current_line_color); } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), cache.current_line_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), current_line_color); } } // Give visual indication of empty selected line. if (selection.active && line >= selection.from_line && line <= selection.to_line && char_margin >= xmargin_beg) { - int char_w = cache.font->get_char_size(' ', 0, cache.font_size).width; + int char_w = font->get_char_size(' ', 0, font_size).width; if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - xmargin_beg - ofs_x - char_w, ofs_y, char_w, row_height), cache.selection_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - xmargin_beg - ofs_x - char_w, ofs_y, char_w, row_height), selection_color); } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, char_w, row_height), cache.selection_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, char_w, row_height), selection_color); } } } else { // If it has text, then draw current line marker in the margin, as line number etc will draw over it, draw the rest of line marker later. - if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { + if (line == caret.line && caret_wrap_index == line_wrap_index && highlight_current_line) { if (rtl) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), cache.current_line_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end, row_height), current_line_color); } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), cache.current_line_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), current_line_color); } } } @@ -1096,7 +903,7 @@ void TextEdit::_notification(int p_what) { if (line_wrap_index == 0) { // Only do these if we are on the first wrapped part of a line. - int gutter_offset = cache.style_normal->get_margin(SIDE_LEFT); + int gutter_offset = style_normal->get_margin(SIDE_LEFT); for (int g = 0; g < gutters.size(); g++) { const GutterInfo gutter = gutters[g]; @@ -1112,16 +919,16 @@ void TextEdit::_notification(int p_what) { } Ref<TextLine> tl; - tl.instance(); - tl->add_string(text, cache.font, cache.font_size); + tl.instantiate(); + tl->add_string(text, font, font_size); int yofs = ofs_y + (row_height - tl->get_size().y) / 2; - if (cache.outline_size > 0 && cache.outline_color.a > 0) { - tl->draw_outline(ci, Point2(gutter_offset + ofs_x, yofs), cache.outline_size, cache.outline_color); + if (outline_size > 0 && outline_color.a > 0) { + tl->draw_outline(ci, Point2(gutter_offset + ofs_x, yofs), outline_size, outline_color); } tl->draw(ci, Point2(gutter_offset + ofs_x, yofs), get_line_gutter_item_color(line, g)); } break; - case GUTTER_TPYE_ICON: { + case GUTTER_TYPE_ICON: { const Ref<Texture2D> icon = get_line_gutter_icon(line, g); if (icon.is_null()) { break; @@ -1149,7 +956,7 @@ void TextEdit::_notification(int p_what) { icon->draw_rect(ci, gutter_rect, false, get_line_gutter_item_color(line, g)); } break; - case GUTTER_TPYE_CUSTOM: { + case GUTTER_TYPE_CUSTOM: { if (gutter.custom_draw_obj.is_valid()) { Object *cdo = ObjectDB::get_instance(gutter.custom_draw_obj); if (cdo) { @@ -1169,7 +976,7 @@ void TextEdit::_notification(int p_what) { // Draw line. RID rid = ldata->get_line_rid(line_wrap_index); - float text_height = TS->shaped_text_get_size(rid).y + cache.font->get_spacing(Font::SPACING_TOP) + cache.font->get_spacing(Font::SPACING_BOTTOM); + float text_height = TS->shaped_text_get_size(rid).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM); if (rtl) { char_margin = size.width - char_margin - TS->shaped_text_get_size(rid).x; @@ -1187,10 +994,11 @@ void TextEdit::_notification(int p_what) { if (rect.position.x < xmargin_beg) { rect.size.x -= (xmargin_beg - rect.position.x); rect.position.x = xmargin_beg; - } else if (rect.position.x + rect.size.x > xmargin_end) { + } + if (rect.position.x + rect.size.x > xmargin_end) { rect.size.x = xmargin_end - rect.position.x; } - draw_rect(rect, cache.selection_color, true); + draw_rect(rect, selection_color, true); } } @@ -1210,8 +1018,8 @@ void TextEdit::_notification(int p_what) { } else if (rect.position.x + rect.size.x > xmargin_end) { rect.size.x = xmargin_end - rect.position.x; } - draw_rect(rect, cache.search_result_color, true); - draw_rect(rect, cache.search_result_border_color, false); + draw_rect(rect, search_result_color, true); + draw_rect(rect, search_result_border_color, false); } search_text_col = _get_column_pos_of_word(search_text, str, search_flags, search_text_col + 1); @@ -1233,18 +1041,18 @@ void TextEdit::_notification(int p_what) { } else if (rect.position.x + rect.size.x > xmargin_end) { rect.size.x = xmargin_end - rect.position.x; } - draw_rect(rect, cache.word_highlighted_color); + draw_rect(rect, word_highlighted_color); } highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, highlighted_text_col + 1); } } - if (!clipped && select_identifiers_enabled && highlighted_word.length() != 0) { // Highlight word - if (_is_char(highlighted_word[0]) || highlighted_word[0] == '.') { - int highlighted_word_col = _get_column_pos_of_word(highlighted_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); + if (!clipped && lookup_symbol_word.length() != 0) { // Highlight word + if (_is_char(lookup_symbol_word[0]) || lookup_symbol_word[0] == '.') { + int highlighted_word_col = _get_column_pos_of_word(lookup_symbol_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); while (highlighted_word_col != -1) { - Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_word_col + start, highlighted_word_col + highlighted_word.length() + start); + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_word_col + start, highlighted_word_col + lookup_symbol_word.length() + start); for (int j = 0; j < sel.size(); j++) { Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, row_height); if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { @@ -1256,17 +1064,16 @@ void TextEdit::_notification(int p_what) { } else if (rect.position.x + rect.size.x > xmargin_end) { rect.size.x = xmargin_end - rect.position.x; } - rect.position.y = TS->shaped_text_get_ascent(rid) + cache.font->get_underline_position(cache.font_size); - rect.size.y = cache.font->get_underline_thickness(cache.font_size); - draw_rect(rect, cache.font_selected_color); + rect.position.y = TS->shaped_text_get_ascent(rid) + font->get_underline_position(font_size); + rect.size.y = font->get_underline_thickness(font_size); + draw_rect(rect, font_selected_color); } - highlighted_word_col = _get_column_pos_of_word(highlighted_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, highlighted_word_col + 1); + highlighted_word_col = _get_column_pos_of_word(lookup_symbol_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, highlighted_word_col + 1); } } } - const int line_top_offset_y = ofs_y; ofs_y += (row_height - text_height) / 2; const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(rid); @@ -1275,12 +1082,12 @@ void TextEdit::_notification(int p_what) { ofs_y += ldata->get_line_ascent(line_wrap_index); int char_ofs = 0; - if (cache.outline_size > 0 && cache.outline_color.a > 0) { + if (outline_size > 0 && outline_color.a > 0) { for (int j = 0; j < gl_size; j++) { for (int k = 0; k < glyphs[j].repeat; k++) { if ((char_ofs + char_margin) >= xmargin_beg && (char_ofs + glyphs[j].advance + char_margin) <= xmargin_end) { if (glyphs[j].font_rid != RID()) { - TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, cache.outline_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, cache.outline_color); + TS->font_draw_glyph_outline(glyphs[j].font_rid, ci, glyphs[j].font_size, outline_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, outline_color); } } char_ofs += glyphs[j].advance; @@ -1292,10 +1099,11 @@ void TextEdit::_notification(int p_what) { char_ofs = 0; } for (int j = 0; j < gl_size; j++) { - if (color_map.has(glyphs[j].start)) { - current_color = color_map[glyphs[j].start].get("color"); - if (readonly && current_color.a > cache.font_readonly_color.a) { - current_color.a = cache.font_readonly_color.a; + const Variant *color_data = color_map.getptr(glyphs[j].start); + if (color_data != nullptr) { + current_color = (color_data->operator Dictionary()).get("color", font_color); + if (!editable && current_color.a > font_readonly_color.a) { + current_color.a = font_readonly_color.a; } } @@ -1304,39 +1112,39 @@ void TextEdit::_notification(int p_what) { int sel_to = (line < selection.to_line) ? TS->shaped_text_get_range(rid).y : selection.to_column; if (glyphs[j].start >= sel_from && glyphs[j].end <= sel_to && override_selected_font_color) { - current_color = cache.font_selected_color; + current_color = font_selected_color; } } int char_pos = char_ofs + char_margin + ofs_x; if (char_pos >= xmargin_beg) { - if (brace_matching_enabled) { + if (highlight_matching_braces_enabled) { if ((brace_open_match_line == line && brace_open_match_column == glyphs[j].start) || - (cursor.column == glyphs[j].start && cursor.line == line && cursor_wrap_index == line_wrap_index && (brace_open_matching || brace_open_mismatch))) { + (caret.column == glyphs[j].start && caret.line == line && caret_wrap_index == line_wrap_index && (brace_open_matching || brace_open_mismatch))) { if (brace_open_mismatch) { - current_color = cache.brace_mismatch_color; + current_color = brace_mismatch_color; } - Rect2 rect = Rect2(char_pos, ofs_y + cache.font->get_underline_position(cache.font_size), glyphs[j].advance * glyphs[j].repeat, cache.font->get_underline_thickness(cache.font_size)); + Rect2 rect = Rect2(char_pos, ofs_y + font->get_underline_position(font_size), glyphs[j].advance * glyphs[j].repeat, font->get_underline_thickness(font_size)); draw_rect(rect, current_color); } if ((brace_close_match_line == line && brace_close_match_column == glyphs[j].start) || - (cursor.column == glyphs[j].start + 1 && cursor.line == line && cursor_wrap_index == line_wrap_index && (brace_close_matching || brace_close_mismatch))) { + (caret.column == glyphs[j].start + 1 && caret.line == line && caret_wrap_index == line_wrap_index && (brace_close_matching || brace_close_mismatch))) { if (brace_close_mismatch) { - current_color = cache.brace_mismatch_color; + current_color = brace_mismatch_color; } - Rect2 rect = Rect2(char_pos, ofs_y + cache.font->get_underline_position(cache.font_size), glyphs[j].advance * glyphs[j].repeat, cache.font->get_underline_thickness(cache.font_size)); + Rect2 rect = Rect2(char_pos, ofs_y + font->get_underline_position(font_size), glyphs[j].advance * glyphs[j].repeat, font->get_underline_thickness(font_size)); draw_rect(rect, current_color); } } if (draw_tabs && ((glyphs[j].flags & TextServer::GRAPHEME_IS_TAB) == TextServer::GRAPHEME_IS_TAB)) { - int yofs = (text_height - cache.tab_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); - cache.tab_icon->draw(ci, Point2(char_pos, ofs_y + yofs), current_color); + int yofs = (text_height - tab_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); + tab_icon->draw(ci, Point2(char_pos, ofs_y + yofs), current_color); } else if (draw_spaces && ((glyphs[j].flags & TextServer::GRAPHEME_IS_SPACE) == TextServer::GRAPHEME_IS_SPACE)) { - int yofs = (text_height - cache.space_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); - int xofs = (glyphs[j].advance * glyphs[j].repeat - cache.space_icon->get_width()) / 2; - cache.space_icon->draw(ci, Point2(char_pos + xofs, ofs_y + yofs), current_color); + int yofs = (text_height - space_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); + int xofs = (glyphs[j].advance * glyphs[j].repeat - space_icon->get_width()) / 2; + space_icon->draw(ci, Point2(char_pos + xofs, ofs_y + yofs), current_color); } } @@ -1355,13 +1163,14 @@ void TextEdit::_notification(int p_what) { } } - if (line_wrap_index == line_wrap_amount && is_folded(line)) { - int xofs = char_ofs + char_margin + ofs_x + (cache.folded_eol_icon->get_width() / 2); + // is_line_folded + if (line_wrap_index == line_wrap_amount && line < text.size() - 1 && _is_line_hidden(line + 1)) { + int xofs = char_ofs + char_margin + ofs_x + (folded_eol_icon->get_width() / 2); if (xofs >= xmargin_beg && xofs < xmargin_end) { - int yofs = (text_height - cache.folded_eol_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); - Color eol_color = cache.code_folding_color; + int yofs = (text_height - folded_eol_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); + Color eol_color = code_folding_color; eol_color.a = 1; - cache.folded_eol_icon->draw(ci, Point2(xofs, ofs_y + yofs), eol_color); + folded_eol_icon->draw(ci, Point2(xofs, ofs_y + yofs), eol_color); } } @@ -1371,19 +1180,19 @@ void TextEdit::_notification(int p_what) { #else int caret_width = 1; #endif - if (!clipped && cursor.line == line && ((line_wrap_index == line_wrap_amount) || (cursor.column != TS->shaped_text_get_range(rid).y))) { - is_cursor_line_visible = true; - cursor_pos.y = line_top_offset_y; + + if (!clipped && caret.line == line && line_wrap_index == caret_wrap_index) { + caret.draw_pos.y = ofs_y + ldata->get_line_descent(line_wrap_index); if (ime_text.length() == 0) { Rect2 l_caret, t_caret; TextServer::Direction l_dir, t_dir; if (str.length() != 0) { // Get carets. - TS->shaped_text_get_carets(rid, cursor.column, l_caret, l_dir, t_caret, t_dir); + TS->shaped_text_get_carets(rid, caret.column, l_caret, l_dir, t_caret, t_dir); } else { // No carets, add one at the start. - int h = cache.font->get_height(cache.font_size); + int h = font->get_height(font_size); if (rtl) { l_dir = TextServer::DIRECTION_RTL; l_caret = Rect2(Vector2(xmargin_end - char_margin + ofs_x, -h / 2), Size2(caret_width * 4, h)); @@ -1394,63 +1203,83 @@ void TextEdit::_notification(int p_what) { } if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { - cursor_pos.x = char_margin + ofs_x + l_caret.position.x; + caret.draw_pos.x = char_margin + ofs_x + l_caret.position.x; } else { - cursor_pos.x = char_margin + ofs_x + t_caret.position.x; + caret.draw_pos.x = char_margin + ofs_x + t_caret.position.x; } - if (draw_caret && cursor_pos.x >= xmargin_beg && cursor_pos.x < xmargin_end) { - if (block_caret || insert_mode) { - //Block or underline caret, draw trailing carets at full height. - int h = cache.font->get_height(cache.font_size); - - if (t_caret != Rect2()) { - if (insert_mode) { - t_caret.position.y = TS->shaped_text_get_descent(rid); - t_caret.size.y = caret_width; - } else { - t_caret.position.y = -TS->shaped_text_get_ascent(rid); - t_caret.size.y = h; - } - t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + if (caret.draw_pos.x >= xmargin_beg && caret.draw_pos.x < xmargin_end) { + caret.visible = true; + if (draw_caret) { + if (caret_type == CaretType::CARET_TYPE_BLOCK || overtype_mode) { + //Block or underline caret, draw trailing carets at full height. + int h = font->get_height(font_size); + + if (t_caret != Rect2()) { + if (overtype_mode) { + t_caret.position.y = TS->shaped_text_get_descent(rid); + t_caret.size.y = caret_width; + } else { + t_caret.position.y = -TS->shaped_text_get_ascent(rid); + t_caret.size.y = h; + } + t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + draw_rect(t_caret, caret_color, overtype_mode); - draw_rect(t_caret, cache.caret_color, false); - } else { // End of the line. - if (insert_mode) { - l_caret.position.y = TS->shaped_text_get_descent(rid); - l_caret.size.y = caret_width; - } else { - l_caret.position.y = -TS->shaped_text_get_ascent(rid); - l_caret.size.y = h; + if (l_caret != Rect2() && l_dir != t_dir) { + l_caret.position += Vector2(char_margin + ofs_x, ofs_y); + l_caret.size.x = caret_width; + draw_rect(l_caret, caret_color * Color(1, 1, 1, 0.5)); + } + } else { // End of the line. + if (gl_size > 0) { + // Adjust for actual line dimensions. + if (overtype_mode) { + l_caret.position.y = TS->shaped_text_get_descent(rid); + l_caret.size.y = caret_width; + } else { + l_caret.position.y = -TS->shaped_text_get_ascent(rid); + l_caret.size.y = h; + } + } else if (overtype_mode) { + l_caret.position.y += l_caret.size.y; + l_caret.size.y = caret_width; + } + if (l_caret.position.x >= TS->shaped_text_get_size(rid).x) { + l_caret.size.x = font->get_char_size('m', 0, font_size).x; + } else { + l_caret.size.x = 3 * caret_width; + } + l_caret.position += Vector2(char_margin + ofs_x, ofs_y); + if (l_dir == TextServer::DIRECTION_RTL) { + l_caret.position.x -= l_caret.size.x; + } + draw_rect(l_caret, caret_color, overtype_mode); + } + } else { + // Normal caret. + if (l_caret != Rect2() && l_dir == TextServer::DIRECTION_AUTO) { + // Draw extra marker on top of mid caret. + Rect2 trect = Rect2(l_caret.position.x - 3 * caret_width, l_caret.position.y, 6 * caret_width, caret_width); + trect.position += Vector2(char_margin + ofs_x, ofs_y); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, caret_color); } l_caret.position += Vector2(char_margin + ofs_x, ofs_y); - l_caret.size.x = cache.font->get_char_size('M', 0, cache.font_size).x; + l_caret.size.x = caret_width; - draw_rect(l_caret, cache.caret_color, false); - } - } else { - // Normal caret. - if (l_caret != Rect2() && l_dir == TextServer::DIRECTION_AUTO) { - // Draw extra marker on top of mid caret. - Rect2 trect = Rect2(l_caret.position.x - 3 * caret_width, l_caret.position.y, 6 * caret_width, caret_width); - trect.position += Vector2(char_margin + ofs_x, ofs_y); - RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, cache.caret_color); - } - l_caret.position += Vector2(char_margin + ofs_x, ofs_y); - l_caret.size.x = caret_width; + draw_rect(l_caret, caret_color); - draw_rect(l_caret, cache.caret_color); - - t_caret.position += Vector2(char_margin + ofs_x, ofs_y); - t_caret.size.x = caret_width; + t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + t_caret.size.x = caret_width; - draw_rect(t_caret, cache.caret_color); + draw_rect(t_caret, caret_color); + } } } } else { { // IME Intermediate text range. - Vector<Vector2> sel = TS->shaped_text_get_selection(rid, cursor.column, cursor.column + ime_text.length()); + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, caret.column, caret.column + ime_text.length()); for (int j = 0; j < sel.size(); j++) { Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height); if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { @@ -1463,13 +1292,13 @@ void TextEdit::_notification(int p_what) { rect.size.x = xmargin_end - rect.position.x; } rect.size.y = caret_width; - draw_rect(rect, cache.caret_color); - cursor_pos.x = rect.position.x; + draw_rect(rect, caret_color); + caret.draw_pos.x = rect.position.x; } } { // IME caret. - Vector<Vector2> sel = TS->shaped_text_get_selection(rid, cursor.column + ime_selection.x, cursor.column + ime_selection.x + ime_selection.y); + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, caret.column + ime_selection.x, caret.column + ime_selection.x + ime_selection.y); for (int j = 0; j < sel.size(); j++) { Rect2 rect = Rect2(sel[j].x + char_margin + ofs_x, ofs_y, sel[j].y - sel[j].x, text_height); if (rect.position.x + rect.size.x <= xmargin_beg || rect.position.x > xmargin_end) { @@ -1482,8 +1311,8 @@ void TextEdit::_notification(int p_what) { rect.size.x = xmargin_end - rect.position.x; } rect.size.y = caret_width * 3; - draw_rect(rect, cache.caret_color); - cursor_pos.x = rect.position.x; + draw_rect(rect, caret_color); + caret.draw_pos.x = rect.position.x; } } } @@ -1491,227 +1320,10 @@ void TextEdit::_notification(int p_what) { } } - bool completion_below = false; - if (completion_active && is_cursor_line_visible && completion_options.size() > 0) { - // Completion panel - - const Ref<StyleBox> csb = get_theme_stylebox("completion"); - const int maxlines = get_theme_constant("completion_lines"); - const int cmax_width = get_theme_constant("completion_max_width") * cache.font->get_char_size('x', 0, cache.font_size).x; - const Color scrollc = get_theme_color("completion_scroll_color"); - - const int completion_options_size = completion_options.size(); - const int row_count = MIN(completion_options_size, maxlines); - const int completion_rows_height = row_count * row_height; - const int completion_base_width = cache.font->get_string_size(completion_base, cache.font_size).width; - - int scroll_rectangle_width = get_theme_constant("completion_scroll_width"); - int width = 0; - - // Compute max width of the panel based on the longest completion option - if (completion_options_size < 50) { - for (int i = 0; i < completion_options_size; i++) { - int line_width = MIN(cache.font->get_string_size(completion_options[i].display, cache.font_size).x, cmax_width); - if (line_width > width) { - width = line_width; - } - } - } else { - width = cmax_width; - } - - // Add space for completion icons. - const int icon_hsep = get_theme_constant("hseparation", "ItemList"); - const Size2 icon_area_size(row_height, row_height); - const int icon_area_width = icon_area_size.width + icon_hsep; - width += icon_area_width; - - const int line_from = CLAMP(completion_index - row_count / 2, 0, completion_options_size - row_count); - - for (int i = 0; i < row_count; i++) { - int l = line_from + i; - ERR_CONTINUE(l < 0 || l >= completion_options_size); - if (completion_options[l].default_value.get_type() == Variant::COLOR) { - width += icon_area_size.width; - break; - } - } - - // Position completion panel - completion_rect.size.width = width + 2; - completion_rect.size.height = completion_rows_height; - - if (completion_options_size <= maxlines) { - scroll_rectangle_width = 0; - } - - const Point2 csb_offset = csb->get_offset(); - - const int total_width = completion_rect.size.width + csb->get_minimum_size().x + scroll_rectangle_width; - const int total_height = completion_rect.size.height + csb->get_minimum_size().y; - - const int rect_left_border_x = cursor_pos.x - completion_base_width - icon_area_width - csb_offset.x; - const int rect_right_border_x = rect_left_border_x + total_width; - - if (rect_left_border_x < 0) { - // Anchor the completion panel to the left - completion_rect.position.x = 0; - } else if (rect_right_border_x > get_size().width) { - // Anchor the completion panel to the right - completion_rect.position.x = get_size().width - total_width; - } else { - // Let the completion panel float with the cursor - completion_rect.position.x = rect_left_border_x; - } - - if (cursor_pos.y + row_height + total_height > get_size().height && cursor_pos.y > total_height) { - // Completion panel above the cursor line - completion_rect.position.y = cursor_pos.y - total_height; - } else { - // Completion panel below the cursor line - completion_rect.position.y = cursor_pos.y + row_height; - completion_below = true; - } - - draw_style_box(csb, Rect2(completion_rect.position - csb_offset, completion_rect.size + csb->get_minimum_size() + Size2(scroll_rectangle_width, 0))); - - if (cache.completion_background_color.a > 0.01) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(completion_rect.position, completion_rect.size + Size2(scroll_rectangle_width, 0)), cache.completion_background_color); - } - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(completion_rect.position.x, completion_rect.position.y + (completion_index - line_from) * get_row_height()), Size2(completion_rect.size.width, get_row_height())), cache.completion_selected_color); - - draw_rect(Rect2(completion_rect.position + Vector2(icon_area_size.x + icon_hsep, 0), Size2(MIN(completion_base_width, completion_rect.size.width - (icon_area_size.x + icon_hsep)), completion_rect.size.height)), cache.completion_existing_color); - - for (int i = 0; i < row_count; i++) { - int l = line_from + i; - ERR_CONTINUE(l < 0 || l >= completion_options_size); - - Ref<TextLine> tl; - tl.instance(); - tl->add_string(completion_options[l].display, cache.font, cache.font_size); - - int yofs = (row_height - tl->get_size().y) / 2; - Point2 title_pos(completion_rect.position.x, completion_rect.position.y + i * row_height + yofs); - - // Draw completion icon if it is valid. - Ref<Texture2D> icon = completion_options[l].icon; - Rect2 icon_area(completion_rect.position.x, completion_rect.position.y + i * row_height, icon_area_size.width, icon_area_size.height); - if (icon.is_valid()) { - const real_t max_scale = 0.7f; - const real_t side = max_scale * icon_area.size.width; - real_t scale = MIN(side / icon->get_width(), side / icon->get_height()); - Size2 icon_size = icon->get_size() * scale; - draw_texture_rect(icon, Rect2(icon_area.position + (icon_area.size - icon_size) / 2, icon_size)); - } - - title_pos.x = icon_area.position.x + icon_area.size.width + icon_hsep; - - tl->set_width(completion_rect.size.width - (icon_area_size.x + icon_hsep)); - - if (rtl) { - if (completion_options[l].default_value.get_type() == Variant::COLOR) { - draw_rect(Rect2(Point2(completion_rect.position.x, icon_area.position.y), icon_area_size), (Color)completion_options[l].default_value); - } - tl->set_align(HALIGN_RIGHT); - } else { - if (completion_options[l].default_value.get_type() == Variant::COLOR) { - draw_rect(Rect2(Point2(completion_rect.position.x + completion_rect.size.width - icon_area_size.x, icon_area.position.y), icon_area_size), (Color)completion_options[l].default_value); - } - tl->set_align(HALIGN_LEFT); - } - if (cache.outline_size > 0 && cache.outline_color.a > 0) { - tl->draw_outline(ci, title_pos, cache.outline_size, cache.outline_color); - } - tl->draw(ci, title_pos, completion_options[l].font_color); - } - - if (scroll_rectangle_width) { - // Draw a small scroll rectangle to show a position in the options. - float r = (float)maxlines / completion_options_size; - float o = (float)line_from / completion_options_size; - draw_rect(Rect2(completion_rect.position.x + completion_rect.size.width, completion_rect.position.y + o * completion_rect.size.y, scroll_rectangle_width, completion_rect.size.y * r), scrollc); - } - - completion_line_ofs = line_from; - } - - // Check to see if the hint should be drawn. - bool show_hint = false; - if (is_cursor_line_visible && completion_hint != "") { - if (completion_active) { - if (completion_below && !callhint_below) { - show_hint = true; - } else if (!completion_below && callhint_below) { - show_hint = true; - } - } else { - show_hint = true; - } - } - - if (show_hint) { - Ref<StyleBox> sb = get_theme_stylebox("panel", "TooltipPanel"); - Ref<Font> font = cache.font; - Color font_color = get_theme_color("font_color", "TooltipLabel"); - - int max_w = 0; - int sc = completion_hint.get_slice_count("\n"); - int offset = 0; - int spacing = 0; - for (int i = 0; i < sc; i++) { - String l = completion_hint.get_slice("\n", i); - int len = font->get_string_size(l, cache.font_size).x; - max_w = MAX(len, max_w); - if (i == 0) { - offset = font->get_string_size(l.substr(0, l.find(String::chr(0xFFFF))), cache.font_size).x; - } else { - spacing += cache.line_spacing; - } - } - - Size2 size2 = Size2(max_w, sc * font->get_height(cache.font_size) + spacing); - Size2 minsize = size2 + sb->get_minimum_size(); - - if (completion_hint_offset == -0xFFFF) { - completion_hint_offset = cursor_pos.x - offset; - } - - Point2 hint_ofs = Vector2(completion_hint_offset, cursor_pos.y) + callhint_offset; - - if (callhint_below) { - hint_ofs.y += row_height + sb->get_offset().y; - } else { - hint_ofs.y -= minsize.y + sb->get_offset().y; - } - - draw_style_box(sb, Rect2(hint_ofs, minsize)); - - spacing = 0; - for (int i = 0; i < sc; i++) { - int begin = 0; - int end = 0; - String l = completion_hint.get_slice("\n", i); - - if (l.find(String::chr(0xFFFF)) != -1) { - begin = font->get_string_size(l.substr(0, l.find(String::chr(0xFFFF))), cache.font_size).x; - end = font->get_string_size(l.substr(0, l.rfind(String::chr(0xFFFF))), cache.font_size).x; - } - - Point2 round_ofs = hint_ofs + sb->get_offset() + Vector2(0, font->get_ascent(cache.font_size) + font->get_height(cache.font_size) * i + spacing); - round_ofs = round_ofs.round(); - draw_string(font, round_ofs, l.replace(String::chr(0xFFFF), ""), HALIGN_LEFT, -1, cache.font_size, font_color); - if (end > 0) { - Vector2 b = hint_ofs + sb->get_offset() + Vector2(begin, font->get_height(cache.font_size) + font->get_height(cache.font_size) * i + spacing - 1); - draw_line(b, b + Vector2(end - begin, 0), font_color); - } - spacing += cache.line_spacing; - } - } - if (has_focus()) { if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + cursor_pos, get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + caret.draw_pos, get_viewport()->get_window_id()); } } } break; @@ -1724,26 +1336,26 @@ void TextEdit::_notification(int p_what) { if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_active(true, get_viewport()->get_window_id()); - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + _get_cursor_pixel_pos(false), get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + get_caret_draw_pos(), get_viewport()->get_window_id()); } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - int cursor_start = -1; - int cursor_end = -1; + int caret_start = -1; + int caret_end = -1; if (!selection.active) { - String full_text = _base_get_text(0, 0, cursor.line, cursor.column); + String full_text = _base_get_text(0, 0, caret.line, caret.column); - cursor_start = full_text.length(); + caret_start = full_text.length(); } else { String pre_text = _base_get_text(0, 0, selection.from_line, selection.from_column); - String post_text = _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); + String post_text = get_selected_text(); - cursor_start = pre_text.length(); - cursor_end = cursor_start + post_text.length(); + caret_start = pre_text.length(); + caret_end = caret_start + post_text.length(); } - DisplayServer::get_singleton()->virtual_keyboard_show(get_text(), get_global_rect(), true, -1, cursor_start, cursor_end); + DisplayServer::get_singleton()->virtual_keyboard_show(get_text(), get_global_rect(), true, -1, caret_start, caret_end); } } break; case NOTIFICATION_FOCUS_EXIT: { @@ -1757,7 +1369,7 @@ void TextEdit::_notification(int p_what) { } ime_text = ""; ime_selection = Point2(); - text.invalidate_cache(cursor.line, cursor.column, ime_text); + text.invalidate_cache(caret.line, caret.column, ime_text); if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); @@ -1769,1106 +1381,20 @@ void TextEdit::_notification(int p_what) { ime_selection = DisplayServer::get_singleton()->ime_get_selection(); String t; - if (cursor.column >= 0) { - t = text[cursor.line].substr(0, cursor.column) + ime_text + text[cursor.line].substr(cursor.column, text[cursor.line].length()); + if (caret.column >= 0) { + t = text[caret.line].substr(0, caret.column) + ime_text + text[caret.line].substr(caret.column, text[caret.line].length()); } else { t = ime_text; } - text.invalidate_cache(cursor.line, cursor.column, t, structured_text_parser(st_parser, st_args, t)); + text.invalidate_cache(caret.line, caret.column, t, structured_text_parser(st_parser, st_args, t)); update(); } } break; } } -void TextEdit::_consume_pair_symbol(char32_t ch) { - int cursor_position_to_move = cursor_get_column() + 1; - - char32_t ch_single[2] = { ch, 0 }; - char32_t ch_single_pair[2] = { _get_right_pair_symbol(ch), 0 }; - char32_t ch_pair[3] = { ch, _get_right_pair_symbol(ch), 0 }; - - if (is_selection_active()) { - int new_column, new_line; - - begin_complex_operation(); - _insert_text(get_selection_from_line(), get_selection_from_column(), - ch_single, - &new_line, &new_column); - - int to_col_offset = 0; - if (get_selection_from_line() == get_selection_to_line()) { - to_col_offset = 1; - } - - _insert_text(get_selection_to_line(), - get_selection_to_column() + to_col_offset, - ch_single_pair, - &new_line, &new_column); - end_complex_operation(); - - cursor_set_line(get_selection_to_line()); - cursor_set_column(get_selection_to_column() + to_col_offset); - - deselect(); - update(); - return; - } - - if ((ch == '\'' || ch == '"') && - cursor_get_column() > 0 && _is_text_char(text[cursor.line][cursor_get_column() - 1]) && !_is_pair_right_symbol(text[cursor.line][cursor_get_column()])) { - insert_text_at_cursor(ch_single); - cursor_set_column(cursor_position_to_move); - return; - } - - if (cursor_get_column() < text[cursor.line].length()) { - if (_is_text_char(text[cursor.line][cursor_get_column()])) { - insert_text_at_cursor(ch_single); - cursor_set_column(cursor_position_to_move); - return; - } - if (_is_pair_right_symbol(ch) && - text[cursor.line][cursor_get_column()] == ch) { - cursor_set_column(cursor_position_to_move); - return; - } - } - - String line = text[cursor.line]; - - bool in_single_quote = false; - bool in_double_quote = false; - bool found_comment = false; - - int c = 0; - while (c < line.length()) { - if (line[c] == '\\') { - c++; // Skip quoted anything. - - if (cursor.column == c) { - break; - } - } else if (!in_single_quote && !in_double_quote && line[c] == '#') { - found_comment = true; - break; - } else { - if (line[c] == '\'' && !in_double_quote) { - in_single_quote = !in_single_quote; - } else if (line[c] == '"' && !in_single_quote) { - in_double_quote = !in_double_quote; - } - } - - c++; - - if (cursor.column == c) { - break; - } - } - - // Do not need to duplicate quotes while in comments - if (found_comment) { - insert_text_at_cursor(ch_single); - cursor_set_column(cursor_position_to_move); - - return; - } - - // Disallow inserting duplicated quotes while already in string - if ((in_single_quote || in_double_quote) && (ch == '"' || ch == '\'')) { - insert_text_at_cursor(ch_single); - cursor_set_column(cursor_position_to_move); - - return; - } - - insert_text_at_cursor(ch_pair); - cursor_set_column(cursor_position_to_move); -} - -void TextEdit::_consume_backspace_for_pair_symbol(int prev_line, int prev_column) { - bool remove_right_symbol = false; - - if (cursor.column < text[cursor.line].length() && cursor.column > 0) { - char32_t left_char = text[cursor.line][cursor.column - 1]; - char32_t right_char = text[cursor.line][cursor.column]; - - if (right_char == _get_right_pair_symbol(left_char)) { - remove_right_symbol = true; - } - } - if (remove_right_symbol) { - _remove_text(prev_line, prev_column, cursor.line, cursor.column + 1); - } else { - _remove_text(prev_line, prev_column, cursor.line, cursor.column); - } -} - -void TextEdit::backspace_at_cursor() { - if (readonly) { - return; - } - - if (cursor.column == 0 && cursor.line == 0) { - return; - } - - int prev_line = cursor.column ? cursor.line : cursor.line - 1; - int prev_column = cursor.column ? (cursor.column - 1) : (text[cursor.line - 1].length()); - - if (cursor.line != prev_line) { - for (int i = 0; i < gutters.size(); i++) { - if (!gutters[i].overwritable) { - continue; - } - - if (text.get_line_gutter_text(cursor.line, i) != "") { - text.set_line_gutter_text(prev_line, i, text.get_line_gutter_text(cursor.line, i)); - text.set_line_gutter_item_color(prev_line, i, text.get_line_gutter_item_color(cursor.line, i)); - } - - if (text.get_line_gutter_icon(cursor.line, i).is_valid()) { - text.set_line_gutter_icon(prev_line, i, text.get_line_gutter_icon(cursor.line, i)); - text.set_line_gutter_item_color(prev_line, i, text.get_line_gutter_item_color(cursor.line, i)); - } - - if (text.get_line_gutter_metadata(cursor.line, i) != "") { - text.set_line_gutter_metadata(prev_line, i, text.get_line_gutter_metadata(cursor.line, i)); - } - - if (text.is_line_gutter_clickable(cursor.line, i)) { - text.set_line_gutter_clickable(prev_line, i, true); - } - } - } - - if (is_line_hidden(cursor.line)) { - set_line_as_hidden(prev_line, true); - } - - if (auto_brace_completion_enabled && - cursor.column > 0 && - _is_pair_left_symbol(text[cursor.line][cursor.column - 1])) { - _consume_backspace_for_pair_symbol(prev_line, prev_column); - } else { - // Handle space indentation. - if (cursor.column != 0 && indent_using_spaces) { - // Check if there are no other chars before cursor, just indentation. - bool unindent = true; - int i = 0; - while (i < cursor.column && i < text[cursor.line].length()) { - if (!_is_whitespace(text[cursor.line][i])) { - unindent = false; - break; - } - i++; - } - - // Then we can remove all spaces as a single character. - if (unindent) { - // We want to remove spaces up to closest indent, or whole indent if cursor is pointing at it. - int spaces_to_delete = _calculate_spaces_till_next_left_indent(cursor.column); - prev_column = cursor.column - spaces_to_delete; - _remove_text(cursor.line, prev_column, cursor.line, cursor.column); - } else { - _remove_text(prev_line, prev_column, cursor.line, cursor.column); - } - } else { - _remove_text(prev_line, prev_column, cursor.line, cursor.column); - } - } - - cursor_set_line(prev_line, false, true); - cursor_set_column(prev_column); -} - -void TextEdit::indent_selected_lines_right() { - int start_line; - int end_line; - - // This value informs us by how much we changed selection position by indenting right. - // Default is 1 for tab indentation. - int selection_offset = 1; - begin_complex_operation(); - - if (is_selection_active()) { - start_line = get_selection_from_line(); - end_line = get_selection_to_line(); - } else { - start_line = cursor.line; - end_line = start_line; - } - - // Ignore if the cursor is not past the first column. - if (is_selection_active() && get_selection_to_column() == 0) { - selection_offset = 0; - end_line--; - } - - for (int i = start_line; i <= end_line; i++) { - String line_text = get_line(i); - if (line_text.size() == 0 && is_selection_active()) { - continue; - } - if (indent_using_spaces) { - // We don't really care where selection is - we just need to know indentation level at the beginning of the line. - int left = _find_first_non_whitespace_column_of_line(line_text); - int spaces_to_add = _calculate_spaces_till_next_right_indent(left); - // Since we will add these many spaces, we want to move the whole selection and cursor by this much. - selection_offset = spaces_to_add; - for (int j = 0; j < spaces_to_add; j++) { - line_text = ' ' + line_text; - } - } else { - line_text = '\t' + line_text; - } - set_line(i, line_text); - } - - // Fix selection and cursor being off after shifting selection right. - if (is_selection_active()) { - select(selection.from_line, selection.from_column + selection_offset, selection.to_line, selection.to_column + selection_offset); - } - cursor_set_column(cursor.column + selection_offset, false); - end_complex_operation(); - update(); -} - -void TextEdit::indent_selected_lines_left() { - int start_line; - int end_line; - - // Moving cursor and selection after unindenting can get tricky because - // changing content of line can move cursor and selection on its own (if new line ends before previous position of either), - // therefore we just remember initial values and at the end of the operation offset them by number of removed characters. - int removed_characters = 0; - int initial_selection_end_column = selection.to_column; - int initial_cursor_column = cursor.column; - - begin_complex_operation(); - - if (is_selection_active()) { - start_line = get_selection_from_line(); - end_line = get_selection_to_line(); - } else { - start_line = cursor.line; - end_line = start_line; - } - - // Ignore if the cursor is not past the first column. - if (is_selection_active() && get_selection_to_column() == 0) { - end_line--; - } - String first_line_text = get_line(start_line); - String last_line_text = get_line(end_line); - - for (int i = start_line; i <= end_line; i++) { - String line_text = get_line(i); - - if (line_text.begins_with("\t")) { - line_text = line_text.substr(1, line_text.length()); - set_line(i, line_text); - removed_characters = 1; - } else if (line_text.begins_with(" ")) { - // When unindenting we aim to remove spaces before line that has selection no matter what is selected, - // so we start of by finding first non whitespace character of line - int left = _find_first_non_whitespace_column_of_line(line_text); - - // Here we remove only enough spaces to align text to nearest full multiple of indentation_size. - // In case where selection begins at the start of indentation_size multiple we remove whole indentation level. - int spaces_to_remove = _calculate_spaces_till_next_left_indent(left); - - line_text = line_text.substr(spaces_to_remove, line_text.length()); - set_line(i, line_text); - removed_characters = spaces_to_remove; - } - } - - if (is_selection_active()) { - // Fix selection being off by one on the first line. - if (first_line_text != get_line(start_line)) { - select(selection.from_line, selection.from_column - removed_characters, - selection.to_line, initial_selection_end_column); - } - // Fix selection being off by one on the last line. - if (last_line_text != get_line(end_line)) { - select(selection.from_line, selection.from_column, - selection.to_line, initial_selection_end_column - removed_characters); - } - } - cursor_set_column(initial_cursor_column - removed_characters, false); - end_complex_operation(); - update(); -} - -int TextEdit::_calculate_spaces_till_next_left_indent(int column) { - int spaces_till_indent = column % indent_size; - if (spaces_till_indent == 0) { - spaces_till_indent = indent_size; - } - return spaces_till_indent; -} - -int TextEdit::_calculate_spaces_till_next_right_indent(int column) { - return indent_size - column % indent_size; -} - -void TextEdit::_swap_current_input_direction() { - if (input_direction == TEXT_DIRECTION_LTR) { - input_direction = TEXT_DIRECTION_RTL; - } else { - input_direction = TEXT_DIRECTION_LTR; - } - cursor_set_column(cursor.column); - update(); -} - -void TextEdit::_new_line(bool p_split_current_line, bool p_above) { - if (readonly) { - return; - } - - String ins = "\n"; - - // Keep indentation. - int space_count = 0; - for (int i = 0; i < cursor.column; i++) { - if (text[cursor.line][i] == '\t') { - if (indent_using_spaces) { - ins += space_indent; - } else { - ins += "\t"; - } - space_count = 0; - } else if (text[cursor.line][i] == ' ') { - space_count++; - - if (space_count == indent_size) { - if (indent_using_spaces) { - ins += space_indent; - } else { - ins += "\t"; - } - space_count = 0; - } - } else { - break; - } - } - - if (is_folded(cursor.line)) { - unfold_line(cursor.line); - } - - bool brace_indent = false; - - // No need to indent if we are going upwards. - if (auto_indent && !p_above) { - // Indent once again if previous line will end with ':','{','[','(' and the line is not a comment - // (i.e. colon/brace precedes current cursor position). - if (cursor.column > 0) { - bool indent_char_found = false; - bool should_indent = false; - char indent_char = ':'; - char c = text[cursor.line][cursor.column]; - - for (int i = 0; i < cursor.column; i++) { - c = text[cursor.line][i]; - switch (c) { - case ':': - case '{': - case '[': - case '(': - indent_char_found = true; - should_indent = true; - indent_char = c; - continue; - } - - if (indent_char_found && is_line_comment(cursor.line)) { - should_indent = true; - break; - } else if (indent_char_found && !_is_whitespace(c)) { - should_indent = false; - indent_char_found = false; - } - } - - if (!is_line_comment(cursor.line) && should_indent) { - if (indent_using_spaces) { - ins += space_indent; - } else { - ins += "\t"; - } - - // No need to move the brace below if we are not taking the text with us. - char32_t closing_char = _get_right_pair_symbol(indent_char); - if ((closing_char != 0) && (closing_char == text[cursor.line][cursor.column])) { - if (p_split_current_line) { - brace_indent = true; - ins += "\n" + ins.substr(1, ins.length() - 2); - } else { - brace_indent = false; - ins = "\n" + ins.substr(1, ins.length() - 2); - } - } - } - } - } - begin_complex_operation(); - bool first_line = false; - if (!p_split_current_line) { - if (p_above) { - if (cursor.line > 0) { - cursor_set_line(cursor.line - 1, false); - cursor_set_column(text[cursor.line].length()); - } else { - cursor_set_column(0); - first_line = true; - } - } else { - cursor_set_column(text[cursor.line].length()); - } - } - - insert_text_at_cursor(ins); - - if (first_line) { - cursor_set_line(0); - } else if (brace_indent) { - cursor_set_line(cursor.line - 1, false); - cursor_set_column(text[cursor.line].length()); - } - end_complex_operation(); -} - -void TextEdit::_indent_right() { - if (readonly) { - return; - } - - if (is_selection_active()) { - indent_selected_lines_right(); - } else { - // Simple indent. - if (indent_using_spaces) { - // Insert only as much spaces as needed till next indentation level. - int spaces_to_add = _calculate_spaces_till_next_right_indent(cursor.column); - String indent_to_insert = String(); - for (int i = 0; i < spaces_to_add; i++) { - indent_to_insert = ' ' + indent_to_insert; - } - _insert_text_at_cursor(indent_to_insert); - } else { - _insert_text_at_cursor("\t"); - } - } -} - -void TextEdit::_indent_left() { - if (readonly) { - return; - } - - if (is_selection_active()) { - indent_selected_lines_left(); - } else { - // Simple unindent. - int cc = cursor.column; - const String &line = text[cursor.line]; - - int left = _find_first_non_whitespace_column_of_line(line); - cc = MIN(cc, left); - - while (cc < indent_size && cc < left && line[cc] == ' ') { - cc++; - } - - if (cc > 0 && cc <= text[cursor.line].length()) { - if (text[cursor.line][cc - 1] == '\t') { - // Tabs unindentation. - _remove_text(cursor.line, cc - 1, cursor.line, cc); - if (cursor.column >= left) { - cursor_set_column(MAX(0, cursor.column - 1)); - } - update(); - } else { - // Spaces unindentation. - int spaces_to_remove = _calculate_spaces_till_next_left_indent(cc); - if (spaces_to_remove > 0) { - _remove_text(cursor.line, cc - spaces_to_remove, cursor.line, cc); - if (cursor.column > left - spaces_to_remove) { // Inside text? - cursor_set_column(MAX(0, cursor.column - spaces_to_remove)); - } - update(); - } - } - } else if (cc == 0 && line.length() > 0 && line[0] == '\t') { - _remove_text(cursor.line, 0, cursor.line, 1); - update(); - } - } -} - -void TextEdit::_move_cursor_left(bool p_select, bool p_move_by_word) { - // Handle selection - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - if (p_move_by_word) { - int cc = cursor.column; - - if (cc == 0 && cursor.line > 0) { - cursor_set_line(cursor.line - 1); - cursor_set_column(text[cursor.line].length()); - } else { - Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(cursor.line)->get_rid()); - for (int i = words.size() - 1; i >= 0; i--) { - if (words[i].x < cc) { - cc = words[i].x; - break; - } - } - cursor_set_column(cc); - } - } else { - // If the cursor is at the start of the line, and not on the first line, move it up to the end of the previous line. - if (cursor.column == 0) { - if (cursor.line > 0) { - cursor_set_line(cursor.line - num_lines_from(CLAMP(cursor.line - 1, 0, text.size() - 1), -1)); - cursor_set_column(text[cursor.line].length()); - } - } else { - if (mid_grapheme_caret_enabled) { - cursor_set_column(cursor_get_column() - 1); - } else { - cursor_set_column(TS->shaped_text_prev_grapheme_pos(text.get_line_data(cursor.line)->get_rid(), cursor_get_column())); - } - } - } - - if (p_select) { - _post_shift_selection(); - } -} - -void TextEdit::_move_cursor_right(bool p_select, bool p_move_by_word) { - // Handle selection - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - if (p_move_by_word) { - int cc = cursor.column; - - if (cc == text[cursor.line].length() && cursor.line < text.size() - 1) { - cursor_set_line(cursor.line + 1); - cursor_set_column(0); - } else { - Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(cursor.line)->get_rid()); - for (int i = 0; i < words.size(); i++) { - if (words[i].y > cc) { - cc = words[i].y; - break; - } - } - cursor_set_column(cc); - } - } else { - // If we are at the end of the line, move the caret to the next line down. - if (cursor.column == text[cursor.line].length()) { - if (cursor.line < text.size() - 1) { - cursor_set_line(cursor_get_line() + num_lines_from(CLAMP(cursor.line + 1, 0, text.size() - 1), 1), true, false); - cursor_set_column(0); - } - } else { - if (mid_grapheme_caret_enabled) { - cursor_set_column(cursor_get_column() + 1); - } else { - cursor_set_column(TS->shaped_text_next_grapheme_pos(text.get_line_data(cursor.line)->get_rid(), cursor_get_column())); - } - } - } - - if (p_select) { - _post_shift_selection(); - } -} - -void TextEdit::_move_cursor_up(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - int cur_wrap_index = get_cursor_wrap_index(); - if (cur_wrap_index > 0) { - cursor_set_line(cursor.line, true, false, cur_wrap_index - 1); - } else if (cursor.line == 0) { - cursor_set_column(0); - } else { - int new_line = cursor.line - num_lines_from(cursor.line - 1, -1); - if (line_wraps(new_line)) { - cursor_set_line(new_line, true, false, times_line_wraps(new_line)); - } else { - cursor_set_line(new_line, true, false); - } - } - - if (p_select) { - _post_shift_selection(); - } - - _cancel_code_hint(); -} - -void TextEdit::_move_cursor_down(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - int cur_wrap_index = get_cursor_wrap_index(); - if (cur_wrap_index < times_line_wraps(cursor.line)) { - cursor_set_line(cursor.line, true, false, cur_wrap_index + 1); - } else if (cursor.line == get_last_unhidden_line()) { - cursor_set_column(text[cursor.line].length()); - } else { - int new_line = cursor.line + num_lines_from(CLAMP(cursor.line + 1, 0, text.size() - 1), 1); - cursor_set_line(new_line, true, false, 0); - } - - if (p_select) { - _post_shift_selection(); - } - - _cancel_code_hint(); -} - -void TextEdit::_move_cursor_to_line_start(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - // Move cursor column to start of wrapped row and then to start of text. - Vector<String> rows = get_wrap_rows_text(cursor.line); - int wi = get_cursor_wrap_index(); - int row_start_col = 0; - for (int i = 0; i < wi; i++) { - row_start_col += rows[i].length(); - } - if (cursor.column == row_start_col || wi == 0) { - // Compute whitespace symbols sequence length. - int current_line_whitespace_len = 0; - while (current_line_whitespace_len < text[cursor.line].length()) { - char32_t c = text[cursor.line][current_line_whitespace_len]; - if (c != '\t' && c != ' ') { - break; - } - current_line_whitespace_len++; - } - - if (cursor_get_column() == current_line_whitespace_len) { - cursor_set_column(0); - } else { - cursor_set_column(current_line_whitespace_len); - } - } else { - cursor_set_column(row_start_col); - } - - if (p_select) { - _post_shift_selection(); - } - - _cancel_completion(); - completion_hint = ""; -} - -void TextEdit::_move_cursor_to_line_end(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - // Move cursor column to end of wrapped row and then to end of text. - Vector<String> rows = get_wrap_rows_text(cursor.line); - int wi = get_cursor_wrap_index(); - int row_end_col = -1; - for (int i = 0; i < wi + 1; i++) { - row_end_col += rows[i].length(); - } - if (wi == rows.size() - 1 || cursor.column == row_end_col) { - cursor_set_column(text[cursor.line].length()); - } else { - cursor_set_column(row_end_col); - } - - if (p_select) { - _post_shift_selection(); - } - _cancel_completion(); - completion_hint = ""; -} - -void TextEdit::_move_cursor_page_up(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - int wi; - int n_line = cursor.line - num_lines_from_rows(cursor.line, get_cursor_wrap_index(), -get_visible_rows(), wi) + 1; - cursor_set_line(n_line, true, false, wi); - - if (p_select) { - _post_shift_selection(); - } - - _cancel_completion(); - completion_hint = ""; -} - -void TextEdit::_move_cursor_page_down(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - int wi; - int n_line = cursor.line + num_lines_from_rows(cursor.line, get_cursor_wrap_index(), get_visible_rows(), wi) - 1; - cursor_set_line(n_line, true, false, wi); - - if (p_select) { - _post_shift_selection(); - } - - _cancel_completion(); - completion_hint = ""; -} - -void TextEdit::_backspace(bool p_word, bool p_all_to_left) { - if (readonly) { - return; - } - - if (is_selection_active()) { - _delete_selection(); - return; - } - if (p_all_to_left) { - int cursor_current_column = cursor.column; - cursor.column = 0; - _remove_text(cursor.line, 0, cursor.line, cursor_current_column); - } else if (p_word) { - int line = cursor.line; - int column = cursor.column; - - Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); - for (int i = words.size() - 1; i >= 0; i--) { - if (words[i].x < column) { - column = words[i].x; - break; - } - } - - _remove_text(line, column, cursor.line, cursor.column); - - cursor_set_line(line, false); - cursor_set_column(column); - } else { - // One character. - if (cursor.line > 0 && is_line_hidden(cursor.line - 1)) { - unfold_line(cursor.line - 1); - } - backspace_at_cursor(); - } -} - -void TextEdit::_delete(bool p_word, bool p_all_to_right) { - if (readonly) { - return; - } - - if (is_selection_active()) { - _delete_selection(); - return; - } - int curline_len = text[cursor.line].length(); - - if (cursor.line == text.size() - 1 && cursor.column == curline_len) { - return; // Last line, last column: Nothing to do. - } - - int next_line = cursor.column < curline_len ? cursor.line : cursor.line + 1; - int next_column; - - if (p_all_to_right) { - // Delete everything to right of cursor - next_column = curline_len; - next_line = cursor.line; - } else if (p_word && cursor.column < curline_len - 1) { - // Delete next word to right of cursor - int line = cursor.line; - int column = cursor.column; - - Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); - for (int i = 0; i < words.size(); i++) { - if (words[i].y > column) { - column = words[i].y; - break; - } - } - - next_line = line; - next_column = column; - } else { - // Delete one character - next_column = cursor.column < curline_len ? (cursor.column + 1) : 0; - if (mid_grapheme_caret_enabled) { - next_column = cursor.column < curline_len ? (cursor.column + 1) : 0; - } else { - next_column = cursor.column < curline_len ? TS->shaped_text_next_grapheme_pos(text.get_line_data(cursor.line)->get_rid(), (cursor.column)) : 0; - } - } - - _remove_text(cursor.line, cursor.column, next_line, next_column); - update(); -} - -void TextEdit::_delete_selection() { - if (is_selection_active()) { - selection.active = false; - update(); - _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - cursor_set_line(selection.from_line, false, false); - cursor_set_column(selection.from_column); - update(); - } -} - -void TextEdit::_move_cursor_document_start(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - cursor_set_line(0); - cursor_set_column(0); - - if (p_select) { - _post_shift_selection(); - } -} - -void TextEdit::_move_cursor_document_end(bool p_select) { - if (p_select) { - _pre_shift_selection(); - } else { - deselect(); - } - - cursor_set_line(get_last_unhidden_line(), true, false, 9999); - cursor_set_column(text[cursor.line].length()); - - if (p_select) { - _post_shift_selection(); - } -} - -void TextEdit::_handle_unicode_character(uint32_t unicode, bool p_had_selection, bool p_update_auto_complete) { - if (p_update_auto_complete) { - _reset_caret_blink_timer(); - } - - if (p_had_selection) { - _delete_selection(); - } - - // Remove the old character if in insert mode and no selection. - if (insert_mode && !p_had_selection) { - begin_complex_operation(); - - // Make sure we don't try and remove empty space. - if (cursor.column < get_line(cursor.line).length()) { - _remove_text(cursor.line, cursor.column, cursor.line, cursor.column + 1); - } - } - - const char32_t chr[2] = { (char32_t)unicode, 0 }; - - // Clear completion hint when function closed - if (completion_hint != "" && unicode == ')') { - completion_hint = ""; - } - - if (auto_brace_completion_enabled && _is_pair_symbol(chr[0])) { - _consume_pair_symbol(chr[0]); - } else { - _insert_text_at_cursor(chr); - } - - if ((insert_mode && !p_had_selection) || (selection.active != p_had_selection)) { - end_complex_operation(); - } - - if (p_update_auto_complete) { - _update_completion_candidates(); - } -} - -void TextEdit::_get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) const { - float rows = p_mouse.y; - rows -= cache.style_normal->get_margin(SIDE_TOP); - rows /= get_row_height(); - rows += get_v_scroll_offset(); - int first_vis_line = get_first_visible_line(); - int row = first_vis_line + Math::floor(rows); - int wrap_index = 0; - - if (is_wrap_enabled() || is_hiding_enabled()) { - int f_ofs = num_lines_from_rows(first_vis_line, cursor.wrap_ofs, rows + (1 * SGN(rows)), wrap_index) - 1; - if (rows < 0) { - row = first_vis_line - f_ofs; - } else { - row = first_vis_line + f_ofs; - } - } - - if (row < 0) { - row = 0; - } - - int col = 0; - - if (row >= text.size()) { - row = text.size() - 1; - col = text[row].size(); - } else { - int colx = p_mouse.x - (cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding); - colx += cursor.x_ofs; - col = get_char_pos_for_line(colx, row, wrap_index); - if (is_wrap_enabled() && wrap_index < times_line_wraps(row)) { - // Move back one if we are at the end of the row. - Vector<String> rows2 = get_wrap_rows_text(row); - int row_end_col = 0; - for (int i = 0; i < wrap_index + 1; i++) { - row_end_col += rows2[i].length(); - } - if (col >= row_end_col) { - col -= 1; - } - } - - RID text_rid = text.get_line_data(row)->get_line_rid(wrap_index); - if (is_layout_rtl()) { - colx = TS->shaped_text_get_size(text_rid).x - colx; - } - col = TS->shaped_text_hit_test_position(text_rid, colx); - } - - r_row = row; - r_col = col; -} - -Vector2i TextEdit::_get_cursor_pixel_pos(bool p_adjust_viewport) { - if (p_adjust_viewport) { - adjust_viewport_to_cursor(); - } - int row = 1; - for (int i = get_first_visible_line(); i < cursor.line; i++) { - if (!is_line_hidden(i)) { - row += times_line_wraps(i) + 1; - } - } - row += cursor.wrap_ofs; - - // Calculate final pixel position - int y = (row - get_v_scroll_offset()) * get_row_height(); - int x = cache.style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding - cursor.x_ofs; - - Rect2 l_caret, t_caret; - TextServer::Direction l_dir, t_dir; - RID text_rid = text.get_line_data(cursor.line)->get_line_rid(cursor.wrap_ofs); - TS->shaped_text_get_carets(text_rid, cursor.column, l_caret, l_dir, t_caret, t_dir); - if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { - x += l_caret.position.x; - } else { - x += t_caret.position.x; - } - - return Vector2i(x, y); -} - -void TextEdit::_get_minimap_mouse_row(const Point2i &p_mouse, int &r_row) const { - float rows = p_mouse.y; - rows -= cache.style_normal->get_margin(SIDE_TOP); - rows /= (minimap_char_size.y + minimap_line_spacing); - rows += get_v_scroll_offset(); - - // calculate visible lines - int minimap_visible_lines = _get_minimap_visible_rows(); - int visible_rows = get_visible_rows() + 1; - int first_visible_line = get_first_visible_line() - 1; - int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0); - draw_amount += times_line_wraps(first_visible_line + 1); - int minimap_line_height = (minimap_char_size.y + minimap_line_spacing); - - // calculate viewport size and y offset - int viewport_height = (draw_amount - 1) * minimap_line_height; - int control_height = _get_control_height() - viewport_height; - int viewport_offset_y = round(get_scroll_pos_for_line(first_visible_line) * control_height) / ((v_scroll->get_max() <= minimap_visible_lines) ? (minimap_visible_lines - draw_amount) : (v_scroll->get_max() - draw_amount)); - - // calculate the first line. - int num_lines_before = round((viewport_offset_y) / minimap_line_height); - int wi; - int minimap_line = (v_scroll->get_max() <= minimap_visible_lines) ? -1 : first_visible_line; - if (first_visible_line > 0 && minimap_line >= 0) { - minimap_line -= num_lines_from_rows(first_visible_line, 0, -num_lines_before, wi); - minimap_line -= (minimap_line > 0 && smooth_scroll_enabled ? 1 : 0); - } else { - minimap_line = 0; - } - - int row = minimap_line + Math::floor(rows); - int wrap_index = 0; - - if (is_wrap_enabled() || is_hiding_enabled()) { - int f_ofs = num_lines_from_rows(minimap_line, cursor.wrap_ofs, rows + (1 * SGN(rows)), wrap_index) - 1; - if (rows < 0) { - row = minimap_line - f_ofs; - } else { - row = minimap_line + f_ofs; - } - } - - if (row < 0) { - row = 0; - } - - if (row >= text.size()) { - row = text.size() - 1; - } - - r_row = row; -} - -void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { +void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { ERR_FAIL_COND(p_gui_input.is_null()); double prev_v_scroll = v_scroll->get_value(); @@ -2885,40 +1411,6 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { // Ignore mouse clicks in IME input mode. return; } - if (completion_active && completion_rect.has_point(mpos)) { - if (!mb->is_pressed()) { - return; - } - - if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP) { - if (completion_index > 0) { - completion_index--; - completion_current = completion_options[completion_index]; - update(); - } - } - if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN) { - if (completion_index < completion_options.size() - 1) { - completion_index++; - completion_current = completion_options[completion_index]; - update(); - } - } - - if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { - completion_index = CLAMP(completion_line_ofs + (mpos.y - completion_rect.position.y) / get_row_height(), 0, completion_options.size() - 1); - - completion_current = completion_options[completion_index]; - update(); - if (mb->is_double_click()) { - _confirm_completion(); - } - } - return; - } else { - _cancel_completion(); - _cancel_code_hint(); - } if (mb->is_pressed()) { if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && !mb->is_command_pressed()) { @@ -2952,32 +1444,24 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { _reset_caret_blink_timer(); - int row, col; - _get_mouse_pos(Point2i(mpos.x, mpos.y), row, col); + Point2i pos = get_line_column_at_pos(Point2i(mpos.x, mpos.y)); + int row = pos.y; + int col = pos.x; - int left_margin = cache.style_normal->get_margin(SIDE_LEFT); + int left_margin = style_normal->get_margin(SIDE_LEFT); for (int i = 0; i < gutters.size(); i++) { if (!gutters[i].draw || gutters[i].width <= 0) { continue; } if (mpos.x > left_margin && mpos.x <= (left_margin + gutters[i].width) - 3) { - emit_signal("gutter_clicked", row, i); + emit_signal(SNAME("gutter_clicked"), row, i); return; } left_margin += gutters[i].width; } - // Unfold on folded icon click. - if (is_folded(row)) { - left_margin += gutter_padding + text.get_line_width(row) - cursor.x_ofs; - if (mpos.x > left_margin && mpos.x <= left_margin + cache.folded_eol_icon->get_width() + 3) { - unfold_line(row); - return; - } - } - // minimap if (draw_minimap) { _update_minimap_click(); @@ -2986,20 +1470,20 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } } - int prev_col = cursor.column; - int prev_line = cursor.line; + int prev_col = caret.column; + int prev_line = caret.line; - cursor_set_line(row, false, false); - cursor_set_column(col); + set_caret_line(row, false, false); + set_caret_column(col); - if (mb->is_shift_pressed() && (cursor.column != prev_col || cursor.line != prev_line)) { + if (mb->is_shift_pressed() && (caret.column != prev_col || caret.line != prev_line)) { if (!selection.active) { selection.active = true; selection.selecting_mode = SelectionMode::SELECTION_MODE_POINTER; selection.from_column = prev_col; selection.from_line = prev_line; - selection.to_column = cursor.column; - selection.to_line = cursor.line; + selection.to_column = caret.column; + selection.to_line = caret.line; if (selection.from_line > selection.to_line || (selection.from_line == selection.to_line && selection.from_column > selection.to_column)) { SWAP(selection.from_column, selection.to_column); @@ -3012,21 +1496,21 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { selection.selecting_column = prev_col; update(); } else { - if (cursor.line < selection.selecting_line || (cursor.line == selection.selecting_line && cursor.column < selection.selecting_column)) { + if (caret.line < selection.selecting_line || (caret.line == selection.selecting_line && caret.column < selection.selecting_column)) { if (selection.shiftclick_left) { selection.shiftclick_left = !selection.shiftclick_left; } - selection.from_column = cursor.column; - selection.from_line = cursor.line; + selection.from_column = caret.column; + selection.from_line = caret.line; - } else if (cursor.line > selection.selecting_line || (cursor.line == selection.selecting_line && cursor.column > selection.selecting_column)) { + } else if (caret.line > selection.selecting_line || (caret.line == selection.selecting_line && caret.column > selection.selecting_column)) { if (!selection.shiftclick_left) { SWAP(selection.from_column, selection.to_column); SWAP(selection.from_line, selection.to_line); selection.shiftclick_left = !selection.shiftclick_left; } - selection.to_column = cursor.column; - selection.to_line = cursor.line; + selection.to_column = caret.column; + selection.to_line = caret.line; } else { selection.active = false; @@ -3041,16 +1525,20 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { selection.selecting_column = col; } - if (!mb->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < 600 && cursor.line == prev_line) { + const int triple_click_timeout = 600; + const int triple_click_tolerance = 5; + + if (!mb->is_double_click() && (OS::get_singleton()->get_ticks_msec() - last_dblclk) < triple_click_timeout && mb->get_position().distance_to(last_dblclk_pos) < triple_click_tolerance) { // Triple-click select line. selection.selecting_mode = SelectionMode::SELECTION_MODE_LINE; _update_selection_mode_line(); last_dblclk = 0; - } else if (mb->is_double_click() && text[cursor.line].length()) { + } else if (mb->is_double_click() && text[caret.line].length()) { // Double-click select word. selection.selecting_mode = SelectionMode::SELECTION_MODE_WORD; _update_selection_mode_word(); last_dblclk = OS::get_singleton()->get_ticks_msec(); + last_dblclk_pos = mb->get_position(); } update(); @@ -3059,11 +1547,12 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { if (mb->get_button_index() == MOUSE_BUTTON_RIGHT && context_menu_enabled) { _reset_caret_blink_timer(); - int row, col; - _get_mouse_pos(Point2i(mpos.x, mpos.y), row, col); + Point2i pos = get_line_column_at_pos(Point2i(mpos.x, mpos.y)); + int row = pos.y; + int col = pos.x; - if (is_right_click_moving_caret()) { - if (is_selection_active()) { + if (is_move_caret_on_right_click_enabled()) { + if (has_selection()) { int from_line = get_selection_from_line(); int to_line = get_selection_to_line(); int from_column = get_selection_from_column(); @@ -3074,28 +1563,20 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { deselect(); } } - if (!is_selection_active()) { - cursor_set_line(row, true, false); - cursor_set_column(col); + if (!has_selection()) { + set_caret_line(row, true, false); + set_caret_column(col); } } + _generate_context_menu(); menu->set_position(get_screen_transform().xform(mpos)); menu->set_size(Vector2(1, 1)); - _generate_context_menu(); menu->popup(); grab_focus(); } } else { if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { - if (mb->is_command_pressed() && highlighted_word != String()) { - int row, col; - _get_mouse_pos(Point2i(mpos.x, mpos.y), row, col); - - emit_signal("symbol_lookup", highlighted_word, row, col); - return; - } - dragging_minimap = false; dragging_selection = false; can_drag_minimap = false; @@ -3130,18 +1611,6 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { if (is_layout_rtl()) { mpos.x = get_size().x - mpos.x; } - if (select_identifiers_enabled) { - if (!dragging_minimap && !dragging_selection && mm->is_command_pressed() && mm->get_button_mask() == 0) { - String new_word = get_word_at_pos(mpos); - if (new_word != highlighted_word) { - emit_signal("symbol_validate", new_word); - } - } else { - if (highlighted_word != String()) { - set_highlighted_word(String()); - } - } - } if (mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. _reset_caret_blink_timer(); @@ -3169,6 +1638,10 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } } + if (draw_minimap && !dragging_selection) { + _update_minimap_hover(); + } + if (v_scroll->get_value() != prev_v_scroll || h_scroll->get_value() != prev_h_scroll) { accept_event(); // Accept event if scroll changed. } @@ -3176,23 +1649,6 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventKey> k = p_gui_input; if (k.is_valid()) { - // Ctrl + Hover symbols -#ifdef OSX_ENABLED - if (k->get_keycode() == KEY_META) { -#else - if (k->get_keycode() == KEY_CTRL) { -#endif - if (select_identifiers_enabled) { - if (k->is_pressed() && !dragging_minimap && !dragging_selection) { - Point2 mp = _get_local_mouse_pos(); - emit_signal("symbol_validate", get_word_at_pos(mp)); - } else { - set_highlighted_word(String()); - } - } - return; - } - if (!k->is_pressed()) { return; } @@ -3208,103 +1664,10 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { // * No Modifiers are pressed (except shift) bool allow_unicode_handling = !(k->is_command_pressed() || k->is_ctrl_pressed() || k->is_alt_pressed() || k->is_meta_pressed()); - // Save here for insert mode, just in case it is cleared in the following section. - bool had_selection = selection.active; - selection.selecting_text = false; // Check and handle all built in shortcuts. - // AUTO-COMPLETE - - if (k->is_action("ui_text_completion_query", true)) { - query_code_comple(); - accept_event(); - return; - } - - if (completion_active) { - if (k->is_action("ui_up", true)) { - if (completion_index > 0) { - completion_index--; - } else { - completion_index = completion_options.size() - 1; - } - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } - if (k->is_action("ui_down", true)) { - if (completion_index < completion_options.size() - 1) { - completion_index++; - } else { - completion_index = 0; - } - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } - if (k->is_action("ui_page_up", true)) { - completion_index -= get_theme_constant("completion_lines"); - if (completion_index < 0) { - completion_index = 0; - } - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } - if (k->is_action("ui_page_down", true)) { - completion_index += get_theme_constant("completion_lines"); - if (completion_index >= completion_options.size()) { - completion_index = completion_options.size() - 1; - } - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } - if (k->is_action("ui_home", true)) { - if (completion_index > 0) { - completion_index = 0; - completion_current = completion_options[completion_index]; - update(); - } - accept_event(); - return; - } - if (k->is_action("ui_end", true)) { - if (completion_index < completion_options.size() - 1) { - completion_index = completion_options.size() - 1; - completion_current = completion_options[completion_index]; - update(); - } - accept_event(); - return; - } - if (k->is_action("ui_text_completion_accept", true)) { - _confirm_completion(); - accept_event(); - return; - } - if (k->is_action("ui_cancel", true)) { - _cancel_completion(); - accept_event(); - return; - } - - // Handle Unicode here (if no modifiers active) and update autocomplete. - if (k->get_unicode() >= 32) { - if (allow_unicode_handling && !readonly) { - _handle_unicode_character(k->get_unicode(), had_selection, true); - accept_event(); - return; - } - } - } - // NEWLINES. if (k->is_action("ui_text_newline_above", true)) { _new_line(false, true); @@ -3322,34 +1685,19 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { return; } - // INDENTATION. - if (k->is_action("ui_text_dedent", true)) { - _indent_left(); - accept_event(); - return; - } - if (k->is_action("ui_text_indent", true)) { - _indent_right(); - accept_event(); - return; - } - // BACKSPACE AND DELETE. if (k->is_action("ui_text_backspace_all_to_left", true)) { - _backspace(false, true); + _do_backspace(false, true); accept_event(); return; } if (k->is_action("ui_text_backspace_word", true)) { - _backspace(true); + _do_backspace(true); accept_event(); return; } if (k->is_action("ui_text_backspace", true)) { - _backspace(); - if (completion_active) { - _update_completion_candidates(); - } + _do_backspace(); accept_event(); return; } @@ -3422,12 +1770,12 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { } // MISC. - if (k->is_action("ui_menu", true)) { if (context_menu_enabled) { - menu->set_position(get_screen_transform().xform(_get_cursor_pixel_pos())); - menu->set_size(Vector2(1, 1)); _generate_context_menu(); + adjust_viewport_to_caret(); + menu->set_position(get_screen_transform().xform(get_caret_draw_pos())); + menu->set_size(Vector2(1, 1)); menu->popup(); menu->grab_focus(); } @@ -3435,15 +1783,7 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { return; } if (k->is_action("ui_text_toggle_insert_mode", true)) { - set_insert_mode(!insert_mode); - accept_event(); - return; - } - if (k->is_action("ui_cancel", true)) { - if (completion_hint != "") { - completion_hint = ""; - update(); - } + set_overtype_mode_enabled(!overtype_mode); accept_event(); return; } @@ -3453,822 +1793,1752 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { return; } - // CURSOR MOVEMENT + // CARET MOVEMENT k = k->duplicate(); bool shift_pressed = k->is_shift_pressed(); // Remove shift or else actions will not match. Use above variable for selection. k->set_shift_pressed(false); - // CURSOR MOVEMENT - LEFT, RIGHT. + // CARET MOVEMENT - LEFT, RIGHT. if (k->is_action("ui_text_caret_word_left", true)) { - _move_cursor_left(shift_pressed, true); + _move_caret_left(shift_pressed, true); accept_event(); return; } if (k->is_action("ui_text_caret_left", true)) { - _move_cursor_left(shift_pressed, false); + _move_caret_left(shift_pressed, false); accept_event(); return; } if (k->is_action("ui_text_caret_word_right", true)) { - _move_cursor_right(shift_pressed, true); + _move_caret_right(shift_pressed, true); accept_event(); return; } if (k->is_action("ui_text_caret_right", true)) { - _move_cursor_right(shift_pressed, false); + _move_caret_right(shift_pressed, false); accept_event(); return; } - // CURSOR MOVEMENT - UP, DOWN. + // CARET MOVEMENT - UP, DOWN. if (k->is_action("ui_text_caret_up", true)) { - _move_cursor_up(shift_pressed); + _move_caret_up(shift_pressed); accept_event(); return; } if (k->is_action("ui_text_caret_down", true)) { - _move_cursor_down(shift_pressed); + _move_caret_down(shift_pressed); accept_event(); return; } - // CURSOR MOVEMENT - DOCUMENT START/END. + // CARET MOVEMENT - DOCUMENT START/END. if (k->is_action("ui_text_caret_document_start", true)) { // && shift_pressed) { - _move_cursor_document_start(shift_pressed); + _move_caret_document_start(shift_pressed); accept_event(); return; } if (k->is_action("ui_text_caret_document_end", true)) { // && shift_pressed) { - _move_cursor_document_end(shift_pressed); + _move_caret_document_end(shift_pressed); accept_event(); return; } - // CURSOR MOVEMENT - LINE START/END. + // CARET MOVEMENT - LINE START/END. if (k->is_action("ui_text_caret_line_start", true)) { - _move_cursor_to_line_start(shift_pressed); + _move_caret_to_line_start(shift_pressed); accept_event(); return; } if (k->is_action("ui_text_caret_line_end", true)) { - _move_cursor_to_line_end(shift_pressed); + _move_caret_to_line_end(shift_pressed); accept_event(); return; } - // CURSOR MOVEMENT - PAGE UP/DOWN. + // CARET MOVEMENT - PAGE UP/DOWN. if (k->is_action("ui_text_caret_page_up", true)) { - _move_cursor_page_up(shift_pressed); + _move_caret_page_up(shift_pressed); accept_event(); return; } if (k->is_action("ui_text_caret_page_down", true)) { - _move_cursor_page_down(shift_pressed); + _move_caret_page_down(shift_pressed); accept_event(); return; } - if (allow_unicode_handling && !readonly && k->get_unicode() >= 32) { - // Handle Unicode (if no modifiers active). - _handle_unicode_character(k->get_unicode(), had_selection, false); + // Handle Unicode (if no modifiers active). Tab has a value of 0x09. + if (allow_unicode_handling && editable && (k->get_unicode() >= 32 || k->get_keycode() == KEY_TAB)) { + handle_unicode_input(k->get_unicode()); accept_event(); return; } } } -void TextEdit::_scroll_up(real_t p_delta) { - if (scrolling && smooth_scroll_enabled && SGN(target_v_scroll - v_scroll->get_value()) != SGN(-p_delta)) { - scrolling = false; - minimap_clicked = false; +/* Input actions. */ +void TextEdit::_swap_current_input_direction() { + if (input_direction == TEXT_DIRECTION_LTR) { + input_direction = TEXT_DIRECTION_RTL; + } else { + input_direction = TEXT_DIRECTION_LTR; } + set_caret_column(caret.column); + update(); +} - if (scrolling) { - target_v_scroll = (target_v_scroll - p_delta); +void TextEdit::_new_line(bool p_split_current_line, bool p_above) { + if (!editable) { + return; + } + + begin_complex_operation(); + + bool first_line = false; + if (!p_split_current_line) { + if (p_above) { + if (caret.line > 0) { + set_caret_line(caret.line - 1, false); + set_caret_column(text[caret.line].length()); + } else { + set_caret_column(0); + first_line = true; + } + } else { + set_caret_column(text[caret.line].length()); + } + } + + insert_text_at_caret("\n"); + + if (first_line) { + set_caret_line(0); + } + + end_complex_operation(); +} + +void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { + // Handle selection + if (p_select) { + _pre_shift_selection(); + } else if (selection.active && !p_move_by_word) { + // If a selection is active, move caret to start of selection + set_caret_line(selection.from_line); + set_caret_column(selection.from_column); + deselect(); + return; } else { - target_v_scroll = (get_v_scroll() - p_delta); + deselect(); } - if (smooth_scroll_enabled) { - if (target_v_scroll <= 0) { - target_v_scroll = 0; + if (p_move_by_word) { + int cc = caret.column; + + if (cc == 0 && caret.line > 0) { + set_caret_line(caret.line - 1); + set_caret_column(text[caret.line].length()); + } else { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < cc) { + cc = words[i].x; + break; + } + } + set_caret_column(cc); } - if (Math::abs(target_v_scroll - v_scroll->get_value()) < 1.0) { - v_scroll->set_value(target_v_scroll); + } else { + // If the caret is at the start of the line, and not on the first line, move it up to the end of the previous line. + if (caret.column == 0) { + if (caret.line > 0) { + set_caret_line(caret.line - get_next_visible_line_offset_from(CLAMP(caret.line - 1, 0, text.size() - 1), -1)); + set_caret_column(text[caret.line].length()); + } } else { - scrolling = true; - set_physics_process_internal(true); + if (caret_mid_grapheme_enabled) { + set_caret_column(get_caret_column() - 1); + } else { + set_caret_column(TS->shaped_text_prev_grapheme_pos(text.get_line_data(caret.line)->get_rid(), get_caret_column())); + } } + } + + if (p_select) { + _post_shift_selection(); + } +} + +void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { + // Handle selection + if (p_select) { + _pre_shift_selection(); + } else if (selection.active && !p_move_by_word) { + // If a selection is active, move caret to end of selection + set_caret_line(selection.to_line); + set_caret_column(selection.to_column); + deselect(); + return; } else { - set_v_scroll(target_v_scroll); + deselect(); + } + + if (p_move_by_word) { + int cc = caret.column; + + if (cc == text[caret.line].length() && caret.line < text.size() - 1) { + set_caret_line(caret.line + 1); + set_caret_column(0); + } else { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > cc) { + cc = words[i].y; + break; + } + } + set_caret_column(cc); + } + } else { + // If we are at the end of the line, move the caret to the next line down. + if (caret.column == text[caret.line].length()) { + if (caret.line < text.size() - 1) { + set_caret_line(get_caret_line() + get_next_visible_line_offset_from(CLAMP(caret.line + 1, 0, text.size() - 1), 1), true, false); + set_caret_column(0); + } + } else { + if (caret_mid_grapheme_enabled) { + set_caret_column(get_caret_column() + 1); + } else { + set_caret_column(TS->shaped_text_next_grapheme_pos(text.get_line_data(caret.line)->get_rid(), get_caret_column())); + } + } + } + + if (p_select) { + _post_shift_selection(); } } -void TextEdit::_scroll_down(real_t p_delta) { - if (scrolling && smooth_scroll_enabled && SGN(target_v_scroll - v_scroll->get_value()) != SGN(p_delta)) { - scrolling = false; - minimap_clicked = false; +void TextEdit::_move_caret_up(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); } - if (scrolling) { - target_v_scroll = (target_v_scroll + p_delta); + int cur_wrap_index = get_caret_wrap_index(); + if (cur_wrap_index > 0) { + set_caret_line(caret.line, true, false, cur_wrap_index - 1); + } else if (caret.line == 0) { + set_caret_column(0); } else { - target_v_scroll = (get_v_scroll() + p_delta); + int new_line = caret.line - get_next_visible_line_offset_from(caret.line - 1, -1); + if (is_line_wrapped(new_line)) { + set_caret_line(new_line, true, false, get_line_wrap_count(new_line)); + } else { + set_caret_line(new_line, true, false); + } } - if (smooth_scroll_enabled) { - int max_v_scroll = round(v_scroll->get_max() - v_scroll->get_page()); - if (target_v_scroll > max_v_scroll) { - target_v_scroll = max_v_scroll; + if (p_select) { + _post_shift_selection(); + } +} + +void TextEdit::_move_caret_down(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + int cur_wrap_index = get_caret_wrap_index(); + if (cur_wrap_index < get_line_wrap_count(caret.line)) { + set_caret_line(caret.line, true, false, cur_wrap_index + 1); + } else if (caret.line == get_last_unhidden_line()) { + set_caret_column(text[caret.line].length()); + } else { + int new_line = caret.line + get_next_visible_line_offset_from(CLAMP(caret.line + 1, 0, text.size() - 1), 1); + set_caret_line(new_line, true, false, 0); + } + + if (p_select) { + _post_shift_selection(); + } +} + +void TextEdit::_move_caret_to_line_start(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } + + // Move caret column to start of wrapped row and then to start of text. + Vector<String> rows = get_line_wrapped_text(caret.line); + int wi = get_caret_wrap_index(); + int row_start_col = 0; + for (int i = 0; i < wi; i++) { + row_start_col += rows[i].length(); + } + if (caret.column == row_start_col || wi == 0) { + // Compute whitespace symbols sequence length. + int current_line_whitespace_len = 0; + while (current_line_whitespace_len < text[caret.line].length()) { + char32_t c = text[caret.line][current_line_whitespace_len]; + if (c != '\t' && c != ' ') { + break; + } + current_line_whitespace_len++; } - if (Math::abs(target_v_scroll - v_scroll->get_value()) < 1.0) { - v_scroll->set_value(target_v_scroll); + + if (get_caret_column() == current_line_whitespace_len) { + set_caret_column(0); } else { - scrolling = true; - set_physics_process_internal(true); + set_caret_column(current_line_whitespace_len); } } else { - set_v_scroll(target_v_scroll); + set_caret_column(row_start_col); + } + + if (p_select) { + _post_shift_selection(); } } -void TextEdit::_pre_shift_selection() { - if (!selection.active || selection.selecting_mode == SelectionMode::SELECTION_MODE_NONE) { - selection.selecting_line = cursor.line; - selection.selecting_column = cursor.column; - selection.active = true; +void TextEdit::_move_caret_to_line_end(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); } - selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; + // Move caret column to end of wrapped row and then to end of text. + Vector<String> rows = get_line_wrapped_text(caret.line); + int wi = get_caret_wrap_index(); + int row_end_col = -1; + for (int i = 0; i < wi + 1; i++) { + row_end_col += rows[i].length(); + } + if (wi == rows.size() - 1 || caret.column == row_end_col) { + set_caret_column(text[caret.line].length()); + } else { + set_caret_column(row_end_col); + } + + if (p_select) { + _post_shift_selection(); + } } -void TextEdit::_post_shift_selection() { - if (selection.active && selection.selecting_mode == SelectionMode::SELECTION_MODE_SHIFT) { - select(selection.selecting_line, selection.selecting_column, cursor.line, cursor.column); - update(); +void TextEdit::_move_caret_page_up(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); } - selection.selecting_text = true; -} + Point2i next_line = get_next_visible_line_index_offset_from(caret.line, get_caret_wrap_index(), -get_visible_line_count()); + int n_line = caret.line - next_line.x + 1; + set_caret_line(n_line, true, false, next_line.y); -void TextEdit::_scroll_lines_up() { - scrolling = false; - minimap_clicked = false; + if (p_select) { + _post_shift_selection(); + } +} - // Adjust the vertical scroll. - set_v_scroll(get_v_scroll() - 1); +void TextEdit::_move_caret_page_down(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - // Adjust the cursor to viewport. - if (!selection.active) { - int cur_line = cursor.line; - int cur_wrap = get_cursor_wrap_index(); - int last_vis_line = get_last_full_visible_line(); - int last_vis_wrap = get_last_full_visible_line_wrap_index(); + Point2i next_line = get_next_visible_line_index_offset_from(caret.line, get_caret_wrap_index(), get_visible_line_count()); + int n_line = caret.line + next_line.x - 1; + set_caret_line(n_line, true, false, next_line.y); - if (cur_line > last_vis_line || (cur_line == last_vis_line && cur_wrap > last_vis_wrap)) { - cursor_set_line(last_vis_line, false, false, last_vis_wrap); - } + if (p_select) { + _post_shift_selection(); } } -void TextEdit::_scroll_lines_down() { - scrolling = false; - minimap_clicked = false; +void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { + if (!editable) { + return; + } - // Adjust the vertical scroll. - set_v_scroll(get_v_scroll() + 1); + if (has_selection() || (!p_all_to_left && !p_word)) { + backspace(); + return; + } - // Adjust the cursor to viewport. - if (!selection.active) { - int cur_line = cursor.line; - int cur_wrap = get_cursor_wrap_index(); - int first_vis_line = get_first_visible_line(); - int first_vis_wrap = cursor.wrap_ofs; + if (p_all_to_left) { + int caret_current_column = caret.column; + caret.column = 0; + _remove_text(caret.line, 0, caret.line, caret_current_column); + return; + } - if (cur_line < first_vis_line || (cur_line == first_vis_line && cur_wrap < first_vis_wrap)) { - cursor_set_line(first_vis_line, false, false, first_vis_wrap); + if (p_word) { + int line = caret.line; + int column = caret.column; + + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < column) { + column = words[i].x; + break; + } } + + _remove_text(line, column, caret.line, caret.column); + + set_caret_line(line, false); + set_caret_column(column); + return; } } -/**** TEXT EDIT CORE API ****/ +void TextEdit::_delete(bool p_word, bool p_all_to_right) { + if (!editable) { + return; + } -void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, int &r_end_line, int &r_end_column) { - // Save for undo. - ERR_FAIL_INDEX(p_line, text.size()); - ERR_FAIL_COND(p_char < 0); + if (has_selection()) { + delete_selection(); + return; + } + int curline_len = text[caret.line].length(); - /* STEP 1: Remove \r from source text and separate in substrings. */ + if (caret.line == text.size() - 1 && caret.column == curline_len) { + return; // Last line, last column: Nothing to do. + } - Vector<String> substrings = p_text.replace("\r", "").split("\n"); + int next_line = caret.column < curline_len ? caret.line : caret.line + 1; + int next_column; - // Is this just a new empty line? - bool shift_first_line = p_char == 0 && p_text.replace("\r", "") == "\n"; + if (p_all_to_right) { + // Delete everything to right of caret + next_column = curline_len; + next_line = caret.line; + } else if (p_word && caret.column < curline_len - 1) { + // Delete next word to right of caret + int line = caret.line; + int column = caret.column; - /* STEP 2: Add spaces if the char is greater than the end of the line. */ - while (p_char > text[p_line].length()) { - text.set(p_line, text[p_line] + String::chr(' '), structured_text_parser(st_parser, st_args, text[p_line] + String::chr(' '))); + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > column) { + column = words[i].y; + break; + } + } + + next_line = line; + next_column = column; + } else { + // Delete one character + if (caret_mid_grapheme_enabled) { + next_column = caret.column < curline_len ? (caret.column + 1) : 0; + } else { + next_column = caret.column < curline_len ? TS->shaped_text_next_grapheme_pos(text.get_line_data(caret.line)->get_rid(), (caret.column)) : 0; + } } - /* STEP 3: Separate dest string in pre and post text. */ + _remove_text(caret.line, caret.column, next_line, next_column); + update(); +} - String preinsert_text = text[p_line].substr(0, p_char); - String postinsert_text = text[p_line].substr(p_char, text[p_line].size()); +void TextEdit::_move_caret_document_start(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - for (int j = 0; j < substrings.size(); j++) { - // Insert the substrings. + set_caret_line(0); + set_caret_column(0); - if (j == 0) { - text.set(p_line, preinsert_text + substrings[j], structured_text_parser(st_parser, st_args, preinsert_text + substrings[j])); - } else { - text.insert(p_line + j, substrings[j], structured_text_parser(st_parser, st_args, substrings[j])); - } + if (p_select) { + _post_shift_selection(); + } +} - if (j == substrings.size() - 1) { - text.set(p_line + j, text[p_line + j] + postinsert_text, structured_text_parser(st_parser, st_args, text[p_line + j] + postinsert_text)); - } +void TextEdit::_move_caret_document_end(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); } - if (shift_first_line) { - text.move_gutters(p_line, p_line + 1); - text.set_hidden(p_line + 1, text.is_hidden(p_line)); + set_caret_line(get_last_unhidden_line(), true, false, 9999); + set_caret_column(text[caret.line].length()); - text.set_hidden(p_line, false); + if (p_select) { + _post_shift_selection(); } +} - text.invalidate_cache(p_line); +void TextEdit::_update_caches() { + /* Internal API for CodeEdit. */ + brace_mismatch_color = get_theme_color(SNAME("brace_mismatch_color"), SNAME("CodeEdit")); + code_folding_color = get_theme_color(SNAME("code_folding_color"), SNAME("CodeEdit")); + folded_eol_icon = get_theme_icon(SNAME("folded_eol_icon"), SNAME("CodeEdit")); - r_end_line = p_line + substrings.size() - 1; - r_end_column = text[r_end_line].length() - postinsert_text.length(); + /* Search */ + search_result_color = get_theme_color(SNAME("search_result_color")); + search_result_border_color = get_theme_color(SNAME("search_result_border_color")); - TextServer::Direction dir = TS->shaped_text_get_dominant_direciton_in_range(text.get_line_data(r_end_line)->get_rid(), (r_end_line == p_line) ? cursor.column : 0, r_end_column); - if (dir != TextServer::DIRECTION_AUTO) { - input_direction = (TextDirection)dir; + /* Caret */ + caret_color = get_theme_color(SNAME("caret_color")); + caret_background_color = get_theme_color(SNAME("caret_background_color")); + + /* Selection */ + font_selected_color = get_theme_color(SNAME("font_selected_color")); + selection_color = get_theme_color(SNAME("selection_color")); + + /* Visual. */ + style_normal = get_theme_stylebox(SNAME("normal")); + style_focus = get_theme_stylebox(SNAME("focus")); + style_readonly = get_theme_stylebox(SNAME("read_only")); + + tab_icon = get_theme_icon(SNAME("tab")); + space_icon = get_theme_icon(SNAME("space")); + + font = get_theme_font(SNAME("font")); + font_size = get_theme_font_size(SNAME("font_size")); + font_color = get_theme_color(SNAME("font_color")); + font_readonly_color = get_theme_color(SNAME("font_readonly_color")); + + outline_size = get_theme_constant(SNAME("outline_size")); + outline_color = get_theme_color(SNAME("font_outline_color")); + + line_spacing = get_theme_constant(SNAME("line_spacing")); + + background_color = get_theme_color(SNAME("background_color")); + current_line_color = get_theme_color(SNAME("current_line_color")); + word_highlighted_color = get_theme_color(SNAME("word_highlighted_color")); + + /* Text properties. */ + TextServer::Direction dir; + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + } else { + dir = (TextServer::Direction)text_direction; } + text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + text.set_font_features(opentype_features); + text.set_draw_control_chars(draw_control_chars); + text.set_font(font); + text.set_font_size(font_size); + text.invalidate_all(); - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); - } - text_changed_dirty = true; + /* Syntax highlighting. */ + if (syntax_highlighter.is_valid()) { + syntax_highlighter->set_text_edit(this); } - emit_signal("lines_edited_from", p_line, r_end_line); } -String TextEdit::_base_get_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) const { - ERR_FAIL_INDEX_V(p_from_line, text.size(), String()); - ERR_FAIL_INDEX_V(p_from_column, text[p_from_line].length() + 1, String()); - ERR_FAIL_INDEX_V(p_to_line, text.size(), String()); - ERR_FAIL_INDEX_V(p_to_column, text[p_to_line].length() + 1, String()); - ERR_FAIL_COND_V(p_to_line < p_from_line, String()); // 'from > to'. - ERR_FAIL_COND_V(p_to_line == p_from_line && p_to_column < p_from_column, String()); // 'from > to'. +/* General overrides. */ +Size2 TextEdit::get_minimum_size() const { + return style_normal->get_minimum_size(); +} - String ret; +bool TextEdit::is_text_field() const { + return true; +} - for (int i = p_from_line; i <= p_to_line; i++) { - int begin = (i == p_from_line) ? p_from_column : 0; - int end = (i == p_to_line) ? p_to_column : text[i].length(); +Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { + Point2i pos = get_line_column_at_pos(p_pos); + int row = pos.y; - if (i > p_from_line) { - ret += "\n"; + int left_margin = style_normal->get_margin(SIDE_LEFT); + int gutter = left_margin + gutters_width; + if (p_pos.x < gutter) { + for (int i = 0; i < gutters.size(); i++) { + if (!gutters[i].draw) { + continue; + } + + if (p_pos.x > left_margin && p_pos.x <= (left_margin + gutters[i].width) - 3) { + if (gutters[i].clickable || is_line_gutter_clickable(row, i)) { + return CURSOR_POINTING_HAND; + } + } + left_margin += gutters[i].width; } - ret += text[i].substr(begin, end - begin); + return CURSOR_ARROW; } - return ret; + int xmargin_end = get_size().width - style_normal->get_margin(SIDE_RIGHT); + if (draw_minimap && p_pos.x > xmargin_end - minimap_width && p_pos.x <= xmargin_end) { + return CURSOR_ARROW; + } + return get_default_cursor_shape(); } -void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { - ERR_FAIL_INDEX(p_from_line, text.size()); - ERR_FAIL_INDEX(p_from_column, text[p_from_line].length() + 1); - ERR_FAIL_INDEX(p_to_line, text.size()); - ERR_FAIL_INDEX(p_to_column, text[p_to_line].length() + 1); - ERR_FAIL_COND(p_to_line < p_from_line); // 'from > to'. - ERR_FAIL_COND(p_to_line == p_from_line && p_to_column < p_from_column); // 'from > to'. +String TextEdit::get_tooltip(const Point2 &p_pos) const { + if (!tooltip_obj) { + return Control::get_tooltip(p_pos); + } + Point2i pos = get_line_column_at_pos(p_pos); + int row = pos.y; + int col = pos.x; - String pre_text = text[p_from_line].substr(0, p_from_column); - String post_text = text[p_to_line].substr(p_to_column, text[p_to_line].length()); + String s = text[row]; + if (s.length() == 0) { + return Control::get_tooltip(p_pos); + } + int beg, end; + if (select_word(s, col, beg, end)) { + String tt = tooltip_obj->call(tooltip_func, s.substr(beg, end - beg), tooltip_ud); - for (int i = p_from_line; i < p_to_line; i++) { - text.remove(p_from_line + 1); + return tt; } - text.set(p_from_line, pre_text + post_text, structured_text_parser(st_parser, st_args, pre_text + post_text)); - //text.set_line_wrap_amount(p_from_line, -1); - text.invalidate_cache(p_from_line); + return Control::get_tooltip(p_pos); +} - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); +void TextEdit::set_tooltip_request_func(Object *p_obj, const StringName &p_function, const Variant &p_udata) { + tooltip_obj = p_obj; + tooltip_func = p_function; + tooltip_ud = p_udata; +} + +/* Text */ +// Text properties. +bool TextEdit::has_ime_text() const { + return !ime_text.is_empty(); +} + +void TextEdit::set_editable(const bool p_editable) { + if (editable == p_editable) { + return; + } + + editable = p_editable; + + update(); +} + +bool TextEdit::is_editable() const { + return editable; +} + +void TextEdit::set_text_direction(Control::TextDirection p_text_direction) { + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (text_direction != p_text_direction) { + text_direction = p_text_direction; + if (text_direction != TEXT_DIRECTION_AUTO && text_direction != TEXT_DIRECTION_INHERITED) { + input_direction = text_direction; } - text_changed_dirty = true; + TextServer::Direction dir; + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + } else { + dir = (TextServer::Direction)text_direction; + } + text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + text.invalidate_all(); + + if (menu_dir) { + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_AUTO), text_direction == TEXT_DIRECTION_AUTO); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); + } + update(); } - emit_signal("lines_edited_from", p_to_line, p_from_line); } -void TextEdit::_insert_text(int p_line, int p_char, const String &p_text, int *r_end_line, int *r_end_char) { - if (!setting_text && idle_detect->is_inside_tree()) { - idle_detect->start(); +Control::TextDirection TextEdit::get_text_direction() const { + return text_direction; +} + +void TextEdit::set_opentype_feature(const String &p_name, int p_value) { + int32_t tag = TS->name_to_tag(p_name); + if (!opentype_features.has(tag) || (int)opentype_features[tag] != p_value) { + opentype_features[tag] = p_value; + text.set_font_features(opentype_features); + text.invalidate_all(); + update(); } +} - if (undo_enabled) { - _clear_redo(); +int TextEdit::get_opentype_feature(const String &p_name) const { + int32_t tag = TS->name_to_tag(p_name); + if (!opentype_features.has(tag)) { + return -1; } + return opentype_features[tag]; +} - int retline, retchar; - _base_insert_text(p_line, p_char, p_text, retline, retchar); - if (r_end_line) { - *r_end_line = retline; +void TextEdit::clear_opentype_features() { + opentype_features.clear(); + text.set_font_features(opentype_features); + text.invalidate_all(); + update(); +} + +void TextEdit::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + TextServer::Direction dir; + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + } else { + dir = (TextServer::Direction)text_direction; + } + text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + text.invalidate_all(); + update(); } - if (r_end_char) { - *r_end_char = retchar; +} + +String TextEdit::get_language() const { + return language; +} + +void TextEdit::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + for (int i = 0; i < text.size(); i++) { + text.set(i, text[i], structured_text_parser(st_parser, st_args, text[i])); + } + update(); } +} - if (!undo_enabled) { +Control::StructuredTextParser TextEdit::get_structured_text_bidi_override() const { + return st_parser; +} + +void TextEdit::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + for (int i = 0; i < text.size(); i++) { + text.set(i, text[i], structured_text_parser(st_parser, st_args, text[i])); + } + update(); +} + +Array TextEdit::get_structured_text_bidi_override_options() const { + return st_args; +} + +void TextEdit::set_tab_size(const int p_size) { + ERR_FAIL_COND_MSG(p_size <= 0, "Tab size must be greater than 0."); + if (p_size == text.get_tab_size()) { return; } + text.set_tab_size(p_size); + text.invalidate_all_lines(); + update(); +} - /* UNDO!! */ - TextOperation op; - op.type = TextOperation::TYPE_INSERT; - op.from_line = p_line; - op.from_column = p_char; - op.to_line = retline; - op.to_column = retchar; - op.text = p_text; - op.version = ++version; - op.chain_forward = false; - op.chain_backward = false; +int TextEdit::get_tab_size() const { + return text.get_tab_size(); +} - // See if it should just be set as current op. - if (current_op.type != op.type) { - op.prev_version = get_version(); - _push_current_op(); - current_op = op; +// User controls +void TextEdit::set_overtype_mode_enabled(const bool p_enabled) { + overtype_mode = p_enabled; + update(); +} - return; // Set as current op, return. - } - // See if it can be merged. - if (current_op.to_line != p_line || current_op.to_column != p_char) { - op.prev_version = get_version(); - _push_current_op(); - current_op = op; - return; // Set as current op, return. - } - // Merge current op. +bool TextEdit::is_overtype_mode_enabled() const { + return overtype_mode; +} - current_op.text += p_text; - current_op.to_column = retchar; - current_op.to_line = retline; - current_op.version = op.version; +void TextEdit::set_context_menu_enabled(bool p_enable) { + context_menu_enabled = p_enable; } -void TextEdit::_remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { - if (!setting_text && idle_detect->is_inside_tree()) { - idle_detect->start(); +bool TextEdit::is_context_menu_enabled() const { + return context_menu_enabled; +} + +void TextEdit::set_shortcut_keys_enabled(bool p_enabled) { + shortcut_keys_enabled = p_enabled; +} + +bool TextEdit::is_shortcut_keys_enabled() const { + return shortcut_keys_enabled; +} + +void TextEdit::set_virtual_keyboard_enabled(bool p_enable) { + virtual_keyboard_enabled = p_enable; +} + +bool TextEdit::is_virtual_keyboard_enabled() const { + return virtual_keyboard_enabled; +} + +// Text manipulation +void TextEdit::clear() { + setting_text = true; + _clear(); + setting_text = false; + emit_signal(SNAME("text_set")); +} + +void TextEdit::_clear() { + clear_undo_history(); + text.clear(); + caret.column = 0; + caret.line = 0; + caret.x_ofs = 0; + caret.line_ofs = 0; + caret.wrap_ofs = 0; + caret.last_fit_x = 0; + selection.active = false; +} + +void TextEdit::set_text(const String &p_text) { + setting_text = true; + if (!undo_enabled) { + _clear(); + insert_text_at_caret(p_text); } - String text; if (undo_enabled) { - _clear_redo(); - text = _base_get_text(p_from_line, p_from_column, p_to_line, p_to_column); + set_caret_line(0); + set_caret_column(0); + + begin_complex_operation(); + deselect(); + _remove_text(0, 0, MAX(0, get_line_count() - 1), MAX(get_line(MAX(get_line_count() - 1, 0)).size() - 1, 0)); + insert_text_at_caret(p_text); + end_complex_operation(); } - _base_remove_text(p_from_line, p_from_column, p_to_line, p_to_column); + set_caret_line(0); + set_caret_column(0); - if (!undo_enabled) { - return; + update(); + setting_text = false; + emit_signal(SNAME("text_set")); +} + +String TextEdit::get_text() const { + StringBuilder ret_text; + const int text_size = text.size(); + for (int i = 0; i < text_size; i++) { + ret_text += text[i]; + if (i != text_size - 1) { + ret_text += "\n"; + } } + return ret_text.as_string(); +} - /* UNDO! */ - TextOperation op; - op.type = TextOperation::TYPE_REMOVE; - op.from_line = p_from_line; - op.from_column = p_from_column; - op.to_line = p_to_line; - op.to_column = p_to_column; - op.text = text; - op.version = ++version; - op.chain_forward = false; - op.chain_backward = false; +int TextEdit::get_line_count() const { + return text.size(); +} - // See if it should just be set as current op. - if (current_op.type != op.type) { - op.prev_version = get_version(); - _push_current_op(); - current_op = op; - return; // Set as current op, return. +void TextEdit::set_line(int p_line, const String &p_new_text) { + if (p_line < 0 || p_line >= text.size()) { + return; } - // See if it can be merged. - if (current_op.from_line == p_to_line && current_op.from_column == p_to_column) { - // Backspace or similar. - current_op.text = text + current_op.text; - current_op.from_line = p_from_line; - current_op.from_column = p_from_column; - return; // Update current op. + _remove_text(p_line, 0, p_line, text[p_line].length()); + _insert_text(p_line, 0, p_new_text); + if (caret.line == p_line) { + caret.column = MIN(caret.column, p_new_text.length()); } + if (has_selection() && p_line == selection.to_line && selection.to_column > text[p_line].length()) { + selection.to_column = text[p_line].length(); + } +} - op.prev_version = get_version(); - _push_current_op(); - current_op = op; +String TextEdit::get_line(int p_line) const { + if (p_line < 0 || p_line >= text.size()) { + return ""; + } + return text[p_line]; } -void TextEdit::_insert_text_at_cursor(const String &p_text) { - int new_column, new_line; - _insert_text(cursor.line, cursor.column, p_text, &new_line, &new_column); - _update_scrollbars(); - cursor_set_line(new_line, false); - cursor_set_column(new_column); +int TextEdit::get_line_width(int p_line, int p_wrap_index) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + ERR_FAIL_COND_V(p_wrap_index > get_line_wrap_count(p_line), 0); - update(); + return text.get_line_width(p_line, p_wrap_index); } -int TextEdit::get_char_count() { - int totalsize = 0; +int TextEdit::get_line_height() const { + return text.get_line_height() + line_spacing; +} - for (int i = 0; i < text.size(); i++) { - if (i > 0) { - totalsize++; // Include \n. +int TextEdit::get_indent_level(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + + int tab_count = 0; + int whitespace_count = 0; + int line_length = text[p_line].size(); + for (int i = 0; i < line_length - 1; i++) { + if (text[p_line][i] == '\t') { + tab_count++; + } else if (text[p_line][i] == ' ') { + whitespace_count++; + } else { + break; } - totalsize += text[i].length(); } + return tab_count * text.get_tab_size() + whitespace_count; +} + +int TextEdit::get_first_non_whitespace_column(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); - return totalsize; // Omit last \n. + int col = 0; + while (col < text[p_line].length() && _is_whitespace(text[p_line][col])) { + col++; + } + return col; } -Size2 TextEdit::get_minimum_size() const { - return cache.style_normal->get_minimum_size(); +void TextEdit::swap_lines(int p_from_line, int p_to_line) { + ERR_FAIL_INDEX(p_from_line, text.size()); + ERR_FAIL_INDEX(p_to_line, text.size()); + + String tmp = get_line(p_from_line); + String tmp2 = get_line(p_to_line); + set_line(p_to_line, tmp); + set_line(p_from_line, tmp2); } -int TextEdit::_get_control_height() const { - int control_height = get_size().height; - control_height -= cache.style_normal->get_minimum_size().height; - if (h_scroll->is_visible_in_tree()) { - control_height -= h_scroll->get_size().height; +void TextEdit::insert_line_at(int p_at, const String &p_text) { + ERR_FAIL_INDEX(p_at, text.size()); + + _insert_text(p_at, 0, p_text + "\n"); + if (caret.line >= p_at) { + // offset caret when located after inserted line + ++caret.line; + } + if (has_selection()) { + if (selection.from_line >= p_at) { + // offset selection when located after inserted line + ++selection.from_line; + ++selection.to_line; + } else if (selection.to_line >= p_at) { + // extend selection that includes inserted line + ++selection.to_line; + } } - return control_height; } -int TextEdit::_get_menu_action_accelerator(const String &p_action) { - const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(p_action); - if (!events) { - return 0; +void TextEdit::insert_text_at_caret(const String &p_text) { + begin_complex_operation(); + + delete_selection(); + + int new_column, new_line; + _insert_text(caret.line, caret.column, p_text, &new_line, &new_column); + _update_scrollbars(); + + set_caret_line(new_line, false); + set_caret_column(new_column); + update(); + + end_complex_operation(); +} + +void TextEdit::remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { + ERR_FAIL_INDEX(p_from_line, text.size()); + ERR_FAIL_INDEX(p_from_column, text[p_from_line].length() + 1); + ERR_FAIL_INDEX(p_to_line, text.size()); + ERR_FAIL_INDEX(p_to_column, text[p_to_line].length() + 1); + ERR_FAIL_COND(p_to_line < p_from_line); + ERR_FAIL_COND(p_to_line == p_from_line && p_to_column < p_from_column); + + _remove_text(p_from_line, p_from_column, p_to_line, p_to_column); +} + +int TextEdit::get_last_unhidden_line() const { + // Returns the last line in the text that is not hidden. + if (!_is_hiding_enabled()) { + return text.size() - 1; } - // Use first event in the list for the accelerator. - const List<Ref<InputEvent>>::Element *first_event = events->front(); - if (!first_event) { - return 0; + int last_line; + for (last_line = text.size() - 1; last_line > 0; last_line--) { + if (!_is_line_hidden(last_line)) { + break; + } } + return last_line; +} - const Ref<InputEventKey> event = first_event->get(); - if (event.is_null()) { - return 0; +int TextEdit::get_next_visible_line_offset_from(int p_line_from, int p_visible_amount) const { + // Returns the number of lines (hidden and unhidden) from p_line_from to (p_line_from + visible_amount of unhidden lines). + ERR_FAIL_INDEX_V(p_line_from, text.size(), ABS(p_visible_amount)); + + if (!_is_hiding_enabled()) { + return ABS(p_visible_amount); } - // Use physical keycode if non-zero - if (event->get_physical_keycode() != 0) { - return event->get_physical_keycode_with_modifiers(); + int num_visible = 0; + int num_total = 0; + if (p_visible_amount >= 0) { + for (int i = p_line_from; i < text.size(); i++) { + num_total++; + if (!_is_line_hidden(i)) { + num_visible++; + } + if (num_visible >= p_visible_amount) { + break; + } + } } else { - return event->get_keycode_with_modifiers(); + p_visible_amount = ABS(p_visible_amount); + for (int i = p_line_from; i >= 0; i--) { + num_total++; + if (!_is_line_hidden(i)) { + num_visible++; + } + if (num_visible >= p_visible_amount) { + break; + } + } } + return num_total; } -void TextEdit::_generate_context_menu() { - // Reorganize context menu. - menu->clear(); - if (!readonly) { - menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_cut") : 0); - } - menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_copy") : 0); - if (!readonly) { - menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_paste") : 0); +Point2i TextEdit::get_next_visible_line_index_offset_from(int p_line_from, int p_wrap_index_from, int p_visible_amount) const { + // Returns the number of lines (hidden and unhidden) from (p_line_from + p_wrap_index_from) row to (p_line_from + visible_amount of unhidden and wrapped rows). + // Wrap index is set to the wrap index of the last line. + int wrap_index = 0; + ERR_FAIL_INDEX_V(p_line_from, text.size(), Point2i(ABS(p_visible_amount), 0)); + + if (!_is_hiding_enabled() && get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) { + return Point2i(ABS(p_visible_amount), 0); } - menu->add_separator(); - if (is_selecting_enabled()) { - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : 0); + + int num_visible = 0; + int num_total = 0; + if (p_visible_amount == 0) { + num_total = 0; + wrap_index = 0; + } else if (p_visible_amount > 0) { + int i; + num_visible -= p_wrap_index_from; + for (i = p_line_from; i < text.size(); i++) { + num_total++; + if (!_is_line_hidden(i)) { + num_visible++; + num_visible += get_line_wrap_count(i); + } + if (num_visible >= p_visible_amount) { + break; + } + } + wrap_index = get_line_wrap_count(MIN(i, text.size() - 1)) - MAX(0, num_visible - p_visible_amount); + } else { + p_visible_amount = ABS(p_visible_amount); + int i; + num_visible -= get_line_wrap_count(p_line_from) - p_wrap_index_from; + for (i = p_line_from; i >= 0; i--) { + num_total++; + if (!_is_line_hidden(i)) { + num_visible++; + num_visible += get_line_wrap_count(i); + } + if (num_visible >= p_visible_amount) { + break; + } + } + wrap_index = MAX(0, num_visible - p_visible_amount); } - if (!readonly) { - menu->add_item(RTR("Clear"), MENU_CLEAR); - menu->add_separator(); - menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_undo") : 0); - menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_redo") : 0); + wrap_index = MAX(wrap_index, 0); + return Point2i(num_total, wrap_index); +} + +// Overridable actions +void TextEdit::handle_unicode_input(const uint32_t p_unicode) { + if (GDVIRTUAL_CALL(_handle_unicode_input, p_unicode)) { + return; } - menu->add_separator(); - menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); - menu->add_separator(); - menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); - if (!readonly) { - menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); + _handle_unicode_input_internal(p_unicode); +} + +void TextEdit::backspace() { + if (GDVIRTUAL_CALL(_backspace)) { + return; } + _backspace_internal(); } -int TextEdit::get_visible_rows() const { - return _get_control_height() / get_row_height(); +void TextEdit::cut() { + if (GDVIRTUAL_CALL(_cut)) { + return; + } + _cut_internal(); } -int TextEdit::_get_minimap_visible_rows() const { - return _get_control_height() / (minimap_char_size.y + minimap_line_spacing); +void TextEdit::copy() { + if (GDVIRTUAL_CALL(_copy)) { + return; + } + _copy_internal(); } -int TextEdit::get_total_visible_rows() const { - // Returns the total amount of rows we need in the editor. - // This skips hidden lines and counts each wrapping of a line. - if (!is_hiding_enabled() && !is_wrap_enabled()) { - return text.size(); +void TextEdit::paste() { + if (GDVIRTUAL_CALL(_paste)) { + return; } + _paste_internal(); +} - int total_rows = 0; - for (int i = 0; i < text.size(); i++) { - if (!text.is_hidden(i)) { - total_rows++; - total_rows += times_line_wraps(i); +// Context menu. +PopupMenu *TextEdit::get_menu() const { + const_cast<TextEdit *>(this)->_generate_context_menu(); + return menu; +} + +bool TextEdit::is_menu_visible() const { + return menu && menu->is_visible(); +} + +void TextEdit::menu_option(int p_option) { + switch (p_option) { + case MENU_CUT: { + cut(); + } break; + case MENU_COPY: { + copy(); + } break; + case MENU_PASTE: { + paste(); + } break; + case MENU_CLEAR: { + if (editable) { + clear(); + } + } break; + case MENU_SELECT_ALL: { + select_all(); + } break; + case MENU_UNDO: { + undo(); + } break; + case MENU_REDO: { + redo(); + } break; + case MENU_DIR_INHERITED: { + set_text_direction(TEXT_DIRECTION_INHERITED); + } break; + case MENU_DIR_AUTO: { + set_text_direction(TEXT_DIRECTION_AUTO); + } break; + case MENU_DIR_LTR: { + set_text_direction(TEXT_DIRECTION_LTR); + } break; + case MENU_DIR_RTL: { + set_text_direction(TEXT_DIRECTION_RTL); + } break; + case MENU_DISPLAY_UCC: { + set_draw_control_chars(!get_draw_control_chars()); + } break; + case MENU_INSERT_LRM: { + if (editable) { + insert_text_at_caret(String::chr(0x200E)); + } + } break; + case MENU_INSERT_RLM: { + if (editable) { + insert_text_at_caret(String::chr(0x200F)); + } + } break; + case MENU_INSERT_LRE: { + if (editable) { + insert_text_at_caret(String::chr(0x202A)); + } + } break; + case MENU_INSERT_RLE: { + if (editable) { + insert_text_at_caret(String::chr(0x202B)); + } + } break; + case MENU_INSERT_LRO: { + if (editable) { + insert_text_at_caret(String::chr(0x202D)); + } + } break; + case MENU_INSERT_RLO: { + if (editable) { + insert_text_at_caret(String::chr(0x202E)); + } + } break; + case MENU_INSERT_PDF: { + if (editable) { + insert_text_at_caret(String::chr(0x202C)); + } + } break; + case MENU_INSERT_ALM: { + if (editable) { + insert_text_at_caret(String::chr(0x061C)); + } + } break; + case MENU_INSERT_LRI: { + if (editable) { + insert_text_at_caret(String::chr(0x2066)); + } + } break; + case MENU_INSERT_RLI: { + if (editable) { + insert_text_at_caret(String::chr(0x2067)); + } + } break; + case MENU_INSERT_FSI: { + if (editable) { + insert_text_at_caret(String::chr(0x2068)); + } + } break; + case MENU_INSERT_PDI: { + if (editable) { + insert_text_at_caret(String::chr(0x2069)); + } + } break; + case MENU_INSERT_ZWJ: { + if (editable) { + insert_text_at_caret(String::chr(0x200D)); + } + } break; + case MENU_INSERT_ZWNJ: { + if (editable) { + insert_text_at_caret(String::chr(0x200C)); + } + } break; + case MENU_INSERT_WJ: { + if (editable) { + insert_text_at_caret(String::chr(0x2060)); + } + } break; + case MENU_INSERT_SHY: { + if (editable) { + insert_text_at_caret(String::chr(0x00AD)); + } } } - return total_rows; } -void TextEdit::_update_wrap_at(bool p_force) { - int new_wrap_at = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding; - if (draw_minimap) { - new_wrap_at -= minimap_width; +/* Versioning */ +void TextEdit::begin_complex_operation() { + _push_current_op(); + if (complex_operation_count == 0) { + next_operation_is_complex = true; } - if (v_scroll->is_visible_in_tree()) { - new_wrap_at -= v_scroll->get_combined_minimum_size().width; + complex_operation_count++; +} + +void TextEdit::end_complex_operation() { + _push_current_op(); + ERR_FAIL_COND(undo_stack.size() == 0); + + complex_operation_count = MAX(complex_operation_count - 1, 0); + if (complex_operation_count > 0) { + return; + } + if (undo_stack.back()->get().chain_forward) { + undo_stack.back()->get().chain_forward = false; + return; } - new_wrap_at -= wrap_right_offset; // Give it a little more space. - if ((wrap_at != new_wrap_at) || p_force) { - wrap_at = new_wrap_at; - if (wrap_enabled) { - text.set_width(wrap_at); - } else { - text.set_width(-1); - } - text.invalidate_all_lines(); + undo_stack.back()->get().chain_backward = true; +} + +bool TextEdit::has_undo() const { + if (undo_stack_pos == nullptr) { + int pending = current_op.type == TextOperation::TYPE_NONE ? 0 : 1; + return undo_stack.size() + pending > 0; } + return undo_stack_pos != undo_stack.front(); +} - update_cursor_wrap_offset(); +bool TextEdit::has_redo() const { + return undo_stack_pos != nullptr; } -void TextEdit::adjust_viewport_to_cursor() { - // Make sure cursor is visible on the screen. - scrolling = false; - minimap_clicked = false; +void TextEdit::undo() { + if (!editable) { + return; + } - int cur_line = cursor.line; - int cur_wrap = get_cursor_wrap_index(); + _push_current_op(); - int first_vis_line = get_first_visible_line(); - int first_vis_wrap = cursor.wrap_ofs; - int last_vis_line = get_last_full_visible_line(); - int last_vis_wrap = get_last_full_visible_line_wrap_index(); + if (undo_stack_pos == nullptr) { + if (!undo_stack.size()) { + return; // Nothing to undo. + } - if (cur_line < first_vis_line || (cur_line == first_vis_line && cur_wrap < first_vis_wrap)) { - // Cursor is above screen. - set_line_as_first_visible(cur_line, cur_wrap); - } else if (cur_line > last_vis_line || (cur_line == last_vis_line && cur_wrap > last_vis_wrap)) { - // Cursor is below screen. - set_line_as_last_visible(cur_line, cur_wrap); - } + undo_stack_pos = undo_stack.back(); - int visible_width = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding - cache.minimap_width; - if (v_scroll->is_visible_in_tree()) { - visible_width -= v_scroll->get_combined_minimum_size().width; + } else if (undo_stack_pos == undo_stack.front()) { + return; // At the bottom of the undo stack. + } else { + undo_stack_pos = undo_stack_pos->prev(); } - visible_width -= 20; // Give it a little more space. - if (!is_wrap_enabled()) { - // Adjust x offset. - Vector2i cursor_pos; + deselect(); - // Get position of the start of caret. - if (ime_text.length() != 0 && ime_selection.x != 0) { - cursor_pos.x = get_column_x_offset_for_line(cursor.column + ime_selection.x, cursor.line); - } else { - cursor_pos.x = get_column_x_offset_for_line(cursor.column, cursor.line); - } + TextOperation op = undo_stack_pos->get(); + _do_text_op(op, true); + if (op.type != TextOperation::TYPE_INSERT && (op.from_line != op.to_line || op.to_column != op.from_column + 1)) { + select(op.from_line, op.from_column, op.to_line, op.to_column); + } - // Get position of the end of caret. - if (ime_text.length() != 0) { - if (ime_selection.y != 0) { - cursor_pos.y = get_column_x_offset_for_line(cursor.column + ime_selection.x + ime_selection.y, cursor.line); - } else { - cursor_pos.y = get_column_x_offset_for_line(cursor.column + ime_text.size(), cursor.line); + current_op.version = op.prev_version; + if (undo_stack_pos->get().chain_backward) { + while (true) { + ERR_BREAK(!undo_stack_pos->prev()); + undo_stack_pos = undo_stack_pos->prev(); + op = undo_stack_pos->get(); + _do_text_op(op, true); + current_op.version = op.prev_version; + if (undo_stack_pos->get().chain_forward) { + break; } - } else { - cursor_pos.y = cursor_pos.x; } + } - if (MAX(cursor_pos.x, cursor_pos.y) > (cursor.x_ofs + visible_width)) { - cursor.x_ofs = MAX(cursor_pos.x, cursor_pos.y) - visible_width + 1; - } + _update_scrollbars(); + if (undo_stack_pos->get().type == TextOperation::TYPE_REMOVE) { + set_caret_line(undo_stack_pos->get().to_line, false); + set_caret_column(undo_stack_pos->get().to_column); + } else { + set_caret_line(undo_stack_pos->get().from_line, false); + set_caret_column(undo_stack_pos->get().from_column); + } + update(); +} - if (MIN(cursor_pos.x, cursor_pos.y) < cursor.x_ofs) { - cursor.x_ofs = MIN(cursor_pos.x, cursor_pos.y); +void TextEdit::redo() { + if (!editable) { + return; + } + _push_current_op(); + + if (undo_stack_pos == nullptr) { + return; // Nothing to do. + } + + deselect(); + + TextOperation op = undo_stack_pos->get(); + _do_text_op(op, false); + current_op.version = op.version; + if (undo_stack_pos->get().chain_forward) { + while (true) { + ERR_BREAK(!undo_stack_pos->next()); + undo_stack_pos = undo_stack_pos->next(); + op = undo_stack_pos->get(); + _do_text_op(op, false); + current_op.version = op.version; + if (undo_stack_pos->get().chain_backward) { + break; + } } - } else { - cursor.x_ofs = 0; } - h_scroll->set_value(cursor.x_ofs); + _update_scrollbars(); + set_caret_line(undo_stack_pos->get().to_line, false); + set_caret_column(undo_stack_pos->get().to_column); + undo_stack_pos = undo_stack_pos->next(); update(); } -void TextEdit::center_viewport_to_cursor() { - // Move viewport so the cursor is in the center of the screen. - scrolling = false; - minimap_clicked = false; +void TextEdit::clear_undo_history() { + saved_version = 0; + current_op.type = TextOperation::TYPE_NONE; + undo_stack_pos = nullptr; + undo_stack.clear(); +} - if (is_line_hidden(cursor.line)) { - unfold_line(cursor.line); - } +bool TextEdit::is_insert_text_operation() const { + return (current_op.type == TextOperation::TYPE_INSERT); +} - set_line_as_center_visible(cursor.line, get_cursor_wrap_index()); - int visible_width = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding - cache.minimap_width; - if (v_scroll->is_visible_in_tree()) { - visible_width -= v_scroll->get_combined_minimum_size().width; +void TextEdit::tag_saved_version() { + saved_version = get_version(); +} + +uint32_t TextEdit::get_version() const { + return current_op.version; +} + +uint32_t TextEdit::get_saved_version() const { + return saved_version; +} + +/* Search */ +void TextEdit::set_search_text(const String &p_search_text) { + search_text = p_search_text; +} + +void TextEdit::set_search_flags(uint32_t p_flags) { + search_flags = p_flags; +} + +Point2i TextEdit::search(const String &p_key, uint32_t p_search_flags, int p_from_line, int p_from_column) const { + if (p_key.length() == 0) { + return Point2(-1, -1); } - visible_width -= 20; // Give it a little more space. + ERR_FAIL_INDEX_V(p_from_line, text.size(), Point2i(-1, -1)); + ERR_FAIL_INDEX_V(p_from_column, text[p_from_line].length() + 1, Point2i(-1, -1)); - if (is_wrap_enabled()) { - // Center x offset. + // Search through the whole document, but start by current line. - Vector2i cursor_pos; + int line = p_from_line; + int pos = -1; - // Get position of the start of caret. - if (ime_text.length() != 0 && ime_selection.x != 0) { - cursor_pos.x = get_column_x_offset_for_line(cursor.column + ime_selection.x, cursor.line); - } else { - cursor_pos.x = get_column_x_offset_for_line(cursor.column, cursor.line); + for (int i = 0; i < text.size() + 1; i++) { + if (line < 0) { + line = text.size() - 1; + } + if (line == text.size()) { + line = 0; } - // Get position of the end of caret. - if (ime_text.length() != 0) { - if (ime_selection.y != 0) { - cursor_pos.y = get_column_x_offset_for_line(cursor.column + ime_selection.x + ime_selection.y, cursor.line); + String text_line = text[line]; + int from_column = 0; + if (line == p_from_line) { + if (i == text.size()) { + // Wrapped. + + if (p_search_flags & SEARCH_BACKWARDS) { + from_column = text_line.length(); + } else { + from_column = 0; + } + } else { - cursor_pos.y = get_column_x_offset_for_line(cursor.column + ime_text.size(), cursor.line); + from_column = p_from_column; } + } else { - cursor_pos.y = cursor_pos.x; + if (p_search_flags & SEARCH_BACKWARDS) { + from_column = text_line.length() - 1; + } else { + from_column = 0; + } + } + + pos = -1; + + int pos_from = (p_search_flags & SEARCH_BACKWARDS) ? text_line.length() : 0; + int last_pos = -1; + + while (true) { + if (p_search_flags & SEARCH_BACKWARDS) { + while ((last_pos = (p_search_flags & SEARCH_MATCH_CASE) ? text_line.rfind(p_key, pos_from) : text_line.rfindn(p_key, pos_from)) != -1) { + if (last_pos <= from_column) { + pos = last_pos; + break; + } + pos_from = last_pos - p_key.length(); + if (pos_from < 0) { + break; + } + } + } else { + while ((last_pos = (p_search_flags & SEARCH_MATCH_CASE) ? text_line.find(p_key, pos_from) : text_line.findn(p_key, pos_from)) != -1) { + if (last_pos >= from_column) { + pos = last_pos; + break; + } + pos_from = last_pos + p_key.length(); + } + } + + bool is_match = true; + + if (pos != -1 && (p_search_flags & SEARCH_WHOLE_WORDS)) { + // Validate for whole words. + if (pos > 0 && _is_text_char(text_line[pos - 1])) { + is_match = false; + } else if (pos + p_key.length() < text_line.length() && _is_text_char(text_line[pos + p_key.length()])) { + is_match = false; + } + } + + if (pos_from == -1) { + pos = -1; + } + + if (is_match || last_pos == -1 || pos == -1) { + break; + } + + pos_from = (p_search_flags & SEARCH_BACKWARDS) ? pos - 1 : pos + 1; + pos = -1; } - if (MAX(cursor_pos.x, cursor_pos.y) > (cursor.x_ofs + visible_width)) { - cursor.x_ofs = MAX(cursor_pos.x, cursor_pos.y) - visible_width + 1; + if (pos != -1) { + break; } - if (MIN(cursor_pos.x, cursor_pos.y) < cursor.x_ofs) { - cursor.x_ofs = MIN(cursor_pos.x, cursor_pos.y); + if (p_search_flags & SEARCH_BACKWARDS) { + line--; + } else { + line++; } - } else { - cursor.x_ofs = 0; } - h_scroll->set_value(cursor.x_ofs); - - update(); + return (pos == -1) ? Point2i(-1, -1) : Point2i(pos, line); } -void TextEdit::update_cursor_wrap_offset() { - int first_vis_line = get_first_visible_line(); - if (line_wraps(first_vis_line)) { - cursor.wrap_ofs = MIN(cursor.wrap_ofs, times_line_wraps(first_vis_line)); - } else { - cursor.wrap_ofs = 0; +/* Mouse */ +Point2 TextEdit::get_local_mouse_pos() const { + Point2 mp = get_local_mouse_position(); + if (is_layout_rtl()) { + mp.x = get_size().width - mp.x; } - set_line_as_first_visible(cursor.line_ofs, cursor.wrap_ofs); + return mp; } -bool TextEdit::line_wraps(int line) const { - ERR_FAIL_INDEX_V(line, text.size(), 0); - if (!is_wrap_enabled()) { - return false; +String TextEdit::get_word_at_pos(const Vector2 &p_pos) const { + Point2i pos = get_line_column_at_pos(p_pos); + int row = pos.y; + int col = pos.x; + + String s = text[row]; + if (s.length() == 0) { + return ""; + } + int beg, end; + if (select_word(s, col, beg, end)) { + bool inside_quotes = false; + char32_t selected_quote = '\0'; + int qbegin = 0, qend = 0; + for (int i = 0; i < s.length(); i++) { + if (s[i] == '"' || s[i] == '\'') { + if (i == 0 || s[i - 1] != '\\') { + if (inside_quotes && selected_quote == s[i]) { + qend = i; + inside_quotes = false; + selected_quote = '\0'; + if (col >= qbegin && col <= qend) { + return s.substr(qbegin, qend - qbegin); + } + } else if (!inside_quotes) { + qbegin = i + 1; + inside_quotes = true; + selected_quote = s[i]; + } + } + } + } + + return s.substr(beg, end - beg); } - return text.get_line_wrap_amount(line) > 0; + + return String(); } -int TextEdit::times_line_wraps(int line) const { - ERR_FAIL_INDEX_V(line, text.size(), 0); +Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos) const { + float rows = p_pos.y; + rows -= style_normal->get_margin(SIDE_TOP); + rows /= get_line_height(); + rows += _get_v_scroll_offset(); + int first_vis_line = get_first_visible_line(); + int row = first_vis_line + Math::floor(rows); + int wrap_index = 0; - if (!line_wraps(line)) { - return 0; + if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE || _is_hiding_enabled()) { + Point2i f_ofs = get_next_visible_line_index_offset_from(first_vis_line, caret.wrap_ofs, rows + (1 * SGN(rows))); + wrap_index = f_ofs.y; + if (rows < 0) { + row = first_vis_line - (f_ofs.x - 1); + } else { + row = first_vis_line + (f_ofs.x - 1); + } + } + + if (row < 0) { + row = 0; } - return text.get_line_wrap_amount(line); + int col = 0; + + if (row >= text.size()) { + row = text.size() - 1; + col = text[row].size(); + } else { + int colx = p_pos.x - (style_normal->get_margin(SIDE_LEFT) + gutters_width + gutter_padding); + colx += caret.x_ofs; + col = _get_char_pos_for_line(colx, row, wrap_index); + if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && wrap_index < get_line_wrap_count(row)) { + // Move back one if we are at the end of the row. + Vector<String> rows2 = get_line_wrapped_text(row); + int row_end_col = 0; + for (int i = 0; i < wrap_index + 1; i++) { + row_end_col += rows2[i].length(); + } + if (col >= row_end_col) { + col -= 1; + } + } + + RID text_rid = text.get_line_data(row)->get_line_rid(wrap_index); + if (is_layout_rtl()) { + colx = TS->shaped_text_get_size(text_rid).x - colx; + } + col = TS->shaped_text_hit_test_position(text_rid, colx); + } + + return Point2i(col, row); } -Vector<String> TextEdit::get_wrap_rows_text(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), Vector<String>()); +int TextEdit::get_minimap_line_at_pos(const Point2i &p_pos) const { + float rows = p_pos.y; + rows -= style_normal->get_margin(SIDE_TOP); + rows /= (minimap_char_size.y + minimap_line_spacing); + rows += _get_v_scroll_offset(); - Vector<String> lines; - if (!line_wraps(p_line)) { - lines.push_back(text[p_line]); - return lines; + // calculate visible lines + int minimap_visible_lines = get_minimap_visible_lines(); + int visible_rows = get_visible_line_count() + 1; + int first_visible_line = get_first_visible_line() - 1; + int draw_amount = visible_rows + (smooth_scroll_enabled ? 1 : 0); + draw_amount += get_line_wrap_count(first_visible_line + 1); + int minimap_line_height = (minimap_char_size.y + minimap_line_spacing); + + // calculate viewport size and y offset + int viewport_height = (draw_amount - 1) * minimap_line_height; + int control_height = _get_control_height() - viewport_height; + int viewport_offset_y = round(get_scroll_pos_for_line(first_visible_line + 1) * control_height) / ((v_scroll->get_max() <= minimap_visible_lines) ? (minimap_visible_lines - draw_amount) : (v_scroll->get_max() - draw_amount)); + + // calculate the first line. + int num_lines_before = round((viewport_offset_y) / minimap_line_height); + int minimap_line = (v_scroll->get_max() <= minimap_visible_lines) ? -1 : first_visible_line; + if (first_visible_line > 0 && minimap_line >= 0) { + minimap_line -= get_next_visible_line_index_offset_from(first_visible_line, 0, -num_lines_before).x; + minimap_line -= (minimap_line > 0 && smooth_scroll_enabled ? 1 : 0); + } else { + minimap_line = 0; } - const String &line_text = text[p_line]; - Vector<Vector2i> line_ranges = text.get_line_wrap_ranges(p_line); - for (int i = 0; i < line_ranges.size(); i++) { - lines.push_back(line_text.substr(line_ranges[i].x, line_ranges[i].y - line_ranges[i].x)); + int row = minimap_line + Math::floor(rows); + if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE || _is_hiding_enabled()) { + int f_ofs = get_next_visible_line_index_offset_from(minimap_line, caret.wrap_ofs, rows + (1 * SGN(rows))).x - 1; + if (rows < 0) { + row = minimap_line - f_ofs; + } else { + row = minimap_line + f_ofs; + } } - return lines; + if (row < 0) { + row = 0; + } + + if (row >= text.size()) { + row = text.size() - 1; + } + + return row; } -int TextEdit::get_cursor_wrap_index() const { - return get_line_wrap_index_at_col(cursor.line, cursor.column); +bool TextEdit::is_dragging_cursor() const { + return dragging_selection || dragging_minimap; } -int TextEdit::get_line_wrap_index_at_col(int p_line, int p_column) const { - ERR_FAIL_INDEX_V(p_line, text.size(), 0); +/* Caret */ +void TextEdit::set_caret_type(CaretType p_type) { + caret_type = p_type; + update(); +} - if (!line_wraps(p_line)) { - return 0; - } +TextEdit::CaretType TextEdit::get_caret_type() const { + return caret_type; +} - // Loop through wraps in the line text until we get to the column. - int wrap_index = 0; - int col = 0; - Vector<String> rows = get_wrap_rows_text(p_line); - for (int i = 0; i < rows.size(); i++) { - wrap_index = i; - String s = rows[wrap_index]; - col += s.length(); - if (col > p_column) { - break; +void TextEdit::set_caret_blink_enabled(const bool p_enabled) { + caret_blink_enabled = p_enabled; + + if (has_focus()) { + if (p_enabled) { + caret_blink_timer->start(); + } else { + caret_blink_timer->stop(); } } - return wrap_index; + draw_caret = true; } -void TextEdit::set_mid_grapheme_caret_enabled(const bool p_enabled) { - mid_grapheme_caret_enabled = p_enabled; +bool TextEdit::is_caret_blink_enabled() const { + return caret_blink_enabled; } -bool TextEdit::get_mid_grapheme_caret_enabled() const { - return mid_grapheme_caret_enabled; +float TextEdit::get_caret_blink_speed() const { + return caret_blink_timer->get_wait_time(); } -void TextEdit::cursor_set_column(int p_col, bool p_adjust_viewport) { - if (p_col < 0) { - p_col = 0; - } +void TextEdit::set_caret_blink_speed(const float p_speed) { + ERR_FAIL_COND(p_speed <= 0); + caret_blink_timer->set_wait_time(p_speed); +} - cursor.column = p_col; - if (cursor.column > get_line(cursor.line).length()) { - cursor.column = get_line(cursor.line).length(); - } +void TextEdit::set_move_caret_on_right_click_enabled(const bool p_enable) { + move_caret_on_right_click = p_enable; +} - cursor.last_fit_x = get_column_x_offset_for_line(cursor.column, cursor.line); +bool TextEdit::is_move_caret_on_right_click_enabled() const { + return move_caret_on_right_click; +} - if (p_adjust_viewport) { - adjust_viewport_to_cursor(); - } +void TextEdit::set_caret_mid_grapheme_enabled(const bool p_enabled) { + caret_mid_grapheme_enabled = p_enabled; +} - if (!cursor_changed_dirty) { - if (is_inside_tree()) { - MessageQueue::get_singleton()->push_call(this, "_cursor_changed_emit"); - } - cursor_changed_dirty = true; - } +bool TextEdit::is_caret_mid_grapheme_enabled() const { + return caret_mid_grapheme_enabled; +} + +bool TextEdit::is_caret_visible() const { + return caret.visible; +} + +Point2 TextEdit::get_caret_draw_pos() const { + return caret.draw_pos; } -void TextEdit::cursor_set_line(int p_row, bool p_adjust_viewport, bool p_can_be_hidden, int p_wrap_index) { - if (setting_row) { +void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_hidden, int p_wrap_index) { + if (setting_caret_line) { return; } - setting_row = true; - if (p_row < 0) { - p_row = 0; + setting_caret_line = true; + if (p_line < 0) { + p_line = 0; } - if (p_row >= text.size()) { - p_row = text.size() - 1; + if (p_line >= text.size()) { + p_line = text.size() - 1; } if (!p_can_be_hidden) { - if (is_line_hidden(CLAMP(p_row, 0, text.size() - 1))) { - int move_down = num_lines_from(p_row, 1) - 1; - if (p_row + move_down <= text.size() - 1 && !is_line_hidden(p_row + move_down)) { - p_row += move_down; + if (_is_line_hidden(CLAMP(p_line, 0, text.size() - 1))) { + int move_down = get_next_visible_line_offset_from(p_line, 1) - 1; + if (p_line + move_down <= text.size() - 1 && !_is_line_hidden(p_line + move_down)) { + p_line += move_down; } else { - int move_up = num_lines_from(p_row, -1) - 1; - if (p_row - move_up > 0 && !is_line_hidden(p_row - move_up)) { - p_row -= move_up; + int move_up = get_next_visible_line_offset_from(p_line, -1) - 1; + if (p_line - move_up > 0 && !_is_line_hidden(p_line - move_up)) { + p_line -= move_up; } else { - WARN_PRINT(("Cursor set to hidden line " + itos(p_row) + " and there are no nonhidden lines.")); + WARN_PRINT(("Caret set to hidden line " + itos(p_line) + " and there are no nonhidden lines.")); } } } } - cursor.line = p_row; + caret.line = p_line; - int n_col = get_char_pos_for_line(cursor.last_fit_x, p_row, p_wrap_index); - if (n_col != 0 && is_wrap_enabled() && p_wrap_index < times_line_wraps(p_row)) { - Vector<String> rows = get_wrap_rows_text(p_row); + int n_col = _get_char_pos_for_line(caret.last_fit_x, p_line, p_wrap_index); + if (n_col != 0 && get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE && p_wrap_index < get_line_wrap_count(p_line)) { + Vector<String> rows = get_line_wrapped_text(p_line); int row_end_col = 0; for (int i = 0; i < p_wrap_index + 1; i++) { row_end_col += rows[i].length(); @@ -4277,76 +3547,87 @@ void TextEdit::cursor_set_line(int p_row, bool p_adjust_viewport, bool p_can_be_ n_col -= 1; } } - cursor.column = n_col; + caret.column = n_col; if (p_adjust_viewport) { - adjust_viewport_to_cursor(); + adjust_viewport_to_caret(); } - setting_row = false; + setting_caret_line = false; - if (!cursor_changed_dirty) { + if (!caret_pos_dirty) { if (is_inside_tree()) { - MessageQueue::get_singleton()->push_call(this, "_cursor_changed_emit"); + MessageQueue::get_singleton()->push_call(this, "_emit_caret_changed"); } - cursor_changed_dirty = true; + caret_pos_dirty = true; } } -int TextEdit::cursor_get_column() const { - return cursor.column; +int TextEdit::get_caret_line() const { + return caret.line; } -int TextEdit::cursor_get_line() const { - return cursor.line; -} +void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport) { + if (p_col < 0) { + p_col = 0; + } -bool TextEdit::cursor_get_blink_enabled() const { - return caret_blink_enabled; -} + caret.column = p_col; + if (caret.column > get_line(caret.line).length()) { + caret.column = get_line(caret.line).length(); + } -void TextEdit::cursor_set_blink_enabled(const bool p_enabled) { - caret_blink_enabled = p_enabled; + caret.last_fit_x = _get_column_x_offset_for_line(caret.column, caret.line); - if (has_focus()) { - if (p_enabled) { - caret_blink_timer->start(); - } else { - caret_blink_timer->stop(); - } + if (p_adjust_viewport) { + adjust_viewport_to_caret(); } - draw_caret = true; + if (!caret_pos_dirty) { + if (is_inside_tree()) { + MessageQueue::get_singleton()->push_call(this, "_emit_caret_changed"); + } + caret_pos_dirty = true; + } } -float TextEdit::cursor_get_blink_speed() const { - return caret_blink_timer->get_wait_time(); +int TextEdit::get_caret_column() const { + return caret.column; } -void TextEdit::cursor_set_blink_speed(const float p_speed) { - ERR_FAIL_COND(p_speed <= 0); - caret_blink_timer->set_wait_time(p_speed); +int TextEdit::get_caret_wrap_index() const { + return get_line_wrap_index_at_column(caret.line, caret.column); } -void TextEdit::cursor_set_block_mode(const bool p_enable) { - block_caret = p_enable; - update(); +String TextEdit::get_word_under_caret() const { + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].x <= caret.column && words[i].y > caret.column) { + return text[caret.line].substr(words[i].x, words[i].y - words[i].x); + } + } + return ""; } -bool TextEdit::cursor_is_block_mode() const { - return block_caret; +/* Selection. */ +void TextEdit::set_selecting_enabled(const bool p_enabled) { + selecting_enabled = p_enabled; + + if (!selecting_enabled) { + deselect(); + } } -void TextEdit::set_right_click_moves_caret(bool p_enable) { - right_click_moves_caret = p_enable; +bool TextEdit::is_selecting_enabled() const { + return selecting_enabled; } -bool TextEdit::is_right_click_moving_caret() const { - return right_click_moves_caret; +void TextEdit::set_override_selected_font_color(bool p_override_selected_font_color) { + override_selected_font_color = p_override_selected_font_color; } -TextEdit::SelectionMode TextEdit::get_selection_mode() const { - return selection.selecting_mode; +bool TextEdit::is_overriding_selected_font_color() const { + return override_selected_font_color; } void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column) { @@ -4361,508 +3642,544 @@ void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column } } -int TextEdit::get_selection_line() const { - return selection.selecting_line; -}; - -int TextEdit::get_selection_column() const { - return selection.selecting_column; -}; - -void TextEdit::_v_scroll_input() { - scrolling = false; - minimap_clicked = false; +TextEdit::SelectionMode TextEdit::get_selection_mode() const { + return selection.selecting_mode; } -void TextEdit::_scroll_moved(double p_to_val) { - if (updating_scrolls) { +void TextEdit::select_all() { + if (!selecting_enabled) { return; } - if (h_scroll->is_visible_in_tree()) { - cursor.x_ofs = h_scroll->get_value(); - } - if (v_scroll->is_visible_in_tree()) { - // Set line ofs and wrap ofs. - int v_scroll_i = floor(get_v_scroll()); - int sc = 0; - int n_line; - for (n_line = 0; n_line < text.size(); n_line++) { - if (!is_line_hidden(n_line)) { - sc++; - sc += times_line_wraps(n_line); - if (sc > v_scroll_i) { - break; - } - } - } - n_line = MIN(n_line, text.size() - 1); - int line_wrap_amount = times_line_wraps(n_line); - int wi = line_wrap_amount - (sc - v_scroll_i - 1); - wi = CLAMP(wi, 0, line_wrap_amount); - - cursor.line_ofs = n_line; - cursor.wrap_ofs = wi; + if (text.size() == 1 && text[0].length() == 0) { + return; } + selection.active = true; + selection.from_line = 0; + selection.from_column = 0; + selection.selecting_line = 0; + selection.selecting_column = 0; + selection.to_line = text.size() - 1; + selection.to_column = text[selection.to_line].length(); + selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; + selection.shiftclick_left = true; + set_caret_line(selection.to_line, false); + set_caret_column(selection.to_column, false); update(); } -int TextEdit::get_row_height() const { - int height = cache.font->get_height(cache.font_size); - for (int i = 0; i < text.size(); i++) { - for (int j = 0; j <= text.get_line_wrap_amount(i); j++) { - height = MAX(height, text.get_line_height(i, j)); - } +void TextEdit::select_word_under_caret() { + if (!selecting_enabled) { + return; } - return height + cache.line_spacing; -} - -int TextEdit::get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) const { - ERR_FAIL_INDEX_V(p_line, text.size(), 0); - p_wrap_index = MIN(p_wrap_index, text.get_line_data(p_line)->get_line_count() - 1); - RID text_rid = text.get_line_data(p_line)->get_line_rid(p_wrap_index); - if (is_layout_rtl()) { - p_px = TS->shaped_text_get_size(text_rid).x - p_px; + if (text.size() == 1 && text[0].length() == 0) { + return; } - return TS->shaped_text_hit_test_position(text_rid, p_px); -} -int TextEdit::get_column_x_offset_for_line(int p_char, int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), 0); + if (selection.active) { + /* Allow toggling selection by pressing the shortcut a second time. */ + /* This is also usable as a general-purpose "deselect" shortcut after */ + /* selecting anything. */ + deselect(); + return; + } - int row = 0; - Vector<Vector2i> rows2 = text.get_line_wrap_ranges(p_line); - for (int i = 0; i < rows2.size(); i++) { - if ((p_char >= rows2[i].x) && (p_char < rows2[i].y)) { - row = i; + int begin = 0; + int end = 0; + const Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].x <= caret.column && words[i].y >= caret.column) { + begin = words[i].x; + end = words[i].y; break; } } - Rect2 l_caret, t_caret; - TextServer::Direction l_dir, t_dir; - RID text_rid = text.get_line_data(p_line)->get_line_rid(row); - TS->shaped_text_get_carets(text_rid, cursor.column, l_caret, l_dir, t_caret, t_dir); - if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { - return l_caret.position.x; - } else { - return t_caret.position.x; - } + select(caret.line, begin, caret.line, end); + /* Move the caret to the end of the word for easier editing. */ + set_caret_column(end, false); } -void TextEdit::insert_text_at_cursor(const String &p_text) { - if (selection.active) { - cursor_set_line(selection.from_line, false); - cursor_set_column(selection.from_column); - - _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - selection.active = false; - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; +void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { + if (!selecting_enabled) { + return; } - _insert_text_at_cursor(p_text); - update(); -} - -Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { - if (highlighted_word != String()) { - return CURSOR_POINTING_HAND; + if (p_from_line < 0) { + p_from_line = 0; + } else if (p_from_line >= text.size()) { + p_from_line = text.size() - 1; + } + if (p_from_column >= text[p_from_line].length()) { + p_from_column = text[p_from_line].length(); + } + if (p_from_column < 0) { + p_from_column = 0; } - if ((completion_active && completion_rect.has_point(p_pos)) || (is_readonly() && (!is_selecting_enabled() || text.size() == 0))) { - return CURSOR_ARROW; + if (p_to_line < 0) { + p_to_line = 0; + } else if (p_to_line >= text.size()) { + p_to_line = text.size() - 1; + } + if (p_to_column >= text[p_to_line].length()) { + p_to_column = text[p_to_line].length(); + } + if (p_to_column < 0) { + p_to_column = 0; } - int row, col; - _get_mouse_pos(p_pos, row, col); + selection.from_line = p_from_line; + selection.from_column = p_from_column; + selection.to_line = p_to_line; + selection.to_column = p_to_column; - int left_margin = cache.style_normal->get_margin(SIDE_LEFT); - int gutter = left_margin + gutters_width; - if (p_pos.x < gutter) { - for (int i = 0; i < gutters.size(); i++) { - if (!gutters[i].draw) { - continue; - } + selection.active = true; - if (p_pos.x > left_margin && p_pos.x <= (left_margin + gutters[i].width) - 3) { - if (gutters[i].clickable || is_line_gutter_clickable(row, i)) { - return CURSOR_POINTING_HAND; - } - } - left_margin += gutters[i].width; + if (selection.from_line == selection.to_line) { + if (selection.from_column == selection.to_column) { + selection.active = false; + + } else if (selection.from_column > selection.to_column) { + selection.shiftclick_left = false; + SWAP(selection.from_column, selection.to_column); + } else { + selection.shiftclick_left = true; } - return CURSOR_ARROW; + } else if (selection.from_line > selection.to_line) { + selection.shiftclick_left = false; + SWAP(selection.from_line, selection.to_line); + SWAP(selection.from_column, selection.to_column); + } else { + selection.shiftclick_left = true; } - int xmargin_end = get_size().width - cache.style_normal->get_margin(SIDE_RIGHT); - if (draw_minimap && p_pos.x > xmargin_end - minimap_width && p_pos.x <= xmargin_end) { - return CURSOR_ARROW; - } + update(); +} - // EOL fold icon. - if (is_folded(row)) { - gutter += gutter_padding + text.get_line_width(row) - cursor.x_ofs; - if (p_pos.x > gutter - 3 && p_pos.x <= gutter + cache.folded_eol_icon->get_width() + 3) { - return CURSOR_POINTING_HAND; - } +bool TextEdit::has_selection() const { + return selection.active; +} + +String TextEdit::get_selected_text() const { + if (!selection.active) { + return ""; } - return get_default_cursor_shape(); + return _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); } -void TextEdit::set_text(String p_text) { - setting_text = true; - if (!undo_enabled) { - _clear(); - _insert_text_at_cursor(p_text); - } +int TextEdit::get_selection_line() const { + return selection.selecting_line; +} - if (undo_enabled) { - cursor_set_line(0); - cursor_set_column(0); +int TextEdit::get_selection_column() const { + return selection.selecting_column; +} - begin_complex_operation(); - _remove_text(0, 0, MAX(0, get_line_count() - 1), MAX(get_line(MAX(get_line_count() - 1, 0)).size() - 1, 0)); - _insert_text_at_cursor(p_text); - end_complex_operation(); - selection.active = false; - } +int TextEdit::get_selection_from_line() const { + ERR_FAIL_COND_V(!selection.active, -1); + return selection.from_line; +} - cursor_set_line(0); - cursor_set_column(0); +int TextEdit::get_selection_from_column() const { + ERR_FAIL_COND_V(!selection.active, -1); + return selection.from_column; +} +int TextEdit::get_selection_to_line() const { + ERR_FAIL_COND_V(!selection.active, -1); + return selection.to_line; +} + +int TextEdit::get_selection_to_column() const { + ERR_FAIL_COND_V(!selection.active, -1); + return selection.to_column; +} + +void TextEdit::deselect() { + selection.active = false; update(); - setting_text = false; } -String TextEdit::get_text() { - String longthing; - int len = text.size(); - for (int i = 0; i < len; i++) { - longthing += text[i]; - if (i != len - 1) { - longthing += "\n"; - } +void TextEdit::delete_selection() { + if (!has_selection()) { + return; } - return longthing; + selection.active = false; + selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; + _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); + set_caret_line(selection.from_line, false, false); + set_caret_column(selection.from_column); + update(); } -void TextEdit::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { - if (st_parser != p_parser) { - st_parser = p_parser; - for (int i = 0; i < text.size(); i++) { - text.set(i, text[i], structured_text_parser(st_parser, st_args, text[i])); - } - update(); +/* line wrapping. */ +void TextEdit::set_line_wrapping_mode(LineWrappingMode p_wrapping_mode) { + if (line_wrapping_mode != p_wrapping_mode) { + line_wrapping_mode = p_wrapping_mode; + _update_wrap_at_column(true); } } -Control::StructuredTextParser TextEdit::get_structured_text_bidi_override() const { - return st_parser; +TextEdit::LineWrappingMode TextEdit::get_line_wrapping_mode() const { + return line_wrapping_mode; } -void TextEdit::set_structured_text_bidi_override_options(Array p_args) { - st_args = p_args; - for (int i = 0; i < text.size(); i++) { - text.set(i, text[i], structured_text_parser(st_parser, st_args, text[i])); +bool TextEdit::is_line_wrapped(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + if (get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) { + return false; } - update(); + return text.get_line_wrap_amount(p_line) > 0; } -Array TextEdit::get_structured_text_bidi_override_options() const { - return st_args; +int TextEdit::get_line_wrap_count(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + + if (!is_line_wrapped(p_line)) { + return 0; + } + + return text.get_line_wrap_amount(p_line); } -void TextEdit::set_text_direction(Control::TextDirection p_text_direction) { - ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); - if (text_direction != p_text_direction) { - text_direction = p_text_direction; - if (text_direction != TEXT_DIRECTION_AUTO && text_direction != TEXT_DIRECTION_INHERITED) { - input_direction = text_direction; - } - TextServer::Direction dir; - if (text_direction == Control::TEXT_DIRECTION_INHERITED) { - dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; - } else { - dir = (TextServer::Direction)text_direction; +int TextEdit::get_line_wrap_index_at_column(int p_line, int p_column) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + ERR_FAIL_COND_V(p_column < 0, 0); + ERR_FAIL_COND_V(p_column > text[p_line].length(), 0); + + if (!is_line_wrapped(p_line)) { + return 0; + } + + /* Loop through wraps in the line text until we get to the column. */ + int wrap_index = 0; + int col = 0; + Vector<String> lines = get_line_wrapped_text(p_line); + for (int i = 0; i < lines.size(); i++) { + wrap_index = i; + String s = lines[wrap_index]; + col += s.length(); + if (col > p_column) { + break; } - text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); - text.invalidate_all(); + } + return wrap_index; +} - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_AUTO), text_direction == TEXT_DIRECTION_AUTO); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); - update(); +Vector<String> TextEdit::get_line_wrapped_text(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), Vector<String>()); + + Vector<String> lines; + if (!is_line_wrapped(p_line)) { + lines.push_back(text[p_line]); + return lines; } + + const String &line_text = text[p_line]; + Vector<Vector2i> line_ranges = text.get_line_wrap_ranges(p_line); + for (int i = 0; i < line_ranges.size(); i++) { + lines.push_back(line_text.substr(line_ranges[i].x, line_ranges[i].y - line_ranges[i].x)); + } + + return lines; } -Control::TextDirection TextEdit::get_text_direction() const { - return text_direction; +/* Viewport */ +// Scrolling. +void TextEdit::set_smooth_scroll_enabled(const bool p_enable) { + v_scroll->set_smooth_scroll_enabled(p_enable); + smooth_scroll_enabled = p_enable; } -void TextEdit::clear_opentype_features() { - opentype_features.clear(); - text.set_font_features(opentype_features); - text.invalidate_all(); +bool TextEdit::is_smooth_scroll_enabled() const { + return smooth_scroll_enabled; +} + +void TextEdit::set_scroll_past_end_of_file_enabled(const bool p_enabled) { + scroll_past_end_of_file_enabled = p_enabled; update(); } -void TextEdit::set_opentype_feature(const String &p_name, int p_value) { - int32_t tag = TS->name_to_tag(p_name); - if (!opentype_features.has(tag) || (int)opentype_features[tag] != p_value) { - opentype_features[tag] = p_value; - text.set_font_features(opentype_features); - text.invalidate_all(); - update(); - } +bool TextEdit::is_scroll_past_end_of_file_enabled() const { + return scroll_past_end_of_file_enabled; } -int TextEdit::get_opentype_feature(const String &p_name) const { - int32_t tag = TS->name_to_tag(p_name); - if (!opentype_features.has(tag)) { - return -1; +void TextEdit::set_v_scroll(double p_scroll) { + v_scroll->set_value(p_scroll); + int max_v_scroll = v_scroll->get_max() - v_scroll->get_page(); + if (p_scroll >= max_v_scroll - 1.0) { + _scroll_moved(v_scroll->get_value()); } - return opentype_features[tag]; } -void TextEdit::set_language(const String &p_language) { - if (language != p_language) { - language = p_language; - TextServer::Direction dir; - if (text_direction == Control::TEXT_DIRECTION_INHERITED) { - dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; - } else { - dir = (TextServer::Direction)text_direction; - } - text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); - text.invalidate_all(); - update(); +double TextEdit::get_v_scroll() const { + return v_scroll->get_value(); +} + +void TextEdit::set_h_scroll(int p_scroll) { + if (p_scroll < 0) { + p_scroll = 0; } + h_scroll->set_value(p_scroll); } -String TextEdit::get_language() const { - return language; +int TextEdit::get_h_scroll() const { + return h_scroll->get_value(); } -void TextEdit::set_draw_control_chars(bool p_draw_control_chars) { - if (draw_control_chars != p_draw_control_chars) { - draw_control_chars = p_draw_control_chars; - menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); - text.set_draw_control_chars(draw_control_chars); - text.invalidate_all(); - update(); - } +void TextEdit::set_v_scroll_speed(float p_speed) { + v_scroll_speed = p_speed; } -bool TextEdit::get_draw_control_chars() const { - return draw_control_chars; +float TextEdit::get_v_scroll_speed() const { + return v_scroll_speed; } -String TextEdit::get_text_for_lookup_completion() { - int row, col; - Point2i mp = _get_local_mouse_pos(); - _get_mouse_pos(mp, row, col); +double TextEdit::get_scroll_pos_for_line(int p_line, int p_wrap_index) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + ERR_FAIL_COND_V(p_wrap_index < 0, 0); + ERR_FAIL_COND_V(p_wrap_index > get_line_wrap_count(p_line), 0); - String longthing; - int len = text.size(); - for (int i = 0; i < len; i++) { - if (i == row) { - longthing += text[i].substr(0, col); - longthing += String::chr(0xFFFF); // Not unicode, represents the cursor. - longthing += text[i].substr(col, text[i].size()); - } else { - longthing += text[i]; - } + if (get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE && !_is_hiding_enabled()) { + return p_line; + } - if (i != len - 1) { - longthing += "\n"; + // Count the number of visible lines up to this line. + double new_line_scroll_pos = 0.0; + int to = CLAMP(p_line, 0, text.size() - 1); + for (int i = 0; i < to; i++) { + if (!text.is_hidden(i)) { + new_line_scroll_pos++; + new_line_scroll_pos += get_line_wrap_count(i); } } - - return longthing; + new_line_scroll_pos += p_wrap_index; + return new_line_scroll_pos; } -String TextEdit::get_text_for_completion() { - String longthing; - int len = text.size(); - for (int i = 0; i < len; i++) { - if (i == cursor.line) { - longthing += text[i].substr(0, cursor.column); - longthing += String::chr(0xFFFF); // Not unicode, represents the cursor. - longthing += text[i].substr(cursor.column, text[i].size()); - } else { - longthing += text[i]; - } - - if (i != len - 1) { - longthing += "\n"; - } - } +// Visible lines. +void TextEdit::set_line_as_first_visible(int p_line, int p_wrap_index) { + ERR_FAIL_INDEX(p_line, text.size()); + ERR_FAIL_COND(p_wrap_index < 0); + ERR_FAIL_COND(p_wrap_index > get_line_wrap_count(p_line)); + set_v_scroll(get_scroll_pos_for_line(p_line, p_wrap_index)); +} - return longthing; -}; +int TextEdit::get_first_visible_line() const { + return CLAMP(caret.line_ofs, 0, text.size() - 1); +} -String TextEdit::get_line(int line) const { - if (line < 0 || line >= text.size()) { - return ""; - } +void TextEdit::set_line_as_center_visible(int p_line, int p_wrap_index) { + ERR_FAIL_INDEX(p_line, text.size()); + ERR_FAIL_COND(p_wrap_index < 0); + ERR_FAIL_COND(p_wrap_index > get_line_wrap_count(p_line)); - return text[line]; -}; + int visible_rows = get_visible_line_count(); + Point2i next_line = get_next_visible_line_index_offset_from(p_line, p_wrap_index, -visible_rows / 2); + int first_line = p_line - next_line.x + 1; -void TextEdit::_clear() { - clear_undo_history(); - text.clear(); - cursor.column = 0; - cursor.line = 0; - cursor.x_ofs = 0; - cursor.line_ofs = 0; - cursor.wrap_ofs = 0; - cursor.last_fit_x = 0; - selection.active = false; + set_v_scroll(get_scroll_pos_for_line(first_line, next_line.y)); } -void TextEdit::clear() { - setting_text = true; - _clear(); - setting_text = false; -}; - -void TextEdit::set_readonly(bool p_readonly) { - if (readonly == p_readonly) { - return; - } +void TextEdit::set_line_as_last_visible(int p_line, int p_wrap_index) { + ERR_FAIL_INDEX(p_line, text.size()); + ERR_FAIL_COND(p_wrap_index < 0); + ERR_FAIL_COND(p_wrap_index > get_line_wrap_count(p_line)); - readonly = p_readonly; - _generate_context_menu(); + Point2i next_line = get_next_visible_line_index_offset_from(p_line, p_wrap_index, -get_visible_line_count() - 1); + int first_line = p_line - next_line.x + 1; - update(); + set_v_scroll(get_scroll_pos_for_line(first_line, next_line.y) + _get_visible_lines_offset()); } -bool TextEdit::is_readonly() const { - return readonly; +int TextEdit::get_last_full_visible_line() const { + int first_vis_line = get_first_visible_line(); + int last_vis_line = 0; + last_vis_line = first_vis_line + get_next_visible_line_index_offset_from(first_vis_line, caret.wrap_ofs, get_visible_line_count()).x - 1; + last_vis_line = CLAMP(last_vis_line, 0, text.size() - 1); + return last_vis_line; } -void TextEdit::set_wrap_enabled(bool p_wrap_enabled) { - if (wrap_enabled != p_wrap_enabled) { - wrap_enabled = p_wrap_enabled; - _update_wrap_at(true); - } +int TextEdit::get_last_full_visible_line_wrap_index() const { + int first_vis_line = get_first_visible_line(); + return get_next_visible_line_index_offset_from(first_vis_line, caret.wrap_ofs, get_visible_line_count()).y; } -bool TextEdit::is_wrap_enabled() const { - return wrap_enabled; +int TextEdit::get_visible_line_count() const { + return _get_control_height() / get_line_height(); } -void TextEdit::set_max_chars(int p_max_chars) { - max_chars = p_max_chars; -} +int TextEdit::get_total_visible_line_count() const { + /* Returns the total number of (lines + wraped - hidden). */ + if (!_is_hiding_enabled() && get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) { + return text.size(); + } -int TextEdit::get_max_chars() const { - return max_chars; + int total_rows = 0; + for (int i = 0; i < text.size(); i++) { + if (!text.is_hidden(i)) { + total_rows++; + total_rows += get_line_wrap_count(i); + } + } + return total_rows; } -void TextEdit::_reset_caret_blink_timer() { - if (caret_blink_enabled) { - draw_caret = true; - if (has_focus()) { - caret_blink_timer->stop(); - caret_blink_timer->start(); - update(); +// Auto adjust +void TextEdit::adjust_viewport_to_caret() { + // Make sure Caret is visible on the screen. + scrolling = false; + minimap_clicked = false; + + int cur_line = caret.line; + int cur_wrap = get_caret_wrap_index(); + + int first_vis_line = get_first_visible_line(); + int first_vis_wrap = caret.wrap_ofs; + int last_vis_line = get_last_full_visible_line(); + int last_vis_wrap = get_last_full_visible_line_wrap_index(); + + if (cur_line < first_vis_line || (cur_line == first_vis_line && cur_wrap < first_vis_wrap)) { + // Caret is above screen. + set_line_as_first_visible(cur_line, cur_wrap); + } else if (cur_line > last_vis_line || (cur_line == last_vis_line && cur_wrap > last_vis_wrap)) { + // Caret is below screen. + set_line_as_last_visible(cur_line, cur_wrap); + } + + int visible_width = get_size().width - style_normal->get_minimum_size().width - gutters_width - gutter_padding; + if (draw_minimap) { + visible_width -= minimap_width; + } + if (v_scroll->is_visible_in_tree()) { + visible_width -= v_scroll->get_combined_minimum_size().width; + } + visible_width -= 20; // Give it a little more space. + + if (get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) { + // Adjust x offset. + Vector2i caret_pos; + + // Get position of the start of caret. + if (ime_text.length() != 0 && ime_selection.x != 0) { + caret_pos.x = _get_column_x_offset_for_line(caret.column + ime_selection.x, caret.line); + } else { + caret_pos.x = _get_column_x_offset_for_line(caret.column, caret.line); } + + // Get position of the end of caret. + if (ime_text.length() != 0) { + if (ime_selection.y != 0) { + caret_pos.y = _get_column_x_offset_for_line(caret.column + ime_selection.x + ime_selection.y, caret.line); + } else { + caret_pos.y = _get_column_x_offset_for_line(caret.column + ime_text.size(), caret.line); + } + } else { + caret_pos.y = caret_pos.x; + } + + if (MAX(caret_pos.x, caret_pos.y) > (caret.x_ofs + visible_width)) { + caret.x_ofs = MAX(caret_pos.x, caret_pos.y) - visible_width + 1; + } + + if (MIN(caret_pos.x, caret_pos.y) < caret.x_ofs) { + caret.x_ofs = MIN(caret_pos.x, caret_pos.y); + } + } else { + caret.x_ofs = 0; } + h_scroll->set_value(caret.x_ofs); + + update(); } -void TextEdit::_toggle_draw_caret() { - draw_caret = !draw_caret; - if (is_visible_in_tree() && has_focus() && window_has_focus) { - update(); +void TextEdit::center_viewport_to_caret() { + // Move viewport so the caret is in the center of the screen. + scrolling = false; + minimap_clicked = false; + + set_line_as_center_visible(caret.line, get_caret_wrap_index()); + int visible_width = get_size().width - style_normal->get_minimum_size().width - gutters_width - gutter_padding; + if (draw_minimap) { + visible_width -= minimap_width; } -} + if (v_scroll->is_visible_in_tree()) { + visible_width -= v_scroll->get_combined_minimum_size().width; + } + visible_width -= 20; // Give it a little more space. -void TextEdit::_update_caches() { - cache.style_normal = get_theme_stylebox("normal"); - cache.style_focus = get_theme_stylebox("focus"); - cache.style_readonly = get_theme_stylebox("read_only"); - cache.completion_background_color = get_theme_color("completion_background_color"); - cache.completion_selected_color = get_theme_color("completion_selected_color"); - cache.completion_existing_color = get_theme_color("completion_existing_color"); - cache.completion_font_color = get_theme_color("completion_font_color"); - cache.font = get_theme_font("font"); - cache.font_size = get_theme_font_size("font_size"); - cache.outline_color = get_theme_color("font_outline_color"); - cache.outline_size = get_theme_constant("outline_size"); - cache.caret_color = get_theme_color("caret_color"); - cache.caret_background_color = get_theme_color("caret_background_color"); - cache.font_color = get_theme_color("font_color"); - cache.font_selected_color = get_theme_color("font_selected_color"); - cache.font_readonly_color = get_theme_color("font_readonly_color"); - cache.selection_color = get_theme_color("selection_color"); - cache.current_line_color = get_theme_color("current_line_color"); - cache.line_length_guideline_color = get_theme_color("line_length_guideline_color"); - cache.code_folding_color = get_theme_color("code_folding_color"); - cache.brace_mismatch_color = get_theme_color("brace_mismatch_color"); - cache.word_highlighted_color = get_theme_color("word_highlighted_color"); - cache.search_result_color = get_theme_color("search_result_color"); - cache.search_result_border_color = get_theme_color("search_result_border_color"); - cache.background_color = get_theme_color("background_color"); -#ifdef TOOLS_ENABLED - cache.line_spacing = get_theme_constant("line_spacing") * EDSCALE; -#else - cache.line_spacing = get_theme_constant("line_spacing"); -#endif - cache.tab_icon = get_theme_icon("tab"); - cache.space_icon = get_theme_icon("space"); - cache.folded_eol_icon = get_theme_icon("GuiEllipsis", "EditorIcons"); + if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE) { + // Center x offset. - TextServer::Direction dir; - if (text_direction == Control::TEXT_DIRECTION_INHERITED) { - dir = is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + Vector2i caret_pos; + + // Get position of the start of caret. + if (ime_text.length() != 0 && ime_selection.x != 0) { + caret_pos.x = _get_column_x_offset_for_line(caret.column + ime_selection.x, caret.line); + } else { + caret_pos.x = _get_column_x_offset_for_line(caret.column, caret.line); + } + + // Get position of the end of caret. + if (ime_text.length() != 0) { + if (ime_selection.y != 0) { + caret_pos.y = _get_column_x_offset_for_line(caret.column + ime_selection.x + ime_selection.y, caret.line); + } else { + caret_pos.y = _get_column_x_offset_for_line(caret.column + ime_text.size(), caret.line); + } + } else { + caret_pos.y = caret_pos.x; + } + + if (MAX(caret_pos.x, caret_pos.y) > (caret.x_ofs + visible_width)) { + caret.x_ofs = MAX(caret_pos.x, caret_pos.y) - visible_width + 1; + } + + if (MIN(caret_pos.x, caret_pos.y) < caret.x_ofs) { + caret.x_ofs = MIN(caret_pos.x, caret_pos.y); + } } else { - dir = (TextServer::Direction)text_direction; + caret.x_ofs = 0; } - text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); - text.set_font_features(opentype_features); - text.set_draw_control_chars(draw_control_chars); - text.set_font(cache.font); - text.set_font_size(cache.font_size); - text.invalidate_all(); + h_scroll->set_value(caret.x_ofs); - if (syntax_highlighter.is_valid()) { - syntax_highlighter->set_text_edit(this); + update(); +} + +/* Minimap */ +void TextEdit::set_draw_minimap(bool p_draw) { + if (draw_minimap != p_draw) { + draw_minimap = p_draw; + _update_wrap_at_column(); } + update(); } -/* Syntax Highlighting. */ -Ref<SyntaxHighlighter> TextEdit::get_syntax_highlighter() { - return syntax_highlighter; +bool TextEdit::is_drawing_minimap() const { + return draw_minimap; } -void TextEdit::set_syntax_highlighter(Ref<SyntaxHighlighter> p_syntax_highlighter) { - syntax_highlighter = p_syntax_highlighter; - if (syntax_highlighter.is_valid()) { - syntax_highlighter->set_text_edit(this); +void TextEdit::set_minimap_width(int p_minimap_width) { + if (minimap_width != p_minimap_width) { + minimap_width = p_minimap_width; + _update_wrap_at_column(); } update(); } -/* Gutters. */ -void TextEdit::_update_gutter_width() { - gutters_width = 0; - for (int i = 0; i < gutters.size(); i++) { - if (gutters[i].draw) { - gutters_width += gutters[i].width; - } - } - if (gutters_width > 0) { - gutter_padding = 2; - } - update(); +int TextEdit::get_minimap_width() const { + return minimap_width; } +int TextEdit::get_minimap_visible_lines() const { + return _get_control_height() / (minimap_char_size.y + minimap_line_spacing); +} + +/* Gutters. */ void TextEdit::add_gutter(int p_at) { if (p_at < 0 || p_at > gutters.size()) { gutters.push_back(GutterInfo()); @@ -4873,7 +4190,7 @@ void TextEdit::add_gutter(int p_at) { for (int i = 0; i < text.size() + 1; i++) { text.add_gutter(p_at); } - emit_signal("gutter_added"); + emit_signal(SNAME("gutter_added")); update(); } @@ -4885,7 +4202,7 @@ void TextEdit::remove_gutter(int p_gutter) { for (int i = 0; i < text.size() + 1; i++) { text.remove_gutter(p_gutter); } - emit_signal("gutter_removed"); + emit_signal(SNAME("gutter_removed")); update(); } @@ -4916,6 +4233,9 @@ TextEdit::GutterType TextEdit::get_gutter_type(int p_gutter) const { void TextEdit::set_gutter_width(int p_gutter, int p_width) { ERR_FAIL_INDEX(p_gutter, gutters.size()); + if (gutters[p_gutter].width == p_width) { + return; + } gutters.write[p_gutter].width = p_width; _update_gutter_width(); } @@ -4925,8 +4245,15 @@ int TextEdit::get_gutter_width(int p_gutter) const { return gutters[p_gutter].width; } +int TextEdit::get_total_gutter_width() const { + return gutters_width + gutter_padding; +} + void TextEdit::set_gutter_draw(int p_gutter, bool p_draw) { ERR_FAIL_INDEX(p_gutter, gutters.size()); + if (gutters[p_gutter].draw == p_draw) { + return; + } gutters.write[p_gutter].draw = p_draw; _update_gutter_width(); } @@ -4957,6 +4284,39 @@ bool TextEdit::is_gutter_overwritable(int p_gutter) const { return gutters[p_gutter].overwritable; } +void TextEdit::merge_gutters(int p_from_line, int p_to_line) { + ERR_FAIL_INDEX(p_from_line, text.size()); + ERR_FAIL_INDEX(p_to_line, text.size()); + if (p_from_line == p_to_line) { + return; + } + + for (int i = 0; i < gutters.size(); i++) { + if (!gutters[i].overwritable) { + continue; + } + + if (text.get_line_gutter_text(p_from_line, i) != "") { + text.set_line_gutter_text(p_to_line, i, text.get_line_gutter_text(p_from_line, i)); + text.set_line_gutter_item_color(p_to_line, i, text.get_line_gutter_item_color(p_from_line, i)); + } + + if (text.get_line_gutter_icon(p_from_line, i).is_valid()) { + text.set_line_gutter_icon(p_to_line, i, text.get_line_gutter_icon(p_from_line, i)); + text.set_line_gutter_item_color(p_to_line, i, text.get_line_gutter_item_color(p_from_line, i)); + } + + if (text.get_line_gutter_metadata(p_from_line, i) != "") { + text.set_line_gutter_metadata(p_to_line, i, text.get_line_gutter_metadata(p_from_line, i)); + } + + if (text.is_line_gutter_clickable(p_from_line, i)) { + text.set_line_gutter_clickable(p_to_line, i, true); + } + } + update(); +} + void TextEdit::set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback) { ERR_FAIL_INDEX(p_gutter, gutters.size()); ERR_FAIL_NULL(p_object); @@ -4992,7 +4352,7 @@ String TextEdit::get_line_gutter_text(int p_line, int p_gutter) const { return text.get_line_gutter_text(p_line, p_gutter); } -void TextEdit::set_line_gutter_icon(int p_line, int p_gutter, Ref<Texture2D> p_icon) { +void TextEdit::set_line_gutter_icon(int p_line, int p_gutter, const Ref<Texture2D> &p_icon) { ERR_FAIL_INDEX(p_line, text.size()); ERR_FAIL_INDEX(p_gutter, gutters.size()); text.set_line_gutter_icon(p_line, p_gutter, p_icon); @@ -5012,7 +4372,7 @@ void TextEdit::set_line_gutter_item_color(int p_line, int p_gutter, const Color update(); } -Color TextEdit::get_line_gutter_item_color(int p_line, int p_gutter) { +Color TextEdit::get_line_gutter_item_color(int p_line, int p_gutter) const { ERR_FAIL_INDEX_V(p_line, text.size(), Color()); ERR_FAIL_INDEX_V(p_gutter, gutters.size(), Color()); return text.get_line_gutter_item_color(p_line, p_gutter); @@ -5037,773 +4397,836 @@ void TextEdit::set_line_background_color(int p_line, const Color &p_color) { update(); } -Color TextEdit::get_line_background_color(int p_line) { +Color TextEdit::get_line_background_color(int p_line) const { ERR_FAIL_INDEX_V(p_line, text.size(), Color()); return text.get_line_background_color(p_line); } -void TextEdit::add_keyword(const String &p_keyword) { - keywords.insert(p_keyword); +/* Syntax Highlighting. */ +void TextEdit::set_syntax_highlighter(Ref<SyntaxHighlighter> p_syntax_highlighter) { + syntax_highlighter = p_syntax_highlighter; + if (syntax_highlighter.is_valid()) { + syntax_highlighter->set_text_edit(this); + } + update(); +} + +Ref<SyntaxHighlighter> TextEdit::get_syntax_highlighter() const { + return syntax_highlighter; } -void TextEdit::clear_keywords() { - keywords.clear(); +/* Visual. */ +void TextEdit::set_highlight_current_line(bool p_enabled) { + highlight_current_line = p_enabled; + update(); } -void TextEdit::set_auto_indent(bool p_auto_indent) { - auto_indent = p_auto_indent; +bool TextEdit::is_highlight_current_line_enabled() const { + return highlight_current_line; } -void TextEdit::cut() { - if (readonly) { - return; - } +void TextEdit::set_highlight_all_occurrences(const bool p_enabled) { + highlight_all_occurrences = p_enabled; + update(); +} - if (!selection.active) { - String clipboard = text[cursor.line]; - DisplayServer::get_singleton()->clipboard_set(clipboard); - cursor_set_line(cursor.line); - cursor_set_column(0); +bool TextEdit::is_highlight_all_occurrences_enabled() const { + return highlight_all_occurrences; +} - if (cursor.line == 0 && get_line_count() > 1) { - _remove_text(cursor.line, 0, cursor.line + 1, 0); - } else { - _remove_text(cursor.line, 0, cursor.line, text[cursor.line].length()); - backspace_at_cursor(); - cursor_set_line(cursor.line + 1); +void TextEdit::set_draw_control_chars(bool p_draw_control_chars) { + if (draw_control_chars != p_draw_control_chars) { + draw_control_chars = p_draw_control_chars; + if (menu) { + menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); } - + text.set_draw_control_chars(draw_control_chars); + text.invalidate_all(); update(); - cut_copy_line = clipboard; + } +} - } else { - String clipboard = _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - DisplayServer::get_singleton()->clipboard_set(clipboard); +bool TextEdit::get_draw_control_chars() const { + return draw_control_chars; +} - _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - cursor_set_line(selection.from_line, false); // Set afterwards else it causes the view to be offset. - cursor_set_column(selection.from_column); +void TextEdit::set_draw_tabs(bool p_draw) { + draw_tabs = p_draw; + update(); +} - selection.active = false; - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; - update(); - cut_copy_line = ""; - } +bool TextEdit::is_drawing_tabs() const { + return draw_tabs; } -void TextEdit::copy() { - if (!selection.active) { - if (text[cursor.line].length() != 0) { - String clipboard = _base_get_text(cursor.line, 0, cursor.line, text[cursor.line].length()); - DisplayServer::get_singleton()->clipboard_set(clipboard); - cut_copy_line = clipboard; - } - } else { - String clipboard = _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - DisplayServer::get_singleton()->clipboard_set(clipboard); - cut_copy_line = ""; - } +void TextEdit::set_draw_spaces(bool p_draw) { + draw_spaces = p_draw; + update(); } -void TextEdit::paste() { - if (readonly) { - return; - } +bool TextEdit::is_drawing_spaces() const { + return draw_spaces; +} - String clipboard = DisplayServer::get_singleton()->clipboard_get(); +void TextEdit::_bind_methods() { + /*Internal. */ - begin_complex_operation(); - if (selection.active) { - selection.active = false; - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; - _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - cursor_set_line(selection.from_line, false); - cursor_set_column(selection.from_column); + ClassDB::bind_method(D_METHOD("_text_changed_emit"), &TextEdit::_text_changed_emit); - } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) { - cursor_set_column(0); - String ins = "\n"; - clipboard += ins; - } + /* Text */ + // Text properties + ClassDB::bind_method(D_METHOD("has_ime_text"), &TextEdit::has_ime_text); - _insert_text_at_cursor(clipboard); - end_complex_operation(); + ClassDB::bind_method(D_METHOD("set_editable", "enable"), &TextEdit::set_editable); + ClassDB::bind_method(D_METHOD("is_editable"), &TextEdit::is_editable); - update(); -} + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &TextEdit::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &TextEdit::get_text_direction); -void TextEdit::select_all() { - if (!selecting_enabled) { - return; - } + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &TextEdit::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &TextEdit::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &TextEdit::clear_opentype_features); - if (text.size() == 1 && text[0].length() == 0) { - return; - } - selection.active = true; - selection.from_line = 0; - selection.from_column = 0; - selection.selecting_line = 0; - selection.selecting_column = 0; - selection.to_line = text.size() - 1; - selection.to_column = text[selection.to_line].length(); - selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; - selection.shiftclick_left = true; - cursor_set_line(selection.to_line, false); - cursor_set_column(selection.to_column, false); - update(); -} + ClassDB::bind_method(D_METHOD("set_language", "language"), &TextEdit::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &TextEdit::get_language); -void TextEdit::select_word_under_caret() { - if (!selecting_enabled) { - return; - } + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &TextEdit::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &TextEdit::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &TextEdit::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &TextEdit::get_structured_text_bidi_override_options); - if (text.size() == 1 && text[0].length() == 0) { - return; - } + ClassDB::bind_method(D_METHOD("set_tab_size", "size"), &TextEdit::set_tab_size); + ClassDB::bind_method(D_METHOD("get_tab_size"), &TextEdit::get_tab_size); - if (selection.active) { - // Allow toggling selection by pressing the shortcut a second time. - // This is also usable as a general-purpose "deselect" shortcut after - // selecting anything. - deselect(); - return; - } + // User controls + ClassDB::bind_method(D_METHOD("set_overtype_mode_enabled", "enabled"), &TextEdit::set_overtype_mode_enabled); + ClassDB::bind_method(D_METHOD("is_overtype_mode_enabled"), &TextEdit::is_overtype_mode_enabled); - int begin = 0; - int end = 0; - const Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(cursor.line)->get_rid()); - for (int i = 0; i < words.size(); i++) { - if (words[i].x <= cursor.column && words[i].y >= cursor.column) { - begin = words[i].x; - end = words[i].y; - break; - } - } + ClassDB::bind_method(D_METHOD("set_context_menu_enabled", "enable"), &TextEdit::set_context_menu_enabled); + ClassDB::bind_method(D_METHOD("is_context_menu_enabled"), &TextEdit::is_context_menu_enabled); - select(cursor.line, begin, cursor.line, end); - // Move the cursor to the end of the word for easier editing. - cursor_set_column(end, false); -} + ClassDB::bind_method(D_METHOD("set_shortcut_keys_enabled", "enable"), &TextEdit::set_shortcut_keys_enabled); + ClassDB::bind_method(D_METHOD("is_shortcut_keys_enabled"), &TextEdit::is_shortcut_keys_enabled); -void TextEdit::deselect() { - selection.active = false; - update(); -} + ClassDB::bind_method(D_METHOD("set_virtual_keyboard_enabled", "enable"), &TextEdit::set_virtual_keyboard_enabled); + ClassDB::bind_method(D_METHOD("is_virtual_keyboard_enabled"), &TextEdit::is_virtual_keyboard_enabled); -void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { - if (!selecting_enabled) { - return; - } + // Text manipulation + ClassDB::bind_method(D_METHOD("clear"), &TextEdit::clear); - if (p_from_line < 0) { - p_from_line = 0; - } else if (p_from_line >= text.size()) { - p_from_line = text.size() - 1; - } - if (p_from_column >= text[p_from_line].length()) { - p_from_column = text[p_from_line].length(); - } - if (p_from_column < 0) { - p_from_column = 0; - } + ClassDB::bind_method(D_METHOD("set_text", "text"), &TextEdit::set_text); + ClassDB::bind_method(D_METHOD("get_text"), &TextEdit::get_text); + ClassDB::bind_method(D_METHOD("get_line_count"), &TextEdit::get_line_count); - if (p_to_line < 0) { - p_to_line = 0; - } else if (p_to_line >= text.size()) { - p_to_line = text.size() - 1; - } - if (p_to_column >= text[p_to_line].length()) { - p_to_column = text[p_to_line].length(); - } - if (p_to_column < 0) { - p_to_column = 0; - } + ClassDB::bind_method(D_METHOD("set_line", "line", "new_text"), &TextEdit::set_line); + ClassDB::bind_method(D_METHOD("get_line", "line"), &TextEdit::get_line); - selection.from_line = p_from_line; - selection.from_column = p_from_column; - selection.to_line = p_to_line; - selection.to_column = p_to_column; + ClassDB::bind_method(D_METHOD("get_line_width", "line", "wrap_index"), &TextEdit::get_line_width, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("get_line_height"), &TextEdit::get_line_height); - selection.active = true; + ClassDB::bind_method(D_METHOD("get_indent_level", "line"), &TextEdit::get_indent_level); + ClassDB::bind_method(D_METHOD("get_first_non_whitespace_column", "line"), &TextEdit::get_first_non_whitespace_column); - if (selection.from_line == selection.to_line) { - if (selection.from_column == selection.to_column) { - selection.active = false; + ClassDB::bind_method(D_METHOD("swap_lines", "from_line", "to_line"), &TextEdit::swap_lines); - } else if (selection.from_column > selection.to_column) { - selection.shiftclick_left = false; - SWAP(selection.from_column, selection.to_column); - } else { - selection.shiftclick_left = true; - } - } else if (selection.from_line > selection.to_line) { - selection.shiftclick_left = false; - SWAP(selection.from_line, selection.to_line); - SWAP(selection.from_column, selection.to_column); - } else { - selection.shiftclick_left = true; - } + ClassDB::bind_method(D_METHOD("insert_line_at", "line", "text"), &TextEdit::insert_line_at); + ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text"), &TextEdit::insert_text_at_caret); - update(); -} + ClassDB::bind_method(D_METHOD("remove_text", "from_line", "from_column", "to_line", "to_column"), &TextEdit::remove_text); -void TextEdit::swap_lines(int line1, int line2) { - String tmp = get_line(line1); - String tmp2 = get_line(line2); - set_line(line2, tmp); - set_line(line1, tmp2); -} + ClassDB::bind_method(D_METHOD("get_last_unhidden_line"), &TextEdit::get_last_unhidden_line); + ClassDB::bind_method(D_METHOD("get_next_visible_line_offset_from", "line", "visible_amount"), &TextEdit::get_next_visible_line_offset_from); + ClassDB::bind_method(D_METHOD("get_next_visible_line_index_offset_from", "line", "wrap_index", "visible_amount"), &TextEdit::get_next_visible_line_index_offset_from); -bool TextEdit::is_selection_active() const { - return selection.active; -} + // Overridable actions + ClassDB::bind_method(D_METHOD("backspace"), &TextEdit::backspace); -int TextEdit::get_selection_from_line() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.from_line; -} + ClassDB::bind_method(D_METHOD("cut"), &TextEdit::cut); + ClassDB::bind_method(D_METHOD("copy"), &TextEdit::copy); + ClassDB::bind_method(D_METHOD("paste"), &TextEdit::paste); -int TextEdit::get_selection_from_column() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.from_column; -} + GDVIRTUAL_BIND(_handle_unicode_input, "unicode_char") + GDVIRTUAL_BIND(_backspace) + GDVIRTUAL_BIND(_cut) + GDVIRTUAL_BIND(_copy) + GDVIRTUAL_BIND(_paste) -int TextEdit::get_selection_to_line() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.to_line; -} + // Context Menu + BIND_ENUM_CONSTANT(MENU_CUT); + BIND_ENUM_CONSTANT(MENU_COPY); + BIND_ENUM_CONSTANT(MENU_PASTE); + BIND_ENUM_CONSTANT(MENU_CLEAR); + BIND_ENUM_CONSTANT(MENU_SELECT_ALL); + BIND_ENUM_CONSTANT(MENU_UNDO); + BIND_ENUM_CONSTANT(MENU_REDO); + BIND_ENUM_CONSTANT(MENU_DIR_INHERITED); + BIND_ENUM_CONSTANT(MENU_DIR_AUTO); + BIND_ENUM_CONSTANT(MENU_DIR_LTR); + BIND_ENUM_CONSTANT(MENU_DIR_RTL); + BIND_ENUM_CONSTANT(MENU_DISPLAY_UCC); + BIND_ENUM_CONSTANT(MENU_INSERT_LRM); + BIND_ENUM_CONSTANT(MENU_INSERT_RLM); + BIND_ENUM_CONSTANT(MENU_INSERT_LRE); + BIND_ENUM_CONSTANT(MENU_INSERT_RLE); + BIND_ENUM_CONSTANT(MENU_INSERT_LRO); + BIND_ENUM_CONSTANT(MENU_INSERT_RLO); + BIND_ENUM_CONSTANT(MENU_INSERT_PDF); + BIND_ENUM_CONSTANT(MENU_INSERT_ALM); + BIND_ENUM_CONSTANT(MENU_INSERT_LRI); + BIND_ENUM_CONSTANT(MENU_INSERT_RLI); + BIND_ENUM_CONSTANT(MENU_INSERT_FSI); + BIND_ENUM_CONSTANT(MENU_INSERT_PDI); + BIND_ENUM_CONSTANT(MENU_INSERT_ZWJ); + BIND_ENUM_CONSTANT(MENU_INSERT_ZWNJ); + BIND_ENUM_CONSTANT(MENU_INSERT_WJ); + BIND_ENUM_CONSTANT(MENU_INSERT_SHY); + BIND_ENUM_CONSTANT(MENU_MAX); -int TextEdit::get_selection_to_column() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.to_column; -} + /* Versioning */ + ClassDB::bind_method(D_METHOD("begin_complex_operation"), &TextEdit::begin_complex_operation); + ClassDB::bind_method(D_METHOD("end_complex_operation"), &TextEdit::end_complex_operation); -String TextEdit::get_selection_text() const { - if (!selection.active) { - return ""; - } + ClassDB::bind_method(D_METHOD("has_undo"), &TextEdit::has_undo); + ClassDB::bind_method(D_METHOD("has_redo"), &TextEdit::has_redo); + ClassDB::bind_method(D_METHOD("undo"), &TextEdit::undo); + ClassDB::bind_method(D_METHOD("redo"), &TextEdit::redo); + ClassDB::bind_method(D_METHOD("clear_undo_history"), &TextEdit::clear_undo_history); - return _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); -} + ClassDB::bind_method(D_METHOD("tag_saved_version"), &TextEdit::tag_saved_version); -String TextEdit::get_word_under_cursor() const { - Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(cursor.line)->get_rid()); - for (int i = 0; i < words.size(); i++) { - if (words[i].x <= cursor.column && words[i].y > cursor.column) { - return text[cursor.line].substr(words[i].x, words[i].y - words[i].x); - } - } - return ""; -} + ClassDB::bind_method(D_METHOD("get_version"), &TextEdit::get_version); + ClassDB::bind_method(D_METHOD("get_saved_version"), &TextEdit::get_saved_version); -void TextEdit::set_search_text(const String &p_search_text) { - search_text = p_search_text; -} + /* Search */ + BIND_ENUM_CONSTANT(SEARCH_MATCH_CASE); + BIND_ENUM_CONSTANT(SEARCH_WHOLE_WORDS); + BIND_ENUM_CONSTANT(SEARCH_BACKWARDS); -void TextEdit::set_search_flags(uint32_t p_flags) { - search_flags = p_flags; -} + ClassDB::bind_method(D_METHOD("set_search_text", "search_text"), &TextEdit::set_search_text); + ClassDB::bind_method(D_METHOD("set_search_flags", "flags"), &TextEdit::set_search_flags); -void TextEdit::set_current_search_result(int line, int col) { - search_result_line = line; - search_result_col = col; - update(); -} + ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_colum"), &TextEdit::search); -void TextEdit::set_highlight_all_occurrences(const bool p_enabled) { - highlight_all_occurrences = p_enabled; - update(); -} + /* Tooltip */ + ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "object", "callback", "data"), &TextEdit::set_tooltip_request_func); -bool TextEdit::is_highlight_all_occurrences_enabled() const { - return highlight_all_occurrences; -} + /* Mouse */ + ClassDB::bind_method(D_METHOD("get_local_mouse_pos"), &TextEdit::get_local_mouse_pos); -int TextEdit::_get_column_pos_of_word(const String &p_key, const String &p_search, uint32_t p_search_flags, int p_from_column) { - int col = -1; + ClassDB::bind_method(D_METHOD("get_word_at_pos", "position"), &TextEdit::get_word_at_pos); - if (p_key.length() > 0 && p_search.length() > 0) { - if (p_from_column < 0 || p_from_column > p_search.length()) { - p_from_column = 0; - } + ClassDB::bind_method(D_METHOD("get_line_column_at_pos", "position"), &TextEdit::get_line_column_at_pos); + ClassDB::bind_method(D_METHOD("get_minimap_line_at_pos", "position"), &TextEdit::get_minimap_line_at_pos); - while (col == -1 && p_from_column <= p_search.length()) { - if (p_search_flags & SEARCH_MATCH_CASE) { - col = p_search.find(p_key, p_from_column); - } else { - col = p_search.findn(p_key, p_from_column); - } + ClassDB::bind_method(D_METHOD("is_dragging_cursor"), &TextEdit::is_dragging_cursor); - // Whole words only. - if (col != -1 && p_search_flags & SEARCH_WHOLE_WORDS) { - p_from_column = col; + /* Caret. */ + BIND_ENUM_CONSTANT(CARET_TYPE_LINE); + BIND_ENUM_CONSTANT(CARET_TYPE_BLOCK); - if (col > 0 && _is_text_char(p_search[col - 1])) { - col = -1; - } else if ((col + p_key.length()) < p_search.length() && _is_text_char(p_search[col + p_key.length()])) { - col = -1; - } - } + // internal. + ClassDB::bind_method(D_METHOD("_emit_caret_changed"), &TextEdit::_emit_caret_changed); - p_from_column += 1; - } - } - return col; -} + ClassDB::bind_method(D_METHOD("set_caret_type", "type"), &TextEdit::set_caret_type); + ClassDB::bind_method(D_METHOD("get_caret_type"), &TextEdit::get_caret_type); -Dictionary TextEdit::_search_bind(const String &p_key, uint32_t p_search_flags, int p_from_line, int p_from_column) const { - int col, line; - if (search(p_key, p_search_flags, p_from_line, p_from_column, line, col)) { - Dictionary result; - result["line"] = line; - result["column"] = col; - return result; + ClassDB::bind_method(D_METHOD("set_caret_blink_enabled", "enable"), &TextEdit::set_caret_blink_enabled); + ClassDB::bind_method(D_METHOD("is_caret_blink_enabled"), &TextEdit::is_caret_blink_enabled); - } else { - return Dictionary(); - } -} + ClassDB::bind_method(D_METHOD("set_caret_blink_speed", "blink_speed"), &TextEdit::set_caret_blink_speed); + ClassDB::bind_method(D_METHOD("get_caret_blink_speed"), &TextEdit::get_caret_blink_speed); -bool TextEdit::search(const String &p_key, uint32_t p_search_flags, int p_from_line, int p_from_column, int &r_line, int &r_column) const { - if (p_key.length() == 0) { - return false; - } - ERR_FAIL_INDEX_V(p_from_line, text.size(), false); - ERR_FAIL_INDEX_V(p_from_column, text[p_from_line].length() + 1, false); + ClassDB::bind_method(D_METHOD("set_move_caret_on_right_click_enabled", "enable"), &TextEdit::set_move_caret_on_right_click_enabled); + ClassDB::bind_method(D_METHOD("is_move_caret_on_right_click_enabled"), &TextEdit::is_move_caret_on_right_click_enabled); - // Search through the whole document, but start by current line. + ClassDB::bind_method(D_METHOD("set_caret_mid_grapheme_enabled", "enabled"), &TextEdit::set_caret_mid_grapheme_enabled); + ClassDB::bind_method(D_METHOD("is_caret_mid_grapheme_enabled"), &TextEdit::is_caret_mid_grapheme_enabled); - int line = p_from_line; - int pos = -1; + ClassDB::bind_method(D_METHOD("is_caret_visible"), &TextEdit::is_caret_visible); + ClassDB::bind_method(D_METHOD("get_caret_draw_pos"), &TextEdit::get_caret_draw_pos); - for (int i = 0; i < text.size() + 1; i++) { - if (line < 0) { - line = text.size() - 1; - } - if (line == text.size()) { - line = 0; - } + ClassDB::bind_method(D_METHOD("set_caret_line", "line", "adjust_viewport", "can_be_hidden", "wrap_index"), &TextEdit::set_caret_line, DEFVAL(true), DEFVAL(true), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_caret_line"), &TextEdit::get_caret_line); - String text_line = text[line]; - int from_column = 0; - if (line == p_from_line) { - if (i == text.size()) { - // Wrapped. + ClassDB::bind_method(D_METHOD("set_caret_column", "column", "adjust_viewport"), &TextEdit::set_caret_column, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_caret_column"), &TextEdit::get_caret_column); - if (p_search_flags & SEARCH_BACKWARDS) { - from_column = text_line.length(); - } else { - from_column = 0; - } + ClassDB::bind_method(D_METHOD("get_caret_wrap_index"), &TextEdit::get_caret_wrap_index); - } else { - from_column = p_from_column; - } + ClassDB::bind_method(D_METHOD("get_word_under_caret"), &TextEdit::get_word_under_caret); - } else { - if (p_search_flags & SEARCH_BACKWARDS) { - from_column = text_line.length() - 1; - } else { - from_column = 0; - } - } + /* Selection. */ + BIND_ENUM_CONSTANT(SELECTION_MODE_NONE); + BIND_ENUM_CONSTANT(SELECTION_MODE_SHIFT); + BIND_ENUM_CONSTANT(SELECTION_MODE_POINTER); + BIND_ENUM_CONSTANT(SELECTION_MODE_WORD); + BIND_ENUM_CONSTANT(SELECTION_MODE_LINE); - pos = -1; + ClassDB::bind_method(D_METHOD("set_selecting_enabled", "enable"), &TextEdit::set_selecting_enabled); + ClassDB::bind_method(D_METHOD("is_selecting_enabled"), &TextEdit::is_selecting_enabled); - int pos_from = (p_search_flags & SEARCH_BACKWARDS) ? text_line.length() : 0; - int last_pos = -1; + ClassDB::bind_method(D_METHOD("set_override_selected_font_color", "override"), &TextEdit::set_override_selected_font_color); + ClassDB::bind_method(D_METHOD("is_overriding_selected_font_color"), &TextEdit::is_overriding_selected_font_color); - while (true) { - if (p_search_flags & SEARCH_BACKWARDS) { - while ((last_pos = (p_search_flags & SEARCH_MATCH_CASE) ? text_line.rfind(p_key, pos_from) : text_line.rfindn(p_key, pos_from)) != -1) { - if (last_pos <= from_column) { - pos = last_pos; - break; - } - pos_from = last_pos - p_key.length(); - if (pos_from < 0) { - break; - } - } - } else { - while ((last_pos = (p_search_flags & SEARCH_MATCH_CASE) ? text_line.find(p_key, pos_from) : text_line.findn(p_key, pos_from)) != -1) { - if (last_pos >= from_column) { - pos = last_pos; - break; - } - pos_from = last_pos + p_key.length(); - } - } + ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("get_selection_mode"), &TextEdit::get_selection_mode); - bool is_match = true; + ClassDB::bind_method(D_METHOD("select_all"), &TextEdit::select_all); + ClassDB::bind_method(D_METHOD("select_word_under_caret"), &TextEdit::select_word_under_caret); + ClassDB::bind_method(D_METHOD("select", "from_line", "from_column", "to_line", "to_column"), &TextEdit::select); - if (pos != -1 && (p_search_flags & SEARCH_WHOLE_WORDS)) { - // Validate for whole words. - if (pos > 0 && _is_text_char(text_line[pos - 1])) { - is_match = false; - } else if (pos + p_key.length() < text_line.length() && _is_text_char(text_line[pos + p_key.length()])) { - is_match = false; - } - } + ClassDB::bind_method(D_METHOD("has_selection"), &TextEdit::has_selection); - if (pos_from == -1) { - pos = -1; - } + ClassDB::bind_method(D_METHOD("get_selected_text"), &TextEdit::get_selected_text); - if (is_match || last_pos == -1 || pos == -1) { - break; - } + ClassDB::bind_method(D_METHOD("get_selection_line"), &TextEdit::get_selection_line); + ClassDB::bind_method(D_METHOD("get_selection_column"), &TextEdit::get_selection_column); - pos_from = (p_search_flags & SEARCH_BACKWARDS) ? pos - 1 : pos + 1; - pos = -1; - } + ClassDB::bind_method(D_METHOD("get_selection_from_line"), &TextEdit::get_selection_from_line); + ClassDB::bind_method(D_METHOD("get_selection_from_column"), &TextEdit::get_selection_from_column); + ClassDB::bind_method(D_METHOD("get_selection_to_line"), &TextEdit::get_selection_to_line); + ClassDB::bind_method(D_METHOD("get_selection_to_column"), &TextEdit::get_selection_to_column); - if (pos != -1) { - break; - } + ClassDB::bind_method(D_METHOD("deselect"), &TextEdit::deselect); + ClassDB::bind_method(D_METHOD("delete_selection"), &TextEdit::delete_selection); - if (p_search_flags & SEARCH_BACKWARDS) { - line--; + /* line wrapping. */ + BIND_ENUM_CONSTANT(LINE_WRAPPING_NONE); + BIND_ENUM_CONSTANT(LINE_WRAPPING_BOUNDARY); + + // internal. + ClassDB::bind_method(D_METHOD("_update_wrap_at_column", "force"), &TextEdit::_update_wrap_at_column, DEFVAL(false)); + + ClassDB::bind_method(D_METHOD("set_line_wrapping_mode", "mode"), &TextEdit::set_line_wrapping_mode); + ClassDB::bind_method(D_METHOD("get_line_wrapping_mode"), &TextEdit::get_line_wrapping_mode); + + ClassDB::bind_method(D_METHOD("is_line_wrapped", "line"), &TextEdit::is_line_wrapped); + ClassDB::bind_method(D_METHOD("get_line_wrap_count", "line"), &TextEdit::get_line_wrap_count); + ClassDB::bind_method(D_METHOD("get_line_wrap_index_at_column", "line", "column"), &TextEdit::get_line_wrap_index_at_column); + + ClassDB::bind_method(D_METHOD("get_line_wrapped_text", "line"), &TextEdit::get_line_wrapped_text); + + /* Viewport. */ + // Scolling. + ClassDB::bind_method(D_METHOD("set_smooth_scroll_enable", "enable"), &TextEdit::set_smooth_scroll_enabled); + ClassDB::bind_method(D_METHOD("is_smooth_scroll_enabled"), &TextEdit::is_smooth_scroll_enabled); + + ClassDB::bind_method(D_METHOD("set_v_scroll", "value"), &TextEdit::set_v_scroll); + ClassDB::bind_method(D_METHOD("get_v_scroll"), &TextEdit::get_v_scroll); + + ClassDB::bind_method(D_METHOD("set_h_scroll", "value"), &TextEdit::set_h_scroll); + ClassDB::bind_method(D_METHOD("get_h_scroll"), &TextEdit::get_h_scroll); + + ClassDB::bind_method(D_METHOD("set_scroll_past_end_of_file_enabled", "enable"), &TextEdit::set_scroll_past_end_of_file_enabled); + ClassDB::bind_method(D_METHOD("is_scroll_past_end_of_file_enabled"), &TextEdit::is_scroll_past_end_of_file_enabled); + + ClassDB::bind_method(D_METHOD("set_v_scroll_speed", "speed"), &TextEdit::set_v_scroll_speed); + ClassDB::bind_method(D_METHOD("get_v_scroll_speed"), &TextEdit::get_v_scroll_speed); + + ClassDB::bind_method(D_METHOD("get_scroll_pos_for_line", "line", "wrap_index"), &TextEdit::get_scroll_pos_for_line, DEFVAL(0)); + + // Visible lines. + ClassDB::bind_method(D_METHOD("set_line_as_first_visible", "line", "wrap_index"), &TextEdit::set_line_as_first_visible, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_first_visible_line"), &TextEdit::get_first_visible_line); + + ClassDB::bind_method(D_METHOD("set_line_as_center_visible", "line", "wrap_index"), &TextEdit::set_line_as_center_visible, DEFVAL(0)); + + ClassDB::bind_method(D_METHOD("set_line_as_last_visible", "line", "wrap_index"), &TextEdit::set_line_as_last_visible, DEFVAL(0)); + ClassDB::bind_method(D_METHOD("get_last_full_visible_line"), &TextEdit::get_last_full_visible_line); + ClassDB::bind_method(D_METHOD("get_last_full_visible_line_wrap_index"), &TextEdit::get_last_full_visible_line_wrap_index); + + ClassDB::bind_method(D_METHOD("get_visible_line_count"), &TextEdit::get_visible_line_count); + ClassDB::bind_method(D_METHOD("get_total_visible_line_count"), &TextEdit::get_total_visible_line_count); + + // Auto adjust + ClassDB::bind_method(D_METHOD("adjust_viewport_to_caret"), &TextEdit::adjust_viewport_to_caret); + ClassDB::bind_method(D_METHOD("center_viewport_to_caret"), &TextEdit::center_viewport_to_caret); + + // Minimap + ClassDB::bind_method(D_METHOD("draw_minimap", "draw"), &TextEdit::set_draw_minimap); + ClassDB::bind_method(D_METHOD("is_drawing_minimap"), &TextEdit::is_drawing_minimap); + + ClassDB::bind_method(D_METHOD("set_minimap_width", "width"), &TextEdit::set_minimap_width); + ClassDB::bind_method(D_METHOD("get_minimap_width"), &TextEdit::get_minimap_width); + + ClassDB::bind_method(D_METHOD("get_minimap_visible_lines"), &TextEdit::get_minimap_visible_lines); + + /* Gutters. */ + BIND_ENUM_CONSTANT(GUTTER_TYPE_STRING); + BIND_ENUM_CONSTANT(GUTTER_TYPE_ICON); + BIND_ENUM_CONSTANT(GUTTER_TYPE_CUSTOM); + + ClassDB::bind_method(D_METHOD("add_gutter", "at"), &TextEdit::add_gutter, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("remove_gutter", "gutter"), &TextEdit::remove_gutter); + ClassDB::bind_method(D_METHOD("get_gutter_count"), &TextEdit::get_gutter_count); + ClassDB::bind_method(D_METHOD("set_gutter_name", "gutter", "name"), &TextEdit::set_gutter_name); + ClassDB::bind_method(D_METHOD("get_gutter_name", "gutter"), &TextEdit::get_gutter_name); + ClassDB::bind_method(D_METHOD("set_gutter_type", "gutter", "type"), &TextEdit::set_gutter_type); + ClassDB::bind_method(D_METHOD("get_gutter_type", "gutter"), &TextEdit::get_gutter_type); + ClassDB::bind_method(D_METHOD("set_gutter_width", "gutter", "width"), &TextEdit::set_gutter_width); + ClassDB::bind_method(D_METHOD("get_gutter_width", "gutter"), &TextEdit::get_gutter_width); + ClassDB::bind_method(D_METHOD("set_gutter_draw", "gutter", "draw"), &TextEdit::set_gutter_draw); + ClassDB::bind_method(D_METHOD("is_gutter_drawn", "gutter"), &TextEdit::is_gutter_drawn); + ClassDB::bind_method(D_METHOD("set_gutter_clickable", "gutter", "clickable"), &TextEdit::set_gutter_clickable); + ClassDB::bind_method(D_METHOD("is_gutter_clickable", "gutter"), &TextEdit::is_gutter_clickable); + ClassDB::bind_method(D_METHOD("set_gutter_overwritable", "gutter", "overwritable"), &TextEdit::set_gutter_overwritable); + ClassDB::bind_method(D_METHOD("is_gutter_overwritable", "gutter"), &TextEdit::is_gutter_overwritable); + ClassDB::bind_method(D_METHOD("merge_gutters", "from_line", "to_line"), &TextEdit::merge_gutters); + ClassDB::bind_method(D_METHOD("set_gutter_custom_draw", "column", "object", "callback"), &TextEdit::set_gutter_custom_draw); + ClassDB::bind_method(D_METHOD("get_total_gutter_width"), &TextEdit::get_total_gutter_width); + + // Line gutters. + ClassDB::bind_method(D_METHOD("set_line_gutter_metadata", "line", "gutter", "metadata"), &TextEdit::set_line_gutter_metadata); + ClassDB::bind_method(D_METHOD("get_line_gutter_metadata", "line", "gutter"), &TextEdit::get_line_gutter_metadata); + ClassDB::bind_method(D_METHOD("set_line_gutter_text", "line", "gutter", "text"), &TextEdit::set_line_gutter_text); + ClassDB::bind_method(D_METHOD("get_line_gutter_text", "line", "gutter"), &TextEdit::get_line_gutter_text); + ClassDB::bind_method(D_METHOD("set_line_gutter_icon", "line", "gutter", "icon"), &TextEdit::set_line_gutter_icon); + ClassDB::bind_method(D_METHOD("get_line_gutter_icon", "line", "gutter"), &TextEdit::get_line_gutter_icon); + ClassDB::bind_method(D_METHOD("set_line_gutter_item_color", "line", "gutter", "color"), &TextEdit::set_line_gutter_item_color); + ClassDB::bind_method(D_METHOD("get_line_gutter_item_color", "line", "gutter"), &TextEdit::get_line_gutter_item_color); + ClassDB::bind_method(D_METHOD("set_line_gutter_clickable", "line", "gutter", "clickable"), &TextEdit::set_line_gutter_clickable); + ClassDB::bind_method(D_METHOD("is_line_gutter_clickable", "line", "gutter"), &TextEdit::is_line_gutter_clickable); + + // Line style + ClassDB::bind_method(D_METHOD("set_line_background_color", "line", "color"), &TextEdit::set_line_background_color); + ClassDB::bind_method(D_METHOD("get_line_background_color", "line"), &TextEdit::get_line_background_color); + + /* Syntax Highlighting. */ + ClassDB::bind_method(D_METHOD("set_syntax_highlighter", "syntax_highlighter"), &TextEdit::set_syntax_highlighter); + ClassDB::bind_method(D_METHOD("get_syntax_highlighter"), &TextEdit::get_syntax_highlighter); + + /* Visual. */ + ClassDB::bind_method(D_METHOD("set_highlight_current_line", "enabled"), &TextEdit::set_highlight_current_line); + ClassDB::bind_method(D_METHOD("is_highlight_current_line_enabled"), &TextEdit::is_highlight_current_line_enabled); + + ClassDB::bind_method(D_METHOD("set_highlight_all_occurrences", "enable"), &TextEdit::set_highlight_all_occurrences); + ClassDB::bind_method(D_METHOD("is_highlight_all_occurrences_enabled"), &TextEdit::is_highlight_all_occurrences_enabled); + + ClassDB::bind_method(D_METHOD("get_draw_control_chars"), &TextEdit::get_draw_control_chars); + ClassDB::bind_method(D_METHOD("set_draw_control_chars", "enable"), &TextEdit::set_draw_control_chars); + + ClassDB::bind_method(D_METHOD("set_draw_tabs"), &TextEdit::set_draw_tabs); + ClassDB::bind_method(D_METHOD("is_drawing_tabs"), &TextEdit::is_drawing_tabs); + + ClassDB::bind_method(D_METHOD("set_draw_spaces"), &TextEdit::set_draw_spaces); + ClassDB::bind_method(D_METHOD("is_drawing_spaces"), &TextEdit::is_drawing_spaces); + + ClassDB::bind_method(D_METHOD("get_menu"), &TextEdit::get_menu); + ClassDB::bind_method(D_METHOD("is_menu_visible"), &TextEdit::is_menu_visible); + ClassDB::bind_method(D_METHOD("menu_option", "option"), &TextEdit::menu_option); + + /* Inspector */ + ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editable"), "set_editable", "is_editable"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "context_menu_enabled"), "set_context_menu_enabled", "is_context_menu_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shortcut_keys_enabled"), "set_shortcut_keys_enabled", "is_shortcut_keys_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selecting_enabled"), "set_selecting_enabled", "is_selecting_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "virtual_keyboard_enabled"), "set_virtual_keyboard_enabled", "is_virtual_keyboard_enabled"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "wrap_mode", PROPERTY_HINT_ENUM, "None,Boundary"), "set_line_wrapping_mode", "get_line_wrapping_mode"); + + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "override_selected_font_color"), "set_override_selected_font_color", "is_overriding_selected_font_color"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "highlight_all_occurrences"), "set_highlight_all_occurrences", "is_highlight_all_occurrences_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "highlight_current_line"), "set_highlight_current_line", "is_highlight_current_line_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_control_chars"), "set_draw_control_chars", "get_draw_control_chars"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_tabs"), "set_draw_tabs", "is_drawing_tabs"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_spaces"), "set_draw_spaces", "is_drawing_spaces"); + + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "syntax_highlighter", PROPERTY_HINT_RESOURCE_TYPE, "SyntaxHighlighter", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_DO_NOT_SHARE_ON_DUPLICATE), "set_syntax_highlighter", "get_syntax_highlighter"); + + ADD_GROUP("Scroll", "scroll_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_smooth"), "set_smooth_scroll_enable", "is_smooth_scroll_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "scroll_v_scroll_speed"), "set_v_scroll_speed", "get_v_scroll_speed"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_past_end_of_file"), "set_scroll_past_end_of_file_enabled", "is_scroll_past_end_of_file_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "scroll_vertical"), "set_v_scroll", "get_v_scroll"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "scroll_horizontal"), "set_h_scroll", "get_h_scroll"); + + ADD_GROUP("Minimap", "minimap_"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "minimap_draw"), "draw_minimap", "is_drawing_minimap"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "minimap_width"), "set_minimap_width", "get_minimap_width"); + + ADD_GROUP("Caret", "caret_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "caret_type", PROPERTY_HINT_ENUM, "Line,Block"), "set_caret_type", "get_caret_type"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_blink"), "set_caret_blink_enabled", "is_caret_blink_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "caret_blink_speed", PROPERTY_HINT_RANGE, "0.1,10,0.01"), "set_caret_blink_speed", "get_caret_blink_speed"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_move_on_right_click"), "set_move_caret_on_right_click_enabled", "is_move_caret_on_right_click_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_mid_grapheme"), "set_caret_mid_grapheme_enabled", "is_caret_mid_grapheme_enabled"); + + ADD_GROUP("Structured Text", "structured_text_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override"); + ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "structured_text_bidi_override_options"), "set_structured_text_bidi_override_options", "get_structured_text_bidi_override_options"); + + /* Signals */ + /* Core. */ + ADD_SIGNAL(MethodInfo("text_set")); + ADD_SIGNAL(MethodInfo("text_changed")); + ADD_SIGNAL(MethodInfo("lines_edited_from", PropertyInfo(Variant::INT, "from_line"), PropertyInfo(Variant::INT, "to_line"))); + + /* Caret. */ + ADD_SIGNAL(MethodInfo("caret_changed")); + + /* Gutters. */ + ADD_SIGNAL(MethodInfo("gutter_clicked", PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::INT, "gutter"))); + ADD_SIGNAL(MethodInfo("gutter_added")); + ADD_SIGNAL(MethodInfo("gutter_removed")); + + /* Settings. */ + GLOBAL_DEF("gui/timers/text_edit_idle_detect_sec", 3); + ProjectSettings::get_singleton()->set_custom_property_info("gui/timers/text_edit_idle_detect_sec", PropertyInfo(Variant::FLOAT, "gui/timers/text_edit_idle_detect_sec", PROPERTY_HINT_RANGE, "0,10,0.01,or_greater")); // No negative numbers. + GLOBAL_DEF("gui/common/text_edit_undo_stack_max_size", 1024); + ProjectSettings::get_singleton()->set_custom_property_info("gui/common/text_edit_undo_stack_max_size", PropertyInfo(Variant::INT, "gui/common/text_edit_undo_stack_max_size", PROPERTY_HINT_RANGE, "0,10000,1,or_greater")); // No negative numbers. +} + +bool TextEdit::_set(const StringName &p_name, const Variant &p_value) { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + double value = p_value; + if (value == -1) { + if (opentype_features.has(tag)) { + opentype_features.erase(tag); + text.set_font_features(opentype_features); + text.invalidate_all(); + update(); + } } else { - line++; + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + text.set_font_features(opentype_features); + text.invalidate_all(); + update(); + } } + notify_property_list_changed(); + return true; } - if (pos == -1) { - r_line = -1; - r_column = -1; - return false; - } - - r_line = line; - r_column = pos; - - return true; + return false; } -void TextEdit::_cursor_changed_emit() { - emit_signal("cursor_changed"); - cursor_changed_dirty = false; +bool TextEdit::_get(const StringName &p_name, Variant &r_ret) const { + String str = p_name; + if (str.begins_with("opentype_features/")) { + String name = str.get_slicec('/', 1); + int32_t tag = TS->name_to_tag(name); + if (opentype_features.has(tag)) { + r_ret = opentype_features[tag]; + return true; + } else { + r_ret = -1; + return true; + } + } + return false; } -void TextEdit::_text_changed_emit() { - emit_signal("text_changed"); - text_changed_dirty = false; +void TextEdit::_get_property_list(List<PropertyInfo> *p_list) const { + for (const Variant *ftr = opentype_features.next(nullptr); ftr != nullptr; ftr = opentype_features.next(ftr)) { + String name = TS->tag_to_name(*ftr); + p_list->push_back(PropertyInfo(Variant::FLOAT, "opentype_features/" + name)); + } + p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); } -void TextEdit::set_line_as_hidden(int p_line, bool p_hidden) { - ERR_FAIL_INDEX(p_line, text.size()); - if (is_hiding_enabled() || !p_hidden) { - text.set_hidden(p_line, p_hidden); +/* Internal API for CodeEdit. */ +// Line hiding. +void TextEdit::_set_hiding_enabled(bool p_enabled) { + if (!p_enabled) { + _unhide_all_lines(); } + hiding_enabled = p_enabled; update(); } -bool TextEdit::is_line_hidden(int p_line) const { +bool TextEdit::_is_hiding_enabled() const { + return hiding_enabled; +} + +bool TextEdit::_is_line_hidden(int p_line) const { ERR_FAIL_INDEX_V(p_line, text.size(), false); return text.is_hidden(p_line); } -void TextEdit::fold_all_lines() { +void TextEdit::_unhide_all_lines() { for (int i = 0; i < text.size(); i++) { - fold_line(i); + text.set_hidden(i, false); } _update_scrollbars(); update(); } -void TextEdit::unhide_all_lines() { - for (int i = 0; i < text.size(); i++) { - text.set_hidden(i, false); +void TextEdit::_set_line_as_hidden(int p_line, bool p_hidden) { + ERR_FAIL_INDEX(p_line, text.size()); + if (_is_hiding_enabled() || !p_hidden) { + text.set_hidden(p_line, p_hidden); } - _update_scrollbars(); update(); } -int TextEdit::num_lines_from(int p_line_from, int visible_amount) const { - // Returns the number of lines (hidden and unhidden) from p_line_from to (p_line_from + visible_amount of unhidden lines). - ERR_FAIL_INDEX_V(p_line_from, text.size(), ABS(visible_amount)); +// Symbol lookup. +void TextEdit::_set_symbol_lookup_word(const String &p_symbol) { + lookup_symbol_word = p_symbol; + update(); +} - if (!is_hiding_enabled()) { - return ABS(visible_amount); +/* Text manipulation */ + +// Overridable actions +void TextEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { + if (!editable) { + return; } - int num_visible = 0; - int num_total = 0; - if (visible_amount >= 0) { - for (int i = p_line_from; i < text.size(); i++) { - num_total++; - if (!is_line_hidden(i)) { - num_visible++; - } - if (num_visible >= visible_amount) { - break; - } - } - } else { - visible_amount = ABS(visible_amount); - for (int i = p_line_from; i >= 0; i--) { - num_total++; - if (!is_line_hidden(i)) { - num_visible++; - } - if (num_visible >= visible_amount) { - break; - } - } + bool had_selection = has_selection(); + if (had_selection) { + begin_complex_operation(); + delete_selection(); } - return num_total; -} -int TextEdit::num_lines_from_rows(int p_line_from, int p_wrap_index_from, int visible_amount, int &wrap_index) const { - // Returns the number of lines (hidden and unhidden) from (p_line_from + p_wrap_index_from) row to (p_line_from + visible_amount of unhidden and wrapped rows). - // Wrap index is set to the wrap index of the last line. - wrap_index = 0; - ERR_FAIL_INDEX_V(p_line_from, text.size(), ABS(visible_amount)); + /* Remove the old character if in insert mode and no selection. */ + if (overtype_mode && !had_selection) { + begin_complex_operation(); - if (!is_hiding_enabled() && !is_wrap_enabled()) { - return ABS(visible_amount); + /* Make sure we don't try and remove empty space. */ + int cl = get_caret_line(); + int cc = get_caret_column(); + if (cc < get_line(cl).length()) { + _remove_text(cl, cc, cl, cc + 1); + } } - int num_visible = 0; - int num_total = 0; - if (visible_amount == 0) { - num_total = 0; - wrap_index = 0; - } else if (visible_amount > 0) { - int i; - num_visible -= p_wrap_index_from; - for (i = p_line_from; i < text.size(); i++) { - num_total++; - if (!is_line_hidden(i)) { - num_visible++; - num_visible += times_line_wraps(i); - } - if (num_visible >= visible_amount) { - break; - } - } - wrap_index = times_line_wraps(MIN(i, text.size() - 1)) - MAX(0, num_visible - visible_amount); - } else { - visible_amount = ABS(visible_amount); - int i; - num_visible -= times_line_wraps(p_line_from) - p_wrap_index_from; - for (i = p_line_from; i >= 0; i--) { - num_total++; - if (!is_line_hidden(i)) { - num_visible++; - num_visible += times_line_wraps(i); - } - if (num_visible >= visible_amount) { - break; - } - } - wrap_index = MAX(0, num_visible - visible_amount); + const char32_t chr[2] = { (char32_t)p_unicode, 0 }; + insert_text_at_caret(chr); + + if ((overtype_mode && !had_selection) || (had_selection)) { + end_complex_operation(); } - wrap_index = MAX(wrap_index, 0); - return num_total; } -int TextEdit::get_last_unhidden_line() const { - // Returns the last line in the text that is not hidden. - if (!is_hiding_enabled()) { - return text.size() - 1; +void TextEdit::_backspace_internal() { + if (!editable) { + return; } - int last_line; - for (last_line = text.size() - 1; last_line > 0; last_line--) { - if (!is_line_hidden(last_line)) { - break; - } + if (has_selection()) { + delete_selection(); + return; } - return last_line; -} -int TextEdit::get_indent_level(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), 0); + int cc = get_caret_column(); + int cl = get_caret_line(); - // Counts number of tabs and spaces before line starts. - int tab_count = 0; - int whitespace_count = 0; - int line_length = text[p_line].size(); - for (int i = 0; i < line_length - 1; i++) { - if (text[p_line][i] == '\t') { - tab_count++; - } else if (text[p_line][i] == ' ') { - whitespace_count++; - } else { - break; - } + if (cc == 0 && cl == 0) { + return; } - return tab_count * indent_size + whitespace_count; -} -bool TextEdit::is_line_comment(int p_line) const { - // Checks to see if this line is the start of a comment. - ERR_FAIL_INDEX_V(p_line, text.size(), false); + int prev_line = cc ? cl : cl - 1; + int prev_column = cc ? (cc - 1) : (text[cl - 1].length()); - int line_length = text[p_line].size(); - for (int i = 0; i < line_length - 1; i++) { - if (_is_whitespace(text[p_line][i])) { - continue; - } - if (_is_symbol(text[p_line][i])) { - if (text[p_line][i] == '\\') { - i++; // Skip quoted anything. - continue; - } - return text[p_line][i] == '#' || (i + 1 < line_length && text[p_line][i] == '/' && text[p_line][i + 1] == '/'); - } - break; + merge_gutters(prev_line, cl); + + if (_is_line_hidden(cl)) { + _set_line_as_hidden(prev_line, true); } - return false; + _remove_text(prev_line, prev_column, cl, cc); + + set_caret_line(prev_line, false, true); + set_caret_column(prev_column); } -bool TextEdit::can_fold(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), false); - if (!is_hiding_enabled()) { - return false; - } - if (p_line + 1 >= text.size()) { - return false; - } - if (text[p_line].strip_edges().size() == 0) { - return false; - } - if (is_folded(p_line)) { - return false; - } - if (is_line_hidden(p_line)) { - return false; +void TextEdit::_cut_internal() { + if (!editable) { + return; } - if (is_line_comment(p_line)) { - return false; + + if (has_selection()) { + DisplayServer::get_singleton()->clipboard_set(get_selected_text()); + delete_selection(); + cut_copy_line = ""; + return; } - int start_indent = get_indent_level(p_line); + int cl = get_caret_line(); - for (int i = p_line + 1; i < text.size(); i++) { - if (text[i].strip_edges().size() == 0) { - continue; - } - int next_indent = get_indent_level(i); - if (is_line_comment(i)) { - continue; - } else if (next_indent > start_indent) { - return true; - } else { - return false; - } + String clipboard = text[cl]; + DisplayServer::get_singleton()->clipboard_set(clipboard); + set_caret_line(cl); + set_caret_column(0); + + if (cl == 0 && get_line_count() > 1) { + _remove_text(cl, 0, cl + 1, 0); + } else { + _remove_text(cl, 0, cl, text[cl].length()); + backspace(); + set_caret_line(get_caret_line() + 1); } - return false; + cut_copy_line = clipboard; } -bool TextEdit::is_folded(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), false); - if (p_line + 1 >= text.size()) { - return false; +void TextEdit::_copy_internal() { + if (has_selection()) { + DisplayServer::get_singleton()->clipboard_set(get_selected_text()); + cut_copy_line = ""; + return; } - return !is_line_hidden(p_line) && is_line_hidden(p_line + 1); -} -Vector<int> TextEdit::get_folded_lines() const { - Vector<int> folded_lines; - - for (int i = 0; i < text.size(); i++) { - if (is_folded(i)) { - folded_lines.push_back(i); - } + int cl = get_caret_line(); + if (text[cl].length() != 0) { + String clipboard = _base_get_text(cl, 0, cl, text[cl].length()); + DisplayServer::get_singleton()->clipboard_set(clipboard); + cut_copy_line = clipboard; } - return folded_lines; } -void TextEdit::fold_line(int p_line) { - ERR_FAIL_INDEX(p_line, text.size()); - if (!is_hiding_enabled()) { - return; - } - if (!can_fold(p_line)) { +void TextEdit::_paste_internal() { + if (!editable) { return; } - // Hide lines below this one. - int start_indent = get_indent_level(p_line); - int last_line = start_indent; - for (int i = p_line + 1; i < text.size(); i++) { - if (text[i].strip_edges().size() != 0) { - if (is_line_comment(i)) { - continue; - } else if (get_indent_level(i) > start_indent) { - last_line = i; - } else { - break; - } - } + String clipboard = DisplayServer::get_singleton()->clipboard_get(); + + begin_complex_operation(); + if (has_selection()) { + delete_selection(); + } else if (!cut_copy_line.is_empty() && cut_copy_line == clipboard) { + set_caret_column(0); + String ins = "\n"; + clipboard += ins; } - for (int i = p_line + 1; i <= last_line; i++) { - set_line_as_hidden(i, true); + + insert_text_at_caret(clipboard); + end_complex_operation(); +} + +/* Text. */ +// Context menu. +void TextEdit::_generate_context_menu() { + if (!menu) { + menu = memnew(PopupMenu); + add_child(menu, false, INTERNAL_MODE_FRONT); + + menu_dir = memnew(PopupMenu); + menu_dir->set_name("DirMenu"); + menu_dir->add_radio_check_item(RTR("Same as layout direction"), MENU_DIR_INHERITED); + menu_dir->add_radio_check_item(RTR("Auto-detect direction"), MENU_DIR_AUTO); + menu_dir->add_radio_check_item(RTR("Left-to-right"), MENU_DIR_LTR); + menu_dir->add_radio_check_item(RTR("Right-to-left"), MENU_DIR_RTL); + menu->add_child(menu_dir, false, INTERNAL_MODE_FRONT); + + menu_ctl = memnew(PopupMenu); + menu_ctl->set_name("CTLMenu"); + menu_ctl->add_item(RTR("Left-to-right mark (LRM)"), MENU_INSERT_LRM); + menu_ctl->add_item(RTR("Right-to-left mark (RLM)"), MENU_INSERT_RLM); + menu_ctl->add_item(RTR("Start of left-to-right embedding (LRE)"), MENU_INSERT_LRE); + menu_ctl->add_item(RTR("Start of right-to-left embedding (RLE)"), MENU_INSERT_RLE); + menu_ctl->add_item(RTR("Start of left-to-right override (LRO)"), MENU_INSERT_LRO); + menu_ctl->add_item(RTR("Start of right-to-left override (RLO)"), MENU_INSERT_RLO); + menu_ctl->add_item(RTR("Pop direction formatting (PDF)"), MENU_INSERT_PDF); + menu_ctl->add_separator(); + menu_ctl->add_item(RTR("Arabic letter mark (ALM)"), MENU_INSERT_ALM); + menu_ctl->add_item(RTR("Left-to-right isolate (LRI)"), MENU_INSERT_LRI); + menu_ctl->add_item(RTR("Right-to-left isolate (RLI)"), MENU_INSERT_RLI); + menu_ctl->add_item(RTR("First strong isolate (FSI)"), MENU_INSERT_FSI); + menu_ctl->add_item(RTR("Pop direction isolate (PDI)"), MENU_INSERT_PDI); + menu_ctl->add_separator(); + menu_ctl->add_item(RTR("Zero width joiner (ZWJ)"), MENU_INSERT_ZWJ); + menu_ctl->add_item(RTR("Zero width non-joiner (ZWNJ)"), MENU_INSERT_ZWNJ); + menu_ctl->add_item(RTR("Word joiner (WJ)"), MENU_INSERT_WJ); + menu_ctl->add_item(RTR("Soft hyphen (SHY)"), MENU_INSERT_SHY); + menu->add_child(menu_ctl, false, INTERNAL_MODE_FRONT); + + menu->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); + menu_dir->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); + menu_ctl->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); } - // Fix selection. - if (is_selection_active()) { - if (is_line_hidden(selection.from_line) && is_line_hidden(selection.to_line)) { - deselect(); - } else if (is_line_hidden(selection.from_line)) { - select(p_line, 9999, selection.to_line, selection.to_column); - } else if (is_line_hidden(selection.to_line)) { - select(selection.from_line, selection.from_column, p_line, 9999); - } + // Reorganize context menu. + menu->clear(); + if (editable) { + menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_cut") : 0); + } + menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_copy") : 0); + if (editable) { + menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_paste") : 0); + } + menu->add_separator(); + if (is_selecting_enabled()) { + menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : 0); + } + if (editable) { + menu->add_item(RTR("Clear"), MENU_CLEAR); + menu->add_separator(); + menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_undo") : 0); + menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_redo") : 0); } + menu->add_separator(); + menu->add_submenu_item(RTR("Text writing direction"), "DirMenu"); + menu->add_separator(); + menu->add_check_item(RTR("Display control characters"), MENU_DISPLAY_UCC); + menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); + if (editable) { + menu->add_submenu_item(RTR("Insert control character"), "CTLMenu"); + } + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_AUTO), text_direction == TEXT_DIRECTION_AUTO); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_LTR), text_direction == TEXT_DIRECTION_LTR); + menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_RTL), text_direction == TEXT_DIRECTION_RTL); - // Reset cursor. - if (is_line_hidden(cursor.line)) { - cursor_set_line(p_line, false, false); - cursor_set_column(get_line(p_line).length(), false); + if (editable) { + menu->set_item_disabled(menu->get_item_index(MENU_UNDO), !has_undo()); + menu->set_item_disabled(menu->get_item_index(MENU_REDO), !has_redo()); } - _update_scrollbars(); - update(); } -void TextEdit::unfold_line(int p_line) { - ERR_FAIL_INDEX(p_line, text.size()); +int TextEdit::_get_menu_action_accelerator(const String &p_action) { + const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(p_action); + if (!events) { + return 0; + } - if (!is_folded(p_line) && !is_line_hidden(p_line)) { - return; + // Use first event in the list for the accelerator. + const List<Ref<InputEvent>>::Element *first_event = events->front(); + if (!first_event) { + return 0; } - int fold_start; - for (fold_start = p_line; fold_start > 0; fold_start--) { - if (is_folded(fold_start)) { - break; - } + + const Ref<InputEventKey> event = first_event->get(); + if (event.is_null()) { + return 0; } - fold_start = is_folded(fold_start) ? fold_start : p_line; - for (int i = fold_start + 1; i < text.size(); i++) { - if (is_line_hidden(i)) { - set_line_as_hidden(i, false); - } else { - break; - } + // Use physical keycode if non-zero + if (event->get_physical_keycode() != 0) { + return event->get_physical_keycode_with_modifiers(); + } else { + return event->get_keycode_with_modifiers(); } - _update_scrollbars(); - update(); } -void TextEdit::toggle_fold_line(int p_line) { - ERR_FAIL_INDEX(p_line, text.size()); +/* Versioning */ +void TextEdit::_push_current_op() { + if (current_op.type == TextOperation::TYPE_NONE) { + return; // Nothing to do. + } - if (!is_folded(p_line)) { - fold_line(p_line); - } else { - unfold_line(p_line); + if (next_operation_is_complex) { + current_op.chain_forward = true; + next_operation_is_complex = false; } -} -int TextEdit::get_line_count() const { - return text.size(); + undo_stack.push_back(current_op); + current_op.type = TextOperation::TYPE_NONE; + current_op.text = ""; + current_op.chain_forward = false; + + if (undo_stack.size() > undo_stack_max_size) { + undo_stack.pop_front(); + } } void TextEdit::_do_text_op(const TextOperation &p_op, bool p_reverse) { @@ -5839,1303 +5262,805 @@ void TextEdit::_clear_redo() { } } -void TextEdit::undo() { - if (readonly) { - return; - } - - _push_current_op(); +/* Search */ +int TextEdit::_get_column_pos_of_word(const String &p_key, const String &p_search, uint32_t p_search_flags, int p_from_column) const { + int col = -1; - if (undo_stack_pos == nullptr) { - if (!undo_stack.size()) { - return; // Nothing to undo. + if (p_key.length() > 0 && p_search.length() > 0) { + if (p_from_column < 0 || p_from_column > p_search.length()) { + p_from_column = 0; } - undo_stack_pos = undo_stack.back(); - - } else if (undo_stack_pos == undo_stack.front()) { - return; // At the bottom of the undo stack. - } else { - undo_stack_pos = undo_stack_pos->prev(); - } - - deselect(); + while (col == -1 && p_from_column <= p_search.length()) { + if (p_search_flags & SEARCH_MATCH_CASE) { + col = p_search.find(p_key, p_from_column); + } else { + col = p_search.findn(p_key, p_from_column); + } - TextOperation op = undo_stack_pos->get(); - _do_text_op(op, true); - if (op.type != TextOperation::TYPE_INSERT && (op.from_line != op.to_line || op.to_column != op.from_column + 1)) { - select(op.from_line, op.from_column, op.to_line, op.to_column); - } + // Whole words only. + if (col != -1 && p_search_flags & SEARCH_WHOLE_WORDS) { + p_from_column = col; - current_op.version = op.prev_version; - if (undo_stack_pos->get().chain_backward) { - while (true) { - ERR_BREAK(!undo_stack_pos->prev()); - undo_stack_pos = undo_stack_pos->prev(); - op = undo_stack_pos->get(); - _do_text_op(op, true); - current_op.version = op.prev_version; - if (undo_stack_pos->get().chain_forward) { - break; + if (col > 0 && _is_text_char(p_search[col - 1])) { + col = -1; + } else if ((col + p_key.length()) < p_search.length() && _is_text_char(p_search[col + p_key.length()])) { + col = -1; + } } - } - } - _update_scrollbars(); - if (undo_stack_pos->get().type == TextOperation::TYPE_REMOVE) { - cursor_set_line(undo_stack_pos->get().to_line, false); - cursor_set_column(undo_stack_pos->get().to_column); - _cancel_code_hint(); - } else { - cursor_set_line(undo_stack_pos->get().from_line, false); - cursor_set_column(undo_stack_pos->get().from_column); + p_from_column += 1; + } } - update(); + return col; } -void TextEdit::redo() { - if (readonly) { - return; - } - _push_current_op(); - - if (undo_stack_pos == nullptr) { - return; // Nothing to do. - } - - deselect(); +/* Mouse */ +int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); + p_wrap_index = MIN(p_wrap_index, text.get_line_data(p_line)->get_line_count() - 1); - TextOperation op = undo_stack_pos->get(); - _do_text_op(op, false); - current_op.version = op.version; - if (undo_stack_pos->get().chain_forward) { - while (true) { - ERR_BREAK(!undo_stack_pos->next()); - undo_stack_pos = undo_stack_pos->next(); - op = undo_stack_pos->get(); - _do_text_op(op, false); - current_op.version = op.version; - if (undo_stack_pos->get().chain_backward) { - break; - } - } + RID text_rid = text.get_line_data(p_line)->get_line_rid(p_wrap_index); + if (is_layout_rtl()) { + p_px = TS->shaped_text_get_size(text_rid).x - p_px; } - - _update_scrollbars(); - cursor_set_line(undo_stack_pos->get().to_line, false); - cursor_set_column(undo_stack_pos->get().to_column); - undo_stack_pos = undo_stack_pos->next(); - update(); -} - -void TextEdit::clear_undo_history() { - saved_version = 0; - current_op.type = TextOperation::TYPE_NONE; - undo_stack_pos = nullptr; - undo_stack.clear(); + return TS->shaped_text_hit_test_position(text_rid, p_px); } -void TextEdit::begin_complex_operation() { - _push_current_op(); - next_operation_is_complex = true; +/* Caret */ +void TextEdit::_emit_caret_changed() { + emit_signal(SNAME("caret_changed")); + caret_pos_dirty = false; } -void TextEdit::end_complex_operation() { - _push_current_op(); - ERR_FAIL_COND(undo_stack.size() == 0); - - if (undo_stack.back()->get().chain_forward) { - undo_stack.back()->get().chain_forward = false; +void TextEdit::_reset_caret_blink_timer() { + if (!caret_blink_enabled) { return; } - undo_stack.back()->get().chain_backward = true; -} - -void TextEdit::_push_current_op() { - if (current_op.type == TextOperation::TYPE_NONE) { - return; // Nothing to do. - } - - if (next_operation_is_complex) { - current_op.chain_forward = true; - next_operation_is_complex = false; - } - - undo_stack.push_back(current_op); - current_op.type = TextOperation::TYPE_NONE; - current_op.text = ""; - current_op.chain_forward = false; - - if (undo_stack.size() > undo_stack_max_size) { - undo_stack.pop_front(); + draw_caret = true; + if (has_focus()) { + caret_blink_timer->stop(); + caret_blink_timer->start(); + update(); } } -void TextEdit::set_indent_using_spaces(const bool p_use_spaces) { - indent_using_spaces = p_use_spaces; +void TextEdit::_toggle_draw_caret() { + draw_caret = !draw_caret; + if (is_visible_in_tree() && has_focus() && window_has_focus) { + update(); + } } -bool TextEdit::is_indent_using_spaces() const { - return indent_using_spaces; -} +int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); -void TextEdit::set_indent_size(const int p_size) { - ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0."); - if (indent_size != p_size) { - indent_size = p_size; - text.set_indent_size(p_size); - text.invalidate_all_lines(); + int row = 0; + Vector<Vector2i> rows2 = text.get_line_wrap_ranges(p_line); + for (int i = 0; i < rows2.size(); i++) { + if ((p_char >= rows2[i].x) && (p_char < rows2[i].y)) { + row = i; + break; + } } - space_indent = ""; - for (int i = 0; i < p_size; i++) { - space_indent += " "; + Rect2 l_caret, t_caret; + TextServer::Direction l_dir, t_dir; + RID text_rid = text.get_line_data(p_line)->get_line_rid(row); + TS->shaped_text_get_carets(text_rid, caret.column, l_caret, l_dir, t_caret, t_dir); + if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { + return l_caret.position.x; + } else { + return t_caret.position.x; } - - update(); } -int TextEdit::get_indent_size() { - return indent_size; -} - -void TextEdit::set_draw_tabs(bool p_draw) { - draw_tabs = p_draw; - update(); -} - -bool TextEdit::is_drawing_tabs() const { - return draw_tabs; -} - -void TextEdit::set_draw_spaces(bool p_draw) { - draw_spaces = p_draw; - update(); +/* Selection */ +void TextEdit::_click_selection_held() { + // Warning: is_mouse_button_pressed(MOUSE_BUTTON_LEFT) returns false for double+ clicks, so this doesn't work for MODE_WORD + // and MODE_LINE. However, moving the mouse triggers _gui_input, which calls these functions too, so that's not a huge problem. + // I'm unsure if there's an actual fix that doesn't have a ton of side effects. + if (Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT) && selection.selecting_mode != SelectionMode::SELECTION_MODE_NONE) { + switch (selection.selecting_mode) { + case SelectionMode::SELECTION_MODE_POINTER: { + _update_selection_mode_pointer(); + } break; + case SelectionMode::SELECTION_MODE_WORD: { + _update_selection_mode_word(); + } break; + case SelectionMode::SELECTION_MODE_LINE: { + _update_selection_mode_line(); + } break; + default: { + break; + } + } + } else { + click_select_held->stop(); + } } -bool TextEdit::is_drawing_spaces() const { - return draw_spaces; -} +void TextEdit::_update_selection_mode_pointer() { + dragging_selection = true; + Point2 mp = get_local_mouse_pos(); -void TextEdit::set_override_selected_font_color(bool p_override_selected_font_color) { - override_selected_font_color = p_override_selected_font_color; -} + Point2i pos = get_line_column_at_pos(Point2i(mp.x, mp.y)); + int line = pos.y; + int col = pos.x; -bool TextEdit::is_overriding_selected_font_color() const { - return override_selected_font_color; -} + select(selection.selecting_line, selection.selecting_column, line, col); -void TextEdit::set_insert_mode(bool p_enabled) { - insert_mode = p_enabled; + set_caret_line(line, false); + set_caret_column(col); update(); -} - -bool TextEdit::is_insert_mode() const { - return insert_mode; -} - -bool TextEdit::is_insert_text_operation() { - return (current_op.type == TextOperation::TYPE_INSERT); -} -uint32_t TextEdit::get_version() const { - return current_op.version; + click_select_held->start(); } -uint32_t TextEdit::get_saved_version() const { - return saved_version; -} +void TextEdit::_update_selection_mode_word() { + dragging_selection = true; + Point2 mp = get_local_mouse_pos(); -void TextEdit::tag_saved_version() { - saved_version = get_version(); -} + Point2i pos = get_line_column_at_pos(Point2i(mp.x, mp.y)); + int line = pos.y; + int col = pos.x; -double TextEdit::get_scroll_pos_for_line(int p_line, int p_wrap_index) const { - if (!is_wrap_enabled() && !is_hiding_enabled()) { - return p_line; + int caret_pos = CLAMP(col, 0, text[line].length()); + int beg = caret_pos; + int end = beg; + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (words[i].x < caret_pos && words[i].y > caret_pos) { + beg = words[i].x; + end = words[i].y; + break; + } } - // Count the number of visible lines up to this line. - double new_line_scroll_pos = 0.0; - int to = CLAMP(p_line, 0, text.size() - 1); - for (int i = 0; i < to; i++) { - if (!text.is_hidden(i)) { - new_line_scroll_pos++; - new_line_scroll_pos += times_line_wraps(i); + /* Initial selection. */ + if (!selection.active) { + select(line, beg, line, end); + selection.selecting_column = beg; + selection.selected_word_beg = beg; + selection.selected_word_end = end; + selection.selected_word_origin = beg; + set_caret_line(selection.to_line, false); + set_caret_column(selection.to_column); + } else { + if ((col <= selection.selected_word_origin && line == selection.selecting_line) || line < selection.selecting_line) { + selection.selecting_column = selection.selected_word_end; + select(line, beg, selection.selecting_line, selection.selected_word_end); + set_caret_line(selection.from_line, false); + set_caret_column(selection.from_column); + } else { + selection.selecting_column = selection.selected_word_beg; + select(selection.selecting_line, selection.selected_word_beg, line, end); + set_caret_line(selection.to_line, false); + set_caret_column(selection.to_column); } } - new_line_scroll_pos += p_wrap_index; - return new_line_scroll_pos; -} -void TextEdit::set_line_as_first_visible(int p_line, int p_wrap_index) { - set_v_scroll(get_scroll_pos_for_line(p_line, p_wrap_index)); + update(); + + click_select_held->start(); } -void TextEdit::set_line_as_center_visible(int p_line, int p_wrap_index) { - int visible_rows = get_visible_rows(); - int wi; - int first_line = p_line - num_lines_from_rows(p_line, p_wrap_index, -visible_rows / 2, wi) + 1; +void TextEdit::_update_selection_mode_line() { + dragging_selection = true; + Point2 mp = get_local_mouse_pos(); - set_v_scroll(get_scroll_pos_for_line(first_line, wi)); -} + Point2i pos = get_line_column_at_pos(Point2i(mp.x, mp.y)); + int line = pos.y; + int col = pos.x; -void TextEdit::set_line_as_last_visible(int p_line, int p_wrap_index) { - int wi; - int first_line = p_line - num_lines_from_rows(p_line, p_wrap_index, -get_visible_rows() - 1, wi) + 1; + col = 0; + if (line < selection.selecting_line) { + /* Caret is above us. */ + set_caret_line(line - 1, false); + selection.selecting_column = text[selection.selecting_line].length(); + } else { + /* Caret is below us. */ + set_caret_line(line + 1, false); + selection.selecting_column = 0; + col = text[line].length(); + } + set_caret_column(0); - set_v_scroll(get_scroll_pos_for_line(first_line, wi) + get_visible_rows_offset()); -} + select(selection.selecting_line, selection.selecting_column, line, col); + update(); -int TextEdit::get_first_visible_line() const { - return CLAMP(cursor.line_ofs, 0, text.size() - 1); + click_select_held->start(); } -int TextEdit::get_last_full_visible_line() const { - int first_vis_line = get_first_visible_line(); - int last_vis_line = 0; - int wi; - last_vis_line = first_vis_line + num_lines_from_rows(first_vis_line, cursor.wrap_ofs, get_visible_rows(), wi) - 1; - last_vis_line = CLAMP(last_vis_line, 0, text.size() - 1); - return last_vis_line; -} +void TextEdit::_pre_shift_selection() { + if (!selection.active || selection.selecting_mode == SelectionMode::SELECTION_MODE_NONE) { + selection.selecting_line = caret.line; + selection.selecting_column = caret.column; + selection.active = true; + } -int TextEdit::get_last_full_visible_line_wrap_index() const { - int first_vis_line = get_first_visible_line(); - int wi; - num_lines_from_rows(first_vis_line, cursor.wrap_ofs, get_visible_rows(), wi); - return wi; + selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; } -double TextEdit::get_visible_rows_offset() const { - double total = _get_control_height(); - total /= (double)get_row_height(); - total = total - floor(total); - total = -CLAMP(total, 0.001, 1) + 1; - return total; -} +void TextEdit::_post_shift_selection() { + if (selection.active && selection.selecting_mode == SelectionMode::SELECTION_MODE_SHIFT) { + select(selection.selecting_line, selection.selecting_column, caret.line, caret.column); + update(); + } -double TextEdit::get_v_scroll_offset() const { - double val = get_v_scroll() - floor(get_v_scroll()); - return CLAMP(val, 0, 1); + selection.selecting_text = true; } -double TextEdit::get_v_scroll() const { - return v_scroll->get_value(); -} +/* Line Wrapping */ +void TextEdit::_update_wrap_at_column(bool p_force) { + int new_wrap_at = get_size().width - style_normal->get_minimum_size().width - gutters_width - gutter_padding; + if (draw_minimap) { + new_wrap_at -= minimap_width; + } + if (v_scroll->is_visible_in_tree()) { + new_wrap_at -= v_scroll->get_combined_minimum_size().width; + } + /* Give it a little more space. */ + new_wrap_at -= wrap_right_offset; -void TextEdit::set_v_scroll(double p_scroll) { - v_scroll->set_value(p_scroll); - int max_v_scroll = v_scroll->get_max() - v_scroll->get_page(); - if (p_scroll >= max_v_scroll - 1.0) { - _scroll_moved(v_scroll->get_value()); + if ((wrap_at_column != new_wrap_at) || p_force) { + wrap_at_column = new_wrap_at; + if (line_wrapping_mode) { + text.set_width(wrap_at_column); + } else { + text.set_width(-1); + } + text.invalidate_all_lines(); } -} -int TextEdit::get_h_scroll() const { - return h_scroll->get_value(); + _update_caret_wrap_offset(); } -void TextEdit::set_h_scroll(int p_scroll) { - if (p_scroll < 0) { - p_scroll = 0; +void TextEdit::_update_caret_wrap_offset() { + int first_vis_line = get_first_visible_line(); + if (is_line_wrapped(first_vis_line)) { + caret.wrap_ofs = MIN(caret.wrap_ofs, get_line_wrap_count(first_vis_line)); + } else { + caret.wrap_ofs = 0; } - h_scroll->set_value(p_scroll); + set_line_as_first_visible(caret.line_ofs, caret.wrap_ofs); } -void TextEdit::set_smooth_scroll_enabled(bool p_enable) { - v_scroll->set_smooth_scroll_enabled(p_enable); - smooth_scroll_enabled = p_enable; -} - -bool TextEdit::is_smooth_scroll_enabled() const { - return smooth_scroll_enabled; -} +/* Viewport. */ +void TextEdit::_update_scrollbars() { + Size2 size = get_size(); + Size2 hmin = h_scroll->get_combined_minimum_size(); + Size2 vmin = v_scroll->get_combined_minimum_size(); -void TextEdit::set_v_scroll_speed(float p_speed) { - v_scroll_speed = p_speed; -} + v_scroll->set_begin(Point2(size.width - vmin.width, style_normal->get_margin(SIDE_TOP))); + v_scroll->set_end(Point2(size.width, size.height - style_normal->get_margin(SIDE_TOP) - style_normal->get_margin(SIDE_BOTTOM))); -float TextEdit::get_v_scroll_speed() const { - return v_scroll_speed; -} + h_scroll->set_begin(Point2(0, size.height - hmin.height)); + h_scroll->set_end(Point2(size.width - vmin.width, size.height)); -void TextEdit::set_completion(bool p_enabled, const Vector<String> &p_prefixes) { - completion_prefixes.clear(); - completion_enabled = p_enabled; - for (int i = 0; i < p_prefixes.size(); i++) { - completion_prefixes.insert(p_prefixes[i]); + int visible_rows = get_visible_line_count(); + int total_rows = get_total_visible_line_count(); + if (scroll_past_end_of_file_enabled) { + total_rows += visible_rows - 1; } -} -void TextEdit::_confirm_completion() { - begin_complex_operation(); + int visible_width = size.width - style_normal->get_minimum_size().width; + int total_width = text.get_max_width() + vmin.x + gutters_width + gutter_padding; + + if (draw_minimap) { + total_width += minimap_width; + } - _remove_text(cursor.line, cursor.column - completion_base.length(), cursor.line, cursor.column); - cursor_set_column(cursor.column - completion_base.length(), false); - insert_text_at_cursor(completion_current.insert_text); + updating_scrolls = true; - // When inserted into the middle of an existing string/method, don't add an unnecessary quote/bracket. - String line = text[cursor.line]; - char32_t next_char = line[cursor.column]; - char32_t last_completion_char = completion_current.insert_text[completion_current.insert_text.length() - 1]; - char32_t last_completion_char_display = completion_current.display[completion_current.display.length() - 1]; + if (total_rows > visible_rows) { + v_scroll->show(); + v_scroll->set_max(total_rows + _get_visible_lines_offset()); + v_scroll->set_page(visible_rows + _get_visible_lines_offset()); + if (smooth_scroll_enabled) { + v_scroll->set_step(0.25); + } else { + v_scroll->set_step(1); + } + set_v_scroll(get_v_scroll()); - if ((last_completion_char == '"' || last_completion_char == '\'') && (last_completion_char == next_char || last_completion_char_display == next_char)) { - _remove_text(cursor.line, cursor.column, cursor.line, cursor.column + 1); + } else { + caret.line_ofs = 0; + caret.wrap_ofs = 0; + v_scroll->set_value(0); + v_scroll->hide(); } - if (last_completion_char == '(') { - if (next_char == last_completion_char) { - _base_remove_text(cursor.line, cursor.column - 1, cursor.line, cursor.column); - } else if (auto_brace_completion_enabled) { - insert_text_at_cursor(")"); - cursor.column--; + if (total_width > visible_width && get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) { + h_scroll->show(); + h_scroll->set_max(total_width); + h_scroll->set_page(visible_width); + if (caret.x_ofs > (total_width - visible_width)) { + caret.x_ofs = (total_width - visible_width); } - } else if (last_completion_char == ')' && next_char == '(') { - _base_remove_text(cursor.line, cursor.column - 2, cursor.line, cursor.column); - if (line[cursor.column + 1] != ')') { - cursor.column--; + if (fabs(h_scroll->get_value() - (double)caret.x_ofs) >= 1) { + h_scroll->set_value(caret.x_ofs); } - } - end_complex_operation(); + } else { + caret.x_ofs = 0; + h_scroll->set_value(0); + h_scroll->hide(); + } - _cancel_completion(); + updating_scrolls = false; +} - if (last_completion_char == '(') { - query_code_comple(); +int TextEdit::_get_control_height() const { + int control_height = get_size().height; + control_height -= style_normal->get_minimum_size().height; + if (h_scroll->is_visible_in_tree()) { + control_height -= h_scroll->get_size().height; } + return control_height; } -void TextEdit::_cancel_code_hint() { - completion_hint = ""; - update(); +void TextEdit::_v_scroll_input() { + scrolling = false; + minimap_clicked = false; } -void TextEdit::_cancel_completion() { - if (!completion_active) { +void TextEdit::_scroll_moved(double p_to_val) { + if (updating_scrolls) { return; } - completion_active = false; - completion_forced = false; + if (h_scroll->is_visible_in_tree()) { + caret.x_ofs = h_scroll->get_value(); + } + if (v_scroll->is_visible_in_tree()) { + // Set line ofs and wrap ofs. + int v_scroll_i = floor(get_v_scroll()); + int sc = 0; + int n_line; + for (n_line = 0; n_line < text.size(); n_line++) { + if (!_is_line_hidden(n_line)) { + sc++; + sc += get_line_wrap_count(n_line); + if (sc > v_scroll_i) { + break; + } + } + } + n_line = MIN(n_line, text.size() - 1); + int line_wrap_amount = get_line_wrap_count(n_line); + int wi = line_wrap_amount - (sc - v_scroll_i - 1); + wi = CLAMP(wi, 0, line_wrap_amount); + + caret.line_ofs = n_line; + caret.wrap_ofs = wi; + } update(); } -static bool _is_completable(char32_t c) { - return !_is_symbol(c) || c == '"' || c == '\''; +double TextEdit::_get_visible_lines_offset() const { + double total = _get_control_height(); + total /= (double)get_line_height(); + total = total - floor(total); + total = -CLAMP(total, 0.001, 1) + 1; + return total; } -void TextEdit::_update_completion_candidates() { - String l = text[cursor.line]; - int cofs = CLAMP(cursor.column, 0, l.length()); - - String s; - - // Look for keywords first. - - bool inquote = false; - int first_quote = -1; - int restore_quotes = -1; +double TextEdit::_get_v_scroll_offset() const { + double val = get_v_scroll() - floor(get_v_scroll()); + return CLAMP(val, 0, 1); +} - int c = cofs - 1; - while (c >= 0) { - if (l[c] == '"' || l[c] == '\'') { - inquote = !inquote; - if (first_quote == -1) { - first_quote = c; - } - restore_quotes = 0; - } else if (restore_quotes == 0 && l[c] == '$') { - restore_quotes = 1; - } else if (restore_quotes == 0 && !_is_whitespace(l[c])) { - restore_quotes = -1; - } - c--; +void TextEdit::_scroll_up(real_t p_delta) { + if (scrolling && smooth_scroll_enabled && SGN(target_v_scroll - v_scroll->get_value()) != SGN(-p_delta)) { + scrolling = false; + minimap_clicked = false; } - bool pre_keyword = false; - bool cancel = false; + if (scrolling) { + target_v_scroll = (target_v_scroll - p_delta); + } else { + target_v_scroll = (get_v_scroll() - p_delta); + } - if (!inquote && first_quote == cofs - 1) { - // No completion here. - cancel = true; - } else if (inquote && first_quote != -1) { - s = l.substr(first_quote, cofs - first_quote); - } else if (cofs > 0 && l[cofs - 1] == ' ') { - int kofs = cofs - 1; - String kw; - while (kofs >= 0 && l[kofs] == ' ') { - kofs--; + if (smooth_scroll_enabled) { + if (target_v_scroll <= 0) { + target_v_scroll = 0; } - - while (kofs >= 0 && l[kofs] > 32 && _is_completable(l[kofs])) { - kw = String::chr(l[kofs]) + kw; - kofs--; + if (Math::abs(target_v_scroll - v_scroll->get_value()) < 1.0) { + v_scroll->set_value(target_v_scroll); + } else { + scrolling = true; + set_physics_process_internal(true); } - - pre_keyword = keywords.has(kw); - } else { - while (cofs > 0 && l[cofs - 1] > 32 && (l[cofs - 1] == '/' || _is_completable(l[cofs - 1]))) { - s = String::chr(l[cofs - 1]) + s; - if (l[cofs - 1] == '\'' || l[cofs - 1] == '"' || l[cofs - 1] == '$') { - break; - } - - cofs--; - } - } - - if (cursor.column > 0 && l[cursor.column - 1] == '(' && !pre_keyword && !completion_forced) { - cancel = true; + set_v_scroll(target_v_scroll); } +} - update(); - - bool prev_is_prefix = false; - if (cofs > 0 && completion_prefixes.has(String::chr(l[cofs - 1]))) { - prev_is_prefix = true; - } - // Check with one space before prefix, to allow indent. - if (cofs > 1 && l[cofs - 1] == ' ' && completion_prefixes.has(String::chr(l[cofs - 2]))) { - prev_is_prefix = true; +void TextEdit::_scroll_down(real_t p_delta) { + if (scrolling && smooth_scroll_enabled && SGN(target_v_scroll - v_scroll->get_value()) != SGN(p_delta)) { + scrolling = false; + minimap_clicked = false; } - if (cancel || (!pre_keyword && s == "" && (cofs == 0 || !prev_is_prefix))) { - // None to complete, cancel. - _cancel_completion(); - return; + if (scrolling) { + target_v_scroll = (target_v_scroll + p_delta); + } else { + target_v_scroll = (get_v_scroll() + p_delta); } - completion_options.clear(); - completion_index = 0; - completion_base = s; - Vector<float> sim_cache; - bool single_quote = s.begins_with("'"); - Vector<ScriptCodeCompletionOption> completion_options_casei; - Vector<ScriptCodeCompletionOption> completion_options_subseq; - Vector<ScriptCodeCompletionOption> completion_options_subseq_casei; - - String s_lower = s.to_lower(); - - for (List<ScriptCodeCompletionOption>::Element *E = completion_sources.front(); E; E = E->next()) { - ScriptCodeCompletionOption &option = E->get(); - - if (single_quote && option.display.is_quoted()) { - option.display = option.display.unquote().quote("'"); - } - - if (inquote && restore_quotes == 1 && !option.display.is_quoted()) { - String quote = single_quote ? "'" : "\""; - option.display = option.display.quote(quote); - option.insert_text = option.insert_text.quote(quote); + if (smooth_scroll_enabled) { + int max_v_scroll = round(v_scroll->get_max() - v_scroll->get_page()); + if (target_v_scroll > max_v_scroll) { + target_v_scroll = max_v_scroll; } - - if (option.display.length() == 0) { - continue; - } else if (s.length() == 0) { - completion_options.push_back(option); + if (Math::abs(target_v_scroll - v_scroll->get_value()) < 1.0) { + v_scroll->set_value(target_v_scroll); } else { - // This code works the same as: - /* - if (option.display.begins_with(s)) { - completion_options.push_back(option); - } else if (option.display.to_lower().begins_with(s.to_lower())) { - completion_options_casei.push_back(option); - } else if (s.is_subsequence_of(option.display)) { - completion_options_subseq.push_back(option); - } else if (s.is_subsequence_ofi(option.display)) { - completion_options_subseq_casei.push_back(option); - } - */ - // But is more performant due to being inlined and looping over the characters only once - - String display_lower = option.display.to_lower(); - - const char32_t *ssq = &s[0]; - const char32_t *ssq_lower = &s_lower[0]; - - const char32_t *tgt = &option.display[0]; - const char32_t *tgt_lower = &display_lower[0]; - - const char32_t *ssq_last_tgt = nullptr; - const char32_t *ssq_lower_last_tgt = nullptr; - - for (; *tgt; tgt++, tgt_lower++) { - if (*ssq == *tgt) { - ssq++; - ssq_last_tgt = tgt; - } - if (*ssq_lower == *tgt_lower) { - ssq_lower++; - ssq_lower_last_tgt = tgt; - } - } - - if (!*ssq) { // Matched the whole subsequence in s - if (ssq_last_tgt == &option.display[s.length() - 1]) { // Finished matching in the first s.length() characters - completion_options.push_back(option); - } else { - completion_options_subseq.push_back(option); - } - } else if (!*ssq_lower) { // Matched the whole subsequence in s_lower - if (ssq_lower_last_tgt == &option.display[s.length() - 1]) { // Finished matching in the first s.length() characters - completion_options_casei.push_back(option); - } else { - completion_options_subseq_casei.push_back(option); - } - } + scrolling = true; + set_physics_process_internal(true); } + } else { + set_v_scroll(target_v_scroll); } - - completion_options.append_array(completion_options_casei); - completion_options.append_array(completion_options_subseq); - completion_options.append_array(completion_options_subseq_casei); - - if (completion_options.size() == 0) { - // No options to complete, cancel. - _cancel_completion(); - return; - } - - if (completion_options.size() == 1 && s == completion_options[0].display) { - // A perfect match, stop completion. - _cancel_completion(); - return; - } - - // The top of the list is the best match. - completion_current = completion_options[0]; - completion_enabled = true; } -void TextEdit::query_code_comple() { - String l = text[cursor.line]; - int ofs = CLAMP(cursor.column, 0, l.length()); - - bool inquote = false; +void TextEdit::_scroll_lines_up() { + scrolling = false; + minimap_clicked = false; - int c = ofs - 1; - while (c >= 0) { - if (l[c] == '"' || l[c] == '\'') { - inquote = !inquote; - } - c--; - } + // Adjust the vertical scroll. + set_v_scroll(get_v_scroll() - 1); - bool ignored = completion_active && !completion_options.is_empty(); - if (ignored) { - ScriptCodeCompletionOption::Kind kind = ScriptCodeCompletionOption::KIND_PLAIN_TEXT; - const ScriptCodeCompletionOption *previous_option = nullptr; - for (int i = 0; i < completion_options.size(); i++) { - const ScriptCodeCompletionOption ¤t_option = completion_options[i]; - if (!previous_option) { - previous_option = ¤t_option; - kind = current_option.kind; - } - if (previous_option->kind != current_option.kind) { - ignored = false; - break; - } - } - ignored = ignored && (kind == ScriptCodeCompletionOption::KIND_FILE_PATH || kind == ScriptCodeCompletionOption::KIND_NODE_PATH || kind == ScriptCodeCompletionOption::KIND_SIGNAL); - } + // Adjust the caret to viewport. + if (!selection.active) { + int cur_line = caret.line; + int cur_wrap = get_caret_wrap_index(); + int last_vis_line = get_last_full_visible_line(); + int last_vis_wrap = get_last_full_visible_line_wrap_index(); - if (!ignored) { - if (ofs > 0 && (inquote || _is_completable(l[ofs - 1]) || completion_prefixes.has(String::chr(l[ofs - 1])))) { - emit_signal("request_completion"); - } else if (ofs > 1 && l[ofs - 1] == ' ' && completion_prefixes.has(String::chr(l[ofs - 2]))) { // Make it work with a space too, it's good enough. - emit_signal("request_completion"); + if (cur_line > last_vis_line || (cur_line == last_vis_line && cur_wrap > last_vis_wrap)) { + set_caret_line(last_vis_line, false, false, last_vis_wrap); } } } -void TextEdit::set_code_hint(const String &p_hint) { - completion_hint = p_hint; - completion_hint_offset = -0xFFFF; - update(); -} +void TextEdit::_scroll_lines_down() { + scrolling = false; + minimap_clicked = false; -void TextEdit::code_complete(const List<ScriptCodeCompletionOption> &p_strings, bool p_forced) { - completion_sources = p_strings; - completion_active = true; - completion_forced = p_forced; - completion_current = ScriptCodeCompletionOption(); - completion_index = 0; - _update_completion_candidates(); -} + // Adjust the vertical scroll. + set_v_scroll(get_v_scroll() + 1); -String TextEdit::get_word_at_pos(const Vector2 &p_pos) const { - int row, col; - _get_mouse_pos(p_pos, row, col); + // Adjust the caret to viewport. + if (!selection.active) { + int cur_line = caret.line; + int cur_wrap = get_caret_wrap_index(); + int first_vis_line = get_first_visible_line(); + int first_vis_wrap = caret.wrap_ofs; - String s = text[row]; - if (s.length() == 0) { - return ""; - } - int beg, end; - if (select_word(s, col, beg, end)) { - bool inside_quotes = false; - char32_t selected_quote = '\0'; - int qbegin = 0, qend = 0; - for (int i = 0; i < s.length(); i++) { - if (s[i] == '"' || s[i] == '\'') { - if (i == 0 || s[i - 1] != '\\') { - if (inside_quotes && selected_quote == s[i]) { - qend = i; - inside_quotes = false; - selected_quote = '\0'; - if (col >= qbegin && col <= qend) { - return s.substr(qbegin, qend - qbegin); - } - } else if (!inside_quotes) { - qbegin = i + 1; - inside_quotes = true; - selected_quote = s[i]; - } - } - } + if (cur_line < first_vis_line || (cur_line == first_vis_line && cur_wrap < first_vis_wrap)) { + set_caret_line(first_vis_line, false, false, first_vis_wrap); } - - return s.substr(beg, end - beg); } - - return String(); } -String TextEdit::get_tooltip(const Point2 &p_pos) const { - if (!tooltip_obj) { - return Control::get_tooltip(p_pos); - } - int row, col; - _get_mouse_pos(p_pos, row, col); - - String s = text[row]; - if (s.length() == 0) { - return Control::get_tooltip(p_pos); - } - int beg, end; - if (select_word(s, col, beg, end)) { - String tt = tooltip_obj->call(tooltip_func, s.substr(beg, end - beg), tooltip_ud); - - return tt; - } +// Minimap - return Control::get_tooltip(p_pos); -} +void TextEdit::_update_minimap_hover() { + const Point2 mp = get_local_mouse_pos(); + const int xmargin_end = get_size().width - style_normal->get_margin(SIDE_RIGHT); -void TextEdit::set_tooltip_request_func(Object *p_obj, const StringName &p_function, const Variant &p_udata) { - tooltip_obj = p_obj; - tooltip_func = p_function; - tooltip_ud = p_udata; -} + const bool hovering_sidebar = mp.x > xmargin_end - minimap_width && mp.x < xmargin_end; + if (!hovering_sidebar) { + if (hovering_minimap) { + // Only redraw if the hovering status changed. + hovering_minimap = false; + update(); + } -void TextEdit::set_line(int line, String new_text) { - if (line < 0 || line >= text.size()) { + // Return early to avoid running the operations below when not needed. return; } - _remove_text(line, 0, line, text[line].length()); - _insert_text(line, 0, new_text); - if (cursor.line == line) { - cursor.column = MIN(cursor.column, new_text.length()); - } - if (is_selection_active() && line == selection.to_line && selection.to_column > text[line].length()) { - selection.to_column = text[line].length(); - } -} -void TextEdit::insert_at(const String &p_text, int at) { - _insert_text(at, 0, p_text + "\n"); - if (cursor.line >= at) { - // offset cursor when located after inserted line - ++cursor.line; - } - if (is_selection_active()) { - if (selection.from_line >= at) { - // offset selection when located after inserted line - ++selection.from_line; - ++selection.to_line; - } else if (selection.to_line >= at) { - // extend selection that includes inserted line - ++selection.to_line; - } - } -} - -void TextEdit::set_show_line_length_guidelines(bool p_show) { - line_length_guidelines = p_show; - update(); -} + const int row = get_minimap_line_at_pos(Point2i(mp.x, mp.y)); -void TextEdit::set_line_length_guideline_soft_column(int p_column) { - line_length_guideline_soft_col = p_column; - update(); + const bool new_hovering_minimap = row >= get_first_visible_line() && row <= get_last_full_visible_line(); + if (new_hovering_minimap != hovering_minimap) { + // Only redraw if the hovering status changed. + hovering_minimap = new_hovering_minimap; + update(); + } } -void TextEdit::set_line_length_guideline_hard_column(int p_column) { - line_length_guideline_hard_col = p_column; - update(); -} +void TextEdit::_update_minimap_click() { + Point2 mp = get_local_mouse_pos(); -void TextEdit::set_draw_minimap(bool p_draw) { - if (draw_minimap != p_draw) { - draw_minimap = p_draw; - _update_wrap_at(); + int xmargin_end = get_size().width - style_normal->get_margin(SIDE_RIGHT); + if (!dragging_minimap && (mp.x < xmargin_end - minimap_width || mp.y > xmargin_end)) { + minimap_clicked = false; + return; } - update(); -} + minimap_clicked = true; + dragging_minimap = true; -bool TextEdit::is_drawing_minimap() const { - return draw_minimap; -} + int row = get_minimap_line_at_pos(Point2i(mp.x, mp.y)); -void TextEdit::set_minimap_width(int p_minimap_width) { - if (minimap_width != p_minimap_width) { - minimap_width = p_minimap_width; - _update_wrap_at(); + if (row >= get_first_visible_line() && (row < get_last_full_visible_line() || row >= (text.size() - 1))) { + minimap_scroll_ratio = v_scroll->get_as_ratio(); + minimap_scroll_click_pos = mp.y; + can_drag_minimap = true; + return; } - update(); -} - -int TextEdit::get_minimap_width() const { - return minimap_width; -} -void TextEdit::set_hiding_enabled(bool p_enabled) { - if (!p_enabled) { - unhide_all_lines(); + Point2i next_line = get_next_visible_line_index_offset_from(row, 0, -get_visible_line_count() / 2); + int first_line = row - next_line.x + 1; + double delta = get_scroll_pos_for_line(first_line, next_line.y) - get_v_scroll(); + if (delta < 0) { + _scroll_up(-delta); + } else { + _scroll_down(delta); } - hiding_enabled = p_enabled; - update(); } -bool TextEdit::is_hiding_enabled() const { - return hiding_enabled; -} +void TextEdit::_update_minimap_drag() { + if (!can_drag_minimap) { + return; + } -void TextEdit::set_highlight_current_line(bool p_enabled) { - highlight_current_line = p_enabled; - update(); -} + int control_height = _get_control_height(); + int scroll_height = v_scroll->get_max() * (minimap_char_size.y + minimap_line_spacing); + if (control_height > scroll_height) { + control_height = scroll_height; + } -bool TextEdit::is_highlight_current_line_enabled() const { - return highlight_current_line; -} + Point2 mp = get_local_mouse_pos(); -bool TextEdit::is_text_field() const { - return true; + double diff = (mp.y - minimap_scroll_click_pos) / control_height; + v_scroll->set_as_ratio(minimap_scroll_ratio + diff); } -void TextEdit::menu_option(int p_option) { - switch (p_option) { - case MENU_CUT: { - if (!readonly) { - cut(); - } - } break; - case MENU_COPY: { - copy(); - } break; - case MENU_PASTE: { - if (!readonly) { - paste(); - } - } break; - case MENU_CLEAR: { - if (!readonly) { - clear(); - } - } break; - case MENU_SELECT_ALL: { - select_all(); - } break; - case MENU_UNDO: { - undo(); - } break; - case MENU_REDO: { - redo(); - } break; - case MENU_DIR_INHERITED: { - set_text_direction(TEXT_DIRECTION_INHERITED); - } break; - case MENU_DIR_AUTO: { - set_text_direction(TEXT_DIRECTION_AUTO); - } break; - case MENU_DIR_LTR: { - set_text_direction(TEXT_DIRECTION_LTR); - } break; - case MENU_DIR_RTL: { - set_text_direction(TEXT_DIRECTION_RTL); - } break; - case MENU_DISPLAY_UCC: { - set_draw_control_chars(!get_draw_control_chars()); - } break; - case MENU_INSERT_LRM: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x200E)); - } - } break; - case MENU_INSERT_RLM: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x200F)); - } - } break; - case MENU_INSERT_LRE: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x202A)); - } - } break; - case MENU_INSERT_RLE: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x202B)); - } - } break; - case MENU_INSERT_LRO: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x202D)); - } - } break; - case MENU_INSERT_RLO: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x202E)); - } - } break; - case MENU_INSERT_PDF: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x202C)); - } - } break; - case MENU_INSERT_ALM: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x061C)); - } - } break; - case MENU_INSERT_LRI: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x2066)); - } - } break; - case MENU_INSERT_RLI: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x2067)); - } - } break; - case MENU_INSERT_FSI: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x2068)); - } - } break; - case MENU_INSERT_PDI: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x2069)); - } - } break; - case MENU_INSERT_ZWJ: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x200D)); - } - } break; - case MENU_INSERT_ZWNJ: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x200C)); - } - } break; - case MENU_INSERT_WJ: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x2060)); - } - } break; - case MENU_INSERT_SHY: { - if (!readonly) { - insert_text_at_cursor(String::chr(0x00AD)); - } +/* Gutters. */ +void TextEdit::_update_gutter_width() { + gutters_width = 0; + for (int i = 0; i < gutters.size(); i++) { + if (gutters[i].draw) { + gutters_width += gutters[i].width; } } -} - -void TextEdit::set_highlighted_word(const String &new_word) { - highlighted_word = new_word; + if (gutters_width > 0) { + gutter_padding = 2; + } update(); } -void TextEdit::set_select_identifiers_on_hover(bool p_enable) { - select_identifiers_enabled = p_enable; -} - -bool TextEdit::is_selecting_identifiers_on_hover_enabled() const { - return select_identifiers_enabled; -} - -void TextEdit::set_context_menu_enabled(bool p_enable) { - context_menu_enabled = p_enable; -} - -bool TextEdit::is_context_menu_enabled() { - return context_menu_enabled; +/* Syntax highlighting. */ +Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) { + return syntax_highlighter.is_null() && !setting_text ? Dictionary() : syntax_highlighter->get_line_syntax_highlighting(p_line); } -void TextEdit::set_shortcut_keys_enabled(bool p_enabled) { - shortcut_keys_enabled = p_enabled; +/*** Super internal Core API. Everything builds on it. ***/ - _generate_context_menu(); -} - -void TextEdit::set_virtual_keyboard_enabled(bool p_enable) { - virtual_keyboard_enabled = p_enable; +void TextEdit::_text_changed_emit() { + emit_signal(SNAME("text_changed")); + text_changed_dirty = false; } -void TextEdit::set_selecting_enabled(bool p_enabled) { - selecting_enabled = p_enabled; - - if (!selecting_enabled) { - deselect(); +void TextEdit::_insert_text(int p_line, int p_char, const String &p_text, int *r_end_line, int *r_end_char) { + if (!setting_text && idle_detect->is_inside_tree()) { + idle_detect->start(); } - _generate_context_menu(); -} + if (undo_enabled) { + _clear_redo(); + } -bool TextEdit::is_selecting_enabled() const { - return selecting_enabled; -} + int retline, retchar; + _base_insert_text(p_line, p_char, p_text, retline, retchar); + if (r_end_line) { + *r_end_line = retline; + } + if (r_end_char) { + *r_end_char = retchar; + } -bool TextEdit::is_shortcut_keys_enabled() const { - return shortcut_keys_enabled; -} + if (!undo_enabled) { + return; + } -bool TextEdit::is_virtual_keyboard_enabled() const { - return virtual_keyboard_enabled; -} + /* UNDO!! */ + TextOperation op; + op.type = TextOperation::TYPE_INSERT; + op.from_line = p_line; + op.from_column = p_char; + op.to_line = retline; + op.to_column = retchar; + op.text = p_text; + op.version = ++version; + op.chain_forward = false; + op.chain_backward = false; -PopupMenu *TextEdit::get_menu() const { - return menu; -} + // See if it should just be set as current op. + if (current_op.type != op.type) { + op.prev_version = get_version(); + _push_current_op(); + current_op = op; -bool TextEdit::_set(const StringName &p_name, const Variant &p_value) { - String str = p_name; - if (str.begins_with("opentype_features/")) { - String name = str.get_slicec('/', 1); - int32_t tag = TS->name_to_tag(name); - double value = p_value; - if (value == -1) { - if (opentype_features.has(tag)) { - opentype_features.erase(tag); - text.set_font_features(opentype_features); - text.invalidate_all(); - update(); - } - } else { - if ((double)opentype_features[tag] != value) { - opentype_features[tag] = value; - text.set_font_features(opentype_features); - text.invalidate_all(); - ; - update(); - } - } - notify_property_list_changed(); - return true; + return; // Set as current op, return. } + // See if it can be merged. + if (current_op.to_line != p_line || current_op.to_column != p_char) { + op.prev_version = get_version(); + _push_current_op(); + current_op = op; + return; // Set as current op, return. + } + // Merge current op. - return false; + current_op.text += p_text; + current_op.to_column = retchar; + current_op.to_line = retline; + current_op.version = op.version; } -bool TextEdit::_get(const StringName &p_name, Variant &r_ret) const { - String str = p_name; - if (str.begins_with("opentype_features/")) { - String name = str.get_slicec('/', 1); - int32_t tag = TS->name_to_tag(name); - if (opentype_features.has(tag)) { - r_ret = opentype_features[tag]; - return true; - } else { - r_ret = -1; - return true; - } +void TextEdit::_remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { + if (!setting_text && idle_detect->is_inside_tree()) { + idle_detect->start(); } - return false; -} -void TextEdit::_get_property_list(List<PropertyInfo> *p_list) const { - for (const Variant *ftr = opentype_features.next(nullptr); ftr != nullptr; ftr = opentype_features.next(ftr)) { - String name = TS->tag_to_name(*ftr); - p_list->push_back(PropertyInfo(Variant::FLOAT, "opentype_features/" + name)); + String text; + if (undo_enabled) { + _clear_redo(); + text = _base_get_text(p_from_line, p_from_column, p_to_line, p_to_column); } - p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); -} - -void TextEdit::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &TextEdit::_gui_input); - ClassDB::bind_method(D_METHOD("_cursor_changed_emit"), &TextEdit::_cursor_changed_emit); - ClassDB::bind_method(D_METHOD("_text_changed_emit"), &TextEdit::_text_changed_emit); - ClassDB::bind_method(D_METHOD("_update_wrap_at", "force"), &TextEdit::_update_wrap_at, DEFVAL(false)); - - BIND_ENUM_CONSTANT(SEARCH_MATCH_CASE); - BIND_ENUM_CONSTANT(SEARCH_WHOLE_WORDS); - BIND_ENUM_CONSTANT(SEARCH_BACKWARDS); - - BIND_ENUM_CONSTANT(SELECTION_MODE_NONE); - BIND_ENUM_CONSTANT(SELECTION_MODE_SHIFT); - BIND_ENUM_CONSTANT(SELECTION_MODE_POINTER); - BIND_ENUM_CONSTANT(SELECTION_MODE_WORD); - BIND_ENUM_CONSTANT(SELECTION_MODE_LINE); - - /* - ClassDB::bind_method(D_METHOD("delete_char"),&TextEdit::delete_char); - ClassDB::bind_method(D_METHOD("delete_line"),&TextEdit::delete_line); -*/ - - ClassDB::bind_method(D_METHOD("get_draw_control_chars"), &TextEdit::get_draw_control_chars); - ClassDB::bind_method(D_METHOD("set_draw_control_chars", "enable"), &TextEdit::set_draw_control_chars); - ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &TextEdit::set_text_direction); - ClassDB::bind_method(D_METHOD("get_text_direction"), &TextEdit::get_text_direction); - ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &TextEdit::set_opentype_feature); - ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &TextEdit::get_opentype_feature); - ClassDB::bind_method(D_METHOD("clear_opentype_features"), &TextEdit::clear_opentype_features); - ClassDB::bind_method(D_METHOD("set_language", "language"), &TextEdit::set_language); - ClassDB::bind_method(D_METHOD("get_language"), &TextEdit::get_language); - ClassDB::bind_method(D_METHOD("set_text", "text"), &TextEdit::set_text); - ClassDB::bind_method(D_METHOD("insert_text_at_cursor", "text"), &TextEdit::insert_text_at_cursor); - - ClassDB::bind_method(D_METHOD("get_line_count"), &TextEdit::get_line_count); - ClassDB::bind_method(D_METHOD("get_text"), &TextEdit::get_text); - ClassDB::bind_method(D_METHOD("get_line", "line"), &TextEdit::get_line); - ClassDB::bind_method(D_METHOD("get_visible_line_count"), &TextEdit::get_total_visible_rows); - ClassDB::bind_method(D_METHOD("set_line", "line", "new_text"), &TextEdit::set_line); - - ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &TextEdit::set_structured_text_bidi_override); - ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &TextEdit::get_structured_text_bidi_override); - ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &TextEdit::set_structured_text_bidi_override_options); - ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &TextEdit::get_structured_text_bidi_override_options); - - ClassDB::bind_method(D_METHOD("center_viewport_to_cursor"), &TextEdit::center_viewport_to_cursor); - ClassDB::bind_method(D_METHOD("cursor_set_column", "column", "adjust_viewport"), &TextEdit::cursor_set_column, DEFVAL(true)); - ClassDB::bind_method(D_METHOD("cursor_set_line", "line", "adjust_viewport", "can_be_hidden", "wrap_index"), &TextEdit::cursor_set_line, DEFVAL(true), DEFVAL(true), DEFVAL(0)); - - ClassDB::bind_method(D_METHOD("cursor_get_column"), &TextEdit::cursor_get_column); - ClassDB::bind_method(D_METHOD("cursor_get_line"), &TextEdit::cursor_get_line); - ClassDB::bind_method(D_METHOD("cursor_set_blink_enabled", "enable"), &TextEdit::cursor_set_blink_enabled); - ClassDB::bind_method(D_METHOD("cursor_get_blink_enabled"), &TextEdit::cursor_get_blink_enabled); - ClassDB::bind_method(D_METHOD("cursor_set_blink_speed", "blink_speed"), &TextEdit::cursor_set_blink_speed); - ClassDB::bind_method(D_METHOD("cursor_get_blink_speed"), &TextEdit::cursor_get_blink_speed); - ClassDB::bind_method(D_METHOD("cursor_set_block_mode", "enable"), &TextEdit::cursor_set_block_mode); - ClassDB::bind_method(D_METHOD("cursor_is_block_mode"), &TextEdit::cursor_is_block_mode); - - ClassDB::bind_method(D_METHOD("set_mid_grapheme_caret_enabled", "enabled"), &TextEdit::set_mid_grapheme_caret_enabled); - ClassDB::bind_method(D_METHOD("get_mid_grapheme_caret_enabled"), &TextEdit::get_mid_grapheme_caret_enabled); - - ClassDB::bind_method(D_METHOD("set_right_click_moves_caret", "enable"), &TextEdit::set_right_click_moves_caret); - ClassDB::bind_method(D_METHOD("is_right_click_moving_caret"), &TextEdit::is_right_click_moving_caret); + _base_remove_text(p_from_line, p_from_column, p_to_line, p_to_column); - ClassDB::bind_method(D_METHOD("get_selection_mode"), &TextEdit::get_selection_mode); - ClassDB::bind_method(D_METHOD("set_selection_mode", "mode", "line", "column"), &TextEdit::set_selection_mode, DEFVAL(-1), DEFVAL(-1)); - ClassDB::bind_method(D_METHOD("get_selection_line"), &TextEdit::get_selection_line); - ClassDB::bind_method(D_METHOD("get_selection_column"), &TextEdit::get_selection_column); + if (!undo_enabled) { + return; + } - ClassDB::bind_method(D_METHOD("set_readonly", "enable"), &TextEdit::set_readonly); - ClassDB::bind_method(D_METHOD("is_readonly"), &TextEdit::is_readonly); + /* UNDO! */ + TextOperation op; + op.type = TextOperation::TYPE_REMOVE; + op.from_line = p_from_line; + op.from_column = p_from_column; + op.to_line = p_to_line; + op.to_column = p_to_column; + op.text = text; + op.version = ++version; + op.chain_forward = false; + op.chain_backward = false; - ClassDB::bind_method(D_METHOD("set_wrap_enabled", "enable"), &TextEdit::set_wrap_enabled); - ClassDB::bind_method(D_METHOD("is_wrap_enabled"), &TextEdit::is_wrap_enabled); - ClassDB::bind_method(D_METHOD("set_context_menu_enabled", "enable"), &TextEdit::set_context_menu_enabled); - ClassDB::bind_method(D_METHOD("is_context_menu_enabled"), &TextEdit::is_context_menu_enabled); - ClassDB::bind_method(D_METHOD("set_shortcut_keys_enabled", "enable"), &TextEdit::set_shortcut_keys_enabled); - ClassDB::bind_method(D_METHOD("is_shortcut_keys_enabled"), &TextEdit::is_shortcut_keys_enabled); - ClassDB::bind_method(D_METHOD("set_virtual_keyboard_enabled", "enable"), &TextEdit::set_virtual_keyboard_enabled); - ClassDB::bind_method(D_METHOD("is_virtual_keyboard_enabled"), &TextEdit::is_virtual_keyboard_enabled); - ClassDB::bind_method(D_METHOD("set_selecting_enabled", "enable"), &TextEdit::set_selecting_enabled); - ClassDB::bind_method(D_METHOD("is_selecting_enabled"), &TextEdit::is_selecting_enabled); + // See if it should just be set as current op. + if (current_op.type != op.type) { + op.prev_version = get_version(); + _push_current_op(); + current_op = op; + return; // Set as current op, return. + } + // See if it can be merged. + if (current_op.from_line == p_to_line && current_op.from_column == p_to_column) { + // Backspace or similar. + current_op.text = text + current_op.text; + current_op.from_line = p_from_line; + current_op.from_column = p_from_column; + return; // Update current op. + } - ClassDB::bind_method(D_METHOD("cut"), &TextEdit::cut); - ClassDB::bind_method(D_METHOD("copy"), &TextEdit::copy); - ClassDB::bind_method(D_METHOD("paste"), &TextEdit::paste); + op.prev_version = get_version(); + _push_current_op(); + current_op = op; +} - ClassDB::bind_method(D_METHOD("select", "from_line", "from_column", "to_line", "to_column"), &TextEdit::select); - ClassDB::bind_method(D_METHOD("select_all"), &TextEdit::select_all); - ClassDB::bind_method(D_METHOD("deselect"), &TextEdit::deselect); +void TextEdit::_base_insert_text(int p_line, int p_char, const String &p_text, int &r_end_line, int &r_end_column) { + // Save for undo. + ERR_FAIL_INDEX(p_line, text.size()); + ERR_FAIL_COND(p_char < 0); - ClassDB::bind_method(D_METHOD("is_selection_active"), &TextEdit::is_selection_active); - ClassDB::bind_method(D_METHOD("get_selection_from_line"), &TextEdit::get_selection_from_line); - ClassDB::bind_method(D_METHOD("get_selection_from_column"), &TextEdit::get_selection_from_column); - ClassDB::bind_method(D_METHOD("get_selection_to_line"), &TextEdit::get_selection_to_line); - ClassDB::bind_method(D_METHOD("get_selection_to_column"), &TextEdit::get_selection_to_column); - ClassDB::bind_method(D_METHOD("get_selection_text"), &TextEdit::get_selection_text); - ClassDB::bind_method(D_METHOD("get_word_under_cursor"), &TextEdit::get_word_under_cursor); - ClassDB::bind_method(D_METHOD("search", "key", "flags", "from_line", "from_column"), &TextEdit::_search_bind); + /* STEP 1: Remove \r from source text and separate in substrings. */ - ClassDB::bind_method(D_METHOD("undo"), &TextEdit::undo); - ClassDB::bind_method(D_METHOD("redo"), &TextEdit::redo); - ClassDB::bind_method(D_METHOD("clear_undo_history"), &TextEdit::clear_undo_history); + Vector<String> substrings = p_text.replace("\r", "").split("\n"); - ClassDB::bind_method(D_METHOD("set_draw_tabs"), &TextEdit::set_draw_tabs); - ClassDB::bind_method(D_METHOD("is_drawing_tabs"), &TextEdit::is_drawing_tabs); - ClassDB::bind_method(D_METHOD("set_draw_spaces"), &TextEdit::set_draw_spaces); - ClassDB::bind_method(D_METHOD("is_drawing_spaces"), &TextEdit::is_drawing_spaces); + // Is this just a new empty line? + bool shift_first_line = p_char == 0 && p_text.replace("\r", "") == "\n"; - ClassDB::bind_method(D_METHOD("set_hiding_enabled", "enable"), &TextEdit::set_hiding_enabled); - ClassDB::bind_method(D_METHOD("is_hiding_enabled"), &TextEdit::is_hiding_enabled); - ClassDB::bind_method(D_METHOD("set_line_as_hidden", "line", "enable"), &TextEdit::set_line_as_hidden); - ClassDB::bind_method(D_METHOD("is_line_hidden", "line"), &TextEdit::is_line_hidden); - ClassDB::bind_method(D_METHOD("fold_all_lines"), &TextEdit::fold_all_lines); - ClassDB::bind_method(D_METHOD("unhide_all_lines"), &TextEdit::unhide_all_lines); - ClassDB::bind_method(D_METHOD("fold_line", "line"), &TextEdit::fold_line); - ClassDB::bind_method(D_METHOD("unfold_line", "line"), &TextEdit::unfold_line); - ClassDB::bind_method(D_METHOD("toggle_fold_line", "line"), &TextEdit::toggle_fold_line); - ClassDB::bind_method(D_METHOD("can_fold", "line"), &TextEdit::can_fold); - ClassDB::bind_method(D_METHOD("is_folded", "line"), &TextEdit::is_folded); + /* STEP 2: Add spaces if the char is greater than the end of the line. */ + while (p_char > text[p_line].length()) { + text.set(p_line, text[p_line] + String::chr(' '), structured_text_parser(st_parser, st_args, text[p_line] + String::chr(' '))); + } - ClassDB::bind_method(D_METHOD("set_highlight_all_occurrences", "enable"), &TextEdit::set_highlight_all_occurrences); - ClassDB::bind_method(D_METHOD("is_highlight_all_occurrences_enabled"), &TextEdit::is_highlight_all_occurrences_enabled); + /* STEP 3: Separate dest string in pre and post text. */ - ClassDB::bind_method(D_METHOD("set_override_selected_font_color", "override"), &TextEdit::set_override_selected_font_color); - ClassDB::bind_method(D_METHOD("is_overriding_selected_font_color"), &TextEdit::is_overriding_selected_font_color); + String preinsert_text = text[p_line].substr(0, p_char); + String postinsert_text = text[p_line].substr(p_char, text[p_line].size()); - ClassDB::bind_method(D_METHOD("set_syntax_highlighter", "syntax_highlighter"), &TextEdit::set_syntax_highlighter); - ClassDB::bind_method(D_METHOD("get_syntax_highlighter"), &TextEdit::get_syntax_highlighter); + for (int j = 0; j < substrings.size(); j++) { + // Insert the substrings. - /* Gutters. */ - BIND_ENUM_CONSTANT(GUTTER_TYPE_STRING); - BIND_ENUM_CONSTANT(GUTTER_TPYE_ICON); - BIND_ENUM_CONSTANT(GUTTER_TPYE_CUSTOM); + if (j == 0) { + text.set(p_line, preinsert_text + substrings[j], structured_text_parser(st_parser, st_args, preinsert_text + substrings[j])); + } else { + text.insert(p_line + j, substrings[j], structured_text_parser(st_parser, st_args, substrings[j])); + } - ClassDB::bind_method(D_METHOD("add_gutter", "at"), &TextEdit::add_gutter, DEFVAL(-1)); - ClassDB::bind_method(D_METHOD("remove_gutter", "gutter"), &TextEdit::remove_gutter); - ClassDB::bind_method(D_METHOD("get_gutter_count"), &TextEdit::get_gutter_count); - ClassDB::bind_method(D_METHOD("set_gutter_name", "gutter", "name"), &TextEdit::set_gutter_name); - ClassDB::bind_method(D_METHOD("get_gutter_name", "gutter"), &TextEdit::get_gutter_name); - ClassDB::bind_method(D_METHOD("set_gutter_type", "gutter", "type"), &TextEdit::set_gutter_type); - ClassDB::bind_method(D_METHOD("get_gutter_type", "gutter"), &TextEdit::get_gutter_type); - ClassDB::bind_method(D_METHOD("set_gutter_width", "gutter", "width"), &TextEdit::set_gutter_width); - ClassDB::bind_method(D_METHOD("get_gutter_width", "gutter"), &TextEdit::get_gutter_width); - ClassDB::bind_method(D_METHOD("set_gutter_draw", "gutter", "draw"), &TextEdit::set_gutter_draw); - ClassDB::bind_method(D_METHOD("is_gutter_drawn", "gutter"), &TextEdit::is_gutter_drawn); - ClassDB::bind_method(D_METHOD("set_gutter_clickable", "gutter", "clickable"), &TextEdit::set_gutter_clickable); - ClassDB::bind_method(D_METHOD("is_gutter_clickable", "gutter"), &TextEdit::is_gutter_clickable); - ClassDB::bind_method(D_METHOD("set_gutter_overwritable", "gutter", "overwritable"), &TextEdit::set_gutter_overwritable); - ClassDB::bind_method(D_METHOD("is_gutter_overwritable", "gutter"), &TextEdit::is_gutter_overwritable); - ClassDB::bind_method(D_METHOD("set_gutter_custom_draw", "column", "object", "callback"), &TextEdit::set_gutter_custom_draw); + if (j == substrings.size() - 1) { + text.set(p_line + j, text[p_line + j] + postinsert_text, structured_text_parser(st_parser, st_args, text[p_line + j] + postinsert_text)); + } + } - // Line gutters. - ClassDB::bind_method(D_METHOD("set_line_gutter_metadata", "line", "gutter", "metadata"), &TextEdit::set_line_gutter_metadata); - ClassDB::bind_method(D_METHOD("get_line_gutter_metadata", "line", "gutter"), &TextEdit::get_line_gutter_metadata); - ClassDB::bind_method(D_METHOD("set_line_gutter_text", "line", "gutter", "text"), &TextEdit::set_line_gutter_text); - ClassDB::bind_method(D_METHOD("get_line_gutter_text", "line", "gutter"), &TextEdit::get_line_gutter_text); - ClassDB::bind_method(D_METHOD("set_line_gutter_icon", "line", "gutter", "icon"), &TextEdit::set_line_gutter_icon); - ClassDB::bind_method(D_METHOD("get_line_gutter_icon", "line", "gutter"), &TextEdit::get_line_gutter_icon); - ClassDB::bind_method(D_METHOD("set_line_gutter_item_color", "line", "gutter", "color"), &TextEdit::set_line_gutter_item_color); - ClassDB::bind_method(D_METHOD("get_line_gutter_item_color", "line", "gutter"), &TextEdit::get_line_gutter_item_color); - ClassDB::bind_method(D_METHOD("set_line_gutter_clickable", "line", "gutter", "clickable"), &TextEdit::set_line_gutter_clickable); - ClassDB::bind_method(D_METHOD("is_line_gutter_clickable", "line", "gutter"), &TextEdit::is_line_gutter_clickable); + if (shift_first_line) { + text.move_gutters(p_line, p_line + 1); + text.set_hidden(p_line + 1, text.is_hidden(p_line)); - // Line style - ClassDB::bind_method(D_METHOD("set_line_background_color", "line", "color"), &TextEdit::set_line_background_color); - ClassDB::bind_method(D_METHOD("get_line_background_color", "line"), &TextEdit::get_line_background_color); + text.set_hidden(p_line, false); + } - ClassDB::bind_method(D_METHOD("set_highlight_current_line", "enabled"), &TextEdit::set_highlight_current_line); - ClassDB::bind_method(D_METHOD("is_highlight_current_line_enabled"), &TextEdit::is_highlight_current_line_enabled); + r_end_line = p_line + substrings.size() - 1; + r_end_column = text[r_end_line].length() - postinsert_text.length(); - ClassDB::bind_method(D_METHOD("set_smooth_scroll_enable", "enable"), &TextEdit::set_smooth_scroll_enabled); - ClassDB::bind_method(D_METHOD("is_smooth_scroll_enabled"), &TextEdit::is_smooth_scroll_enabled); - ClassDB::bind_method(D_METHOD("set_v_scroll_speed", "speed"), &TextEdit::set_v_scroll_speed); - ClassDB::bind_method(D_METHOD("get_v_scroll_speed"), &TextEdit::get_v_scroll_speed); - ClassDB::bind_method(D_METHOD("set_v_scroll", "value"), &TextEdit::set_v_scroll); - ClassDB::bind_method(D_METHOD("get_v_scroll"), &TextEdit::get_v_scroll); - ClassDB::bind_method(D_METHOD("set_h_scroll", "value"), &TextEdit::set_h_scroll); - ClassDB::bind_method(D_METHOD("get_h_scroll"), &TextEdit::get_h_scroll); + TextServer::Direction dir = TS->shaped_text_get_dominant_direciton_in_range(text.get_line_data(r_end_line)->get_rid(), (r_end_line == p_line) ? caret.column : 0, r_end_column); + if (dir != TextServer::DIRECTION_AUTO) { + input_direction = (TextDirection)dir; + } - ClassDB::bind_method(D_METHOD("menu_option", "option"), &TextEdit::menu_option); - ClassDB::bind_method(D_METHOD("get_menu"), &TextEdit::get_menu); + if (!text_changed_dirty && !setting_text) { + if (is_inside_tree()) { + MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); + } + text_changed_dirty = true; + } + emit_signal(SNAME("lines_edited_from"), p_line, r_end_line); +} - ClassDB::bind_method(D_METHOD("draw_minimap", "draw"), &TextEdit::set_draw_minimap); - ClassDB::bind_method(D_METHOD("is_drawing_minimap"), &TextEdit::is_drawing_minimap); - ClassDB::bind_method(D_METHOD("set_minimap_width", "width"), &TextEdit::set_minimap_width); - ClassDB::bind_method(D_METHOD("get_minimap_width"), &TextEdit::get_minimap_width); +String TextEdit::_base_get_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) const { + ERR_FAIL_INDEX_V(p_from_line, text.size(), String()); + ERR_FAIL_INDEX_V(p_from_column, text[p_from_line].length() + 1, String()); + ERR_FAIL_INDEX_V(p_to_line, text.size(), String()); + ERR_FAIL_INDEX_V(p_to_column, text[p_to_line].length() + 1, String()); + ERR_FAIL_COND_V(p_to_line < p_from_line, String()); // 'from > to'. + ERR_FAIL_COND_V(p_to_line == p_from_line && p_to_column < p_from_column, String()); // 'from > to'. - ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction"); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_control_chars"), "set_draw_control_chars", "get_draw_control_chars"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "readonly"), "set_readonly", "is_readonly"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "highlight_current_line"), "set_highlight_current_line", "is_highlight_current_line_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_tabs"), "set_draw_tabs", "is_drawing_tabs"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_spaces"), "set_draw_spaces", "is_drawing_spaces"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "highlight_all_occurrences"), "set_highlight_all_occurrences", "is_highlight_all_occurrences_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "override_selected_font_color"), "set_override_selected_font_color", "is_overriding_selected_font_color"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "context_menu_enabled"), "set_context_menu_enabled", "is_context_menu_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shortcut_keys_enabled"), "set_shortcut_keys_enabled", "is_shortcut_keys_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "virtual_keyboard_enabled"), "set_virtual_keyboard_enabled", "is_virtual_keyboard_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selecting_enabled"), "set_selecting_enabled", "is_selecting_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "smooth_scrolling"), "set_smooth_scroll_enable", "is_smooth_scroll_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "v_scroll_speed"), "set_v_scroll_speed", "get_v_scroll_speed"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hiding_enabled"), "set_hiding_enabled", "is_hiding_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "wrap_enabled"), "set_wrap_enabled", "is_wrap_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "scroll_vertical"), "set_v_scroll", "get_v_scroll"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "scroll_horizontal"), "set_h_scroll", "get_h_scroll"); + String ret; - ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "syntax_highlighter", PROPERTY_HINT_RESOURCE_TYPE, "SyntaxHighlighter", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_DO_NOT_SHARE_ON_DUPLICATE), "set_syntax_highlighter", "get_syntax_highlighter"); + for (int i = p_from_line; i <= p_to_line; i++) { + int begin = (i == p_from_line) ? p_from_column : 0; + int end = (i == p_to_line) ? p_to_column : text[i].length(); - ADD_GROUP("Minimap", "minimap_"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "minimap_draw"), "draw_minimap", "is_drawing_minimap"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "minimap_width"), "set_minimap_width", "get_minimap_width"); + if (i > p_from_line) { + ret += "\n"; + } + ret += text[i].substr(begin, end - begin); + } - ADD_GROUP("Caret", "caret_"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_block_mode"), "cursor_set_block_mode", "cursor_is_block_mode"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_blink"), "cursor_set_blink_enabled", "cursor_get_blink_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "caret_blink_speed", PROPERTY_HINT_RANGE, "0.1,10,0.01"), "cursor_set_blink_speed", "cursor_get_blink_speed"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_moving_by_right_click"), "set_right_click_moves_caret", "is_right_click_moving_caret"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_mid_grapheme"), "set_mid_grapheme_caret_enabled", "get_mid_grapheme_caret_enabled"); + return ret; +} - ADD_GROUP("Structured Text", "structured_text_"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "structured_text_bidi_override", PROPERTY_HINT_ENUM, "Default,URI,File,Email,List,None,Custom"), "set_structured_text_bidi_override", "get_structured_text_bidi_override"); - ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "structured_text_bidi_override_options"), "set_structured_text_bidi_override_options", "get_structured_text_bidi_override_options"); +void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { + ERR_FAIL_INDEX(p_from_line, text.size()); + ERR_FAIL_INDEX(p_from_column, text[p_from_line].length() + 1); + ERR_FAIL_INDEX(p_to_line, text.size()); + ERR_FAIL_INDEX(p_to_column, text[p_to_line].length() + 1); + ERR_FAIL_COND(p_to_line < p_from_line); // 'from > to'. + ERR_FAIL_COND(p_to_line == p_from_line && p_to_column < p_from_column); // 'from > to'. - ADD_SIGNAL(MethodInfo("cursor_changed")); - ADD_SIGNAL(MethodInfo("text_changed")); - ADD_SIGNAL(MethodInfo("lines_edited_from", PropertyInfo(Variant::INT, "from_line"), PropertyInfo(Variant::INT, "to_line"))); - ADD_SIGNAL(MethodInfo("request_completion")); - ADD_SIGNAL(MethodInfo("gutter_clicked", PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::INT, "gutter"))); - ADD_SIGNAL(MethodInfo("gutter_added")); - ADD_SIGNAL(MethodInfo("gutter_removed")); - ADD_SIGNAL(MethodInfo("symbol_lookup", PropertyInfo(Variant::STRING, "symbol"), PropertyInfo(Variant::INT, "row"), PropertyInfo(Variant::INT, "column"))); - ADD_SIGNAL(MethodInfo("symbol_validate", PropertyInfo(Variant::STRING, "symbol"))); + String pre_text = text[p_from_line].substr(0, p_from_column); + String post_text = text[p_to_line].substr(p_to_column, text[p_to_line].length()); - BIND_ENUM_CONSTANT(MENU_CUT); - BIND_ENUM_CONSTANT(MENU_COPY); - BIND_ENUM_CONSTANT(MENU_PASTE); - BIND_ENUM_CONSTANT(MENU_CLEAR); - BIND_ENUM_CONSTANT(MENU_SELECT_ALL); - BIND_ENUM_CONSTANT(MENU_UNDO); - BIND_ENUM_CONSTANT(MENU_REDO); - BIND_ENUM_CONSTANT(MENU_DIR_INHERITED); - BIND_ENUM_CONSTANT(MENU_DIR_AUTO); - BIND_ENUM_CONSTANT(MENU_DIR_LTR); - BIND_ENUM_CONSTANT(MENU_DIR_RTL); - BIND_ENUM_CONSTANT(MENU_DISPLAY_UCC); - BIND_ENUM_CONSTANT(MENU_INSERT_LRM); - BIND_ENUM_CONSTANT(MENU_INSERT_RLM); - BIND_ENUM_CONSTANT(MENU_INSERT_LRE); - BIND_ENUM_CONSTANT(MENU_INSERT_RLE); - BIND_ENUM_CONSTANT(MENU_INSERT_LRO); - BIND_ENUM_CONSTANT(MENU_INSERT_RLO); - BIND_ENUM_CONSTANT(MENU_INSERT_PDF); - BIND_ENUM_CONSTANT(MENU_INSERT_ALM); - BIND_ENUM_CONSTANT(MENU_INSERT_LRI); - BIND_ENUM_CONSTANT(MENU_INSERT_RLI); - BIND_ENUM_CONSTANT(MENU_INSERT_FSI); - BIND_ENUM_CONSTANT(MENU_INSERT_PDI); - BIND_ENUM_CONSTANT(MENU_INSERT_ZWJ); - BIND_ENUM_CONSTANT(MENU_INSERT_ZWNJ); - BIND_ENUM_CONSTANT(MENU_INSERT_WJ); - BIND_ENUM_CONSTANT(MENU_INSERT_SHY); - BIND_ENUM_CONSTANT(MENU_MAX); + for (int i = p_from_line; i < p_to_line; i++) { + text.remove(p_from_line + 1); + } + text.set(p_from_line, pre_text + post_text, structured_text_parser(st_parser, st_args, pre_text + post_text)); - GLOBAL_DEF("gui/timers/text_edit_idle_detect_sec", 3); - ProjectSettings::get_singleton()->set_custom_property_info("gui/timers/text_edit_idle_detect_sec", PropertyInfo(Variant::FLOAT, "gui/timers/text_edit_idle_detect_sec", PROPERTY_HINT_RANGE, "0,10,0.01,or_greater")); // No negative numbers. - GLOBAL_DEF("gui/common/text_edit_undo_stack_max_size", 1024); - ProjectSettings::get_singleton()->set_custom_property_info("gui/common/text_edit_undo_stack_max_size", PropertyInfo(Variant::INT, "gui/common/text_edit_undo_stack_max_size", PROPERTY_HINT_RANGE, "0,10000,1,or_greater")); // No negative numbers. + if (!text_changed_dirty && !setting_text) { + if (is_inside_tree()) { + MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); + } + text_changed_dirty = true; + } + emit_signal(SNAME("lines_edited_from"), p_to_line, p_from_line); } TextEdit::TextEdit() { @@ -7144,84 +6069,39 @@ TextEdit::TextEdit() { _update_caches(); set_default_cursor_shape(CURSOR_IBEAM); - text.set_indent_size(indent_size); - text.clear(); + text.set_tab_size(text.get_tab_size()); h_scroll = memnew(HScrollBar); v_scroll = memnew(VScrollBar); - add_child(h_scroll); - add_child(v_scroll); + add_child(h_scroll, false, INTERNAL_MODE_FRONT); + add_child(v_scroll, false, INTERNAL_MODE_FRONT); h_scroll->connect("value_changed", callable_mp(this, &TextEdit::_scroll_moved)); v_scroll->connect("value_changed", callable_mp(this, &TextEdit::_scroll_moved)); v_scroll->connect("scrolling", callable_mp(this, &TextEdit::_v_scroll_input)); + /* Caret. */ caret_blink_timer = memnew(Timer); - add_child(caret_blink_timer); + add_child(caret_blink_timer, false, INTERNAL_MODE_FRONT); caret_blink_timer->set_wait_time(0.65); caret_blink_timer->connect("timeout", callable_mp(this, &TextEdit::_toggle_draw_caret)); - cursor_set_blink_enabled(false); + set_caret_blink_enabled(false); + + /* Selection. */ + click_select_held = memnew(Timer); + add_child(click_select_held, false, INTERNAL_MODE_FRONT); + click_select_held->set_wait_time(0.05); + click_select_held->connect("timeout", callable_mp(this, &TextEdit::_click_selection_held)); idle_detect = memnew(Timer); - add_child(idle_detect); + add_child(idle_detect, false, INTERNAL_MODE_FRONT); idle_detect->set_one_shot(true); idle_detect->set_wait_time(GLOBAL_GET("gui/timers/text_edit_idle_detect_sec")); idle_detect->connect("timeout", callable_mp(this, &TextEdit::_push_current_op)); - click_select_held = memnew(Timer); - add_child(click_select_held); - click_select_held->set_wait_time(0.05); - click_select_held->connect("timeout", callable_mp(this, &TextEdit::_click_selection_held)); - undo_stack_max_size = GLOBAL_GET("gui/common/text_edit_undo_stack_max_size"); - menu = memnew(PopupMenu); - add_child(menu); - - menu_dir = memnew(PopupMenu); - menu_dir->set_name("DirMenu"); - menu_dir->add_radio_check_item(RTR("Same as layout direction"), MENU_DIR_INHERITED); - menu_dir->add_radio_check_item(RTR("Auto-detect direction"), MENU_DIR_AUTO); - menu_dir->add_radio_check_item(RTR("Left-to-right"), MENU_DIR_LTR); - menu_dir->add_radio_check_item(RTR("Right-to-left"), MENU_DIR_RTL); - menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), true); - menu->add_child(menu_dir); - - menu_ctl = memnew(PopupMenu); - menu_ctl->set_name("CTLMenu"); - menu_ctl->add_item(RTR("Left-to-right mark (LRM)"), MENU_INSERT_LRM); - menu_ctl->add_item(RTR("Right-to-left mark (RLM)"), MENU_INSERT_RLM); - menu_ctl->add_item(RTR("Start of left-to-right embedding (LRE)"), MENU_INSERT_LRE); - menu_ctl->add_item(RTR("Start of right-to-left embedding (RLE)"), MENU_INSERT_RLE); - menu_ctl->add_item(RTR("Start of left-to-right override (LRO)"), MENU_INSERT_LRO); - menu_ctl->add_item(RTR("Start of right-to-left override (RLO)"), MENU_INSERT_RLO); - menu_ctl->add_item(RTR("Pop direction formatting (PDF)"), MENU_INSERT_PDF); - menu_ctl->add_separator(); - menu_ctl->add_item(RTR("Arabic letter mark (ALM)"), MENU_INSERT_ALM); - menu_ctl->add_item(RTR("Left-to-right isolate (LRI)"), MENU_INSERT_LRI); - menu_ctl->add_item(RTR("Right-to-left isolate (RLI)"), MENU_INSERT_RLI); - menu_ctl->add_item(RTR("First strong isolate (FSI)"), MENU_INSERT_FSI); - menu_ctl->add_item(RTR("Pop direction isolate (PDI)"), MENU_INSERT_PDI); - menu_ctl->add_separator(); - menu_ctl->add_item(RTR("Zero width joiner (ZWJ)"), MENU_INSERT_ZWJ); - menu_ctl->add_item(RTR("Zero width non-joiner (ZWNJ)"), MENU_INSERT_ZWNJ); - menu_ctl->add_item(RTR("Word joiner (WJ)"), MENU_INSERT_WJ); - menu_ctl->add_item(RTR("Soft hyphen (SHY)"), MENU_INSERT_SHY); - menu->add_child(menu_ctl); - - set_readonly(false); - menu->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); - menu_dir->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); - menu_ctl->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); -} - -TextEdit::~TextEdit() { -} - -/////////////////////////////////////////////////////////////////////////////// - -Dictionary TextEdit::_get_line_syntax_highlighting(int p_line) { - return syntax_highlighter.is_null() && !setting_text ? Dictionary() : syntax_highlighter->get_line_syntax_highlighting(p_line); + set_editable(true); } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 6ca50f3e2d..b1226f2aff 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -42,12 +42,13 @@ class TextEdit : public Control { GDCLASS(TextEdit, Control); public: - enum GutterType { - GUTTER_TYPE_STRING, - GUTTER_TPYE_ICON, - GUTTER_TPYE_CUSTOM + /* Caret. */ + enum CaretType { + CARET_TYPE_LINE, + CARET_TYPE_BLOCK }; + /* Selection */ enum SelectionMode { SELECTION_MODE_NONE, SELECTION_MODE_SHIFT, @@ -56,6 +57,60 @@ public: SELECTION_MODE_LINE }; + /* Line Wrapping.*/ + enum LineWrappingMode { + LINE_WRAPPING_NONE, + LINE_WRAPPING_BOUNDARY + }; + + /* Gutters. */ + enum GutterType { + GUTTER_TYPE_STRING, + GUTTER_TYPE_ICON, + GUTTER_TYPE_CUSTOM + }; + + /* Contex Menu. */ + enum MenuItems { + MENU_CUT, + MENU_COPY, + MENU_PASTE, + MENU_CLEAR, + MENU_SELECT_ALL, + MENU_UNDO, + MENU_REDO, + MENU_DIR_INHERITED, + MENU_DIR_AUTO, + MENU_DIR_LTR, + MENU_DIR_RTL, + MENU_DISPLAY_UCC, + MENU_INSERT_LRM, + MENU_INSERT_RLM, + MENU_INSERT_LRE, + MENU_INSERT_RLE, + MENU_INSERT_LRO, + MENU_INSERT_RLO, + MENU_INSERT_PDF, + MENU_INSERT_ALM, + MENU_INSERT_LRI, + MENU_INSERT_RLI, + MENU_INSERT_FSI, + MENU_INSERT_PDI, + MENU_INSERT_ZWJ, + MENU_INSERT_ZWNJ, + MENU_INSERT_WJ, + MENU_INSERT_SHY, + MENU_MAX + + }; + + /* Search. */ + enum SearchFlags { + SEARCH_MATCH_CASE = 1, + SEARCH_WHOLE_WORDS = 2, + SEARCH_BACKWARDS = 4 + }; + private: struct GutterInfo { GutterType type = GutterType::GUTTER_TYPE_STRING; @@ -68,11 +123,6 @@ private: ObjectID custom_draw_obj = ObjectID(); StringName custom_draw_callback; }; - Vector<GutterInfo> gutters; - int gutters_width = 0; - int gutter_padding = 0; - - void _update_gutter_width(); class Text { public: @@ -94,13 +144,18 @@ private: Color background_color = Color(0, 0, 0, 0); bool hidden = false; + int height = 0; + int width = 0; Line() { - data_buf.instance(); + data_buf.instantiate(); } }; private: + bool is_dirty = false; + bool tab_size_dirty = false; + mutable Vector<Line> text; Ref<Font> font; int font_size = -1; @@ -110,22 +165,28 @@ private: TextServer::Direction direction = TextServer::DIRECTION_AUTO; bool draw_control_chars = false; + int line_height = -1; + int max_width = -1; int width = -1; - int indent_size = 4; + int tab_size = 4; int gutter_count = 0; + void _calculate_line_height(); + void _calculate_max_line_width(); + public: - void set_indent_size(int p_indent_size); + void set_tab_size(int p_tab_size); + int get_tab_size() const; void set_font(const Ref<Font> &p_font); void set_font_size(int p_font_size); void set_font_features(const Dictionary &p_features); - void set_direction_and_language(TextServer::Direction p_direction, String p_language); + void set_direction_and_language(TextServer::Direction p_direction, const String &p_language); void set_draw_control_chars(bool p_draw_control_chars); - int get_line_height(int p_line, int p_wrap_index) const; - int get_line_width(int p_line) const; - int get_max_width(bool p_exclude_hidden = false) const; + int get_line_height() const; + int get_line_width(int p_line, int p_wrap_index = -1) const; + int get_max_width() const; void set_width(float p_width); int get_line_wrap_amount(int p_line) const; @@ -134,7 +195,14 @@ private: const Ref<TextParagraph> get_line_data(int p_line) const; void set(int p_line, const String &p_text, const Vector<Vector2i> &p_bidi_override); - void set_hidden(int p_line, bool p_hidden) { text.write[p_line].hidden = p_hidden; } + void set_hidden(int p_line, bool p_hidden) { + text.write[p_line].hidden = p_hidden; + if (!p_hidden && text[p_line].width > max_width) { + max_width = text[p_line].width; + } else if (p_hidden && text[p_line].width == max_width) { + _calculate_max_line_width(); + } + } bool is_hidden(int p_line) const { return text[p_line].hidden; } void insert(int p_at, const String &p_text, const Vector<Vector2i> &p_bidi_override); void remove(int p_at); @@ -158,7 +226,7 @@ private: void set_line_gutter_text(int p_line, int p_gutter, const String &p_text) { text.write[p_line].gutters.write[p_gutter].text = p_text; } const String &get_line_gutter_text(int p_line, int p_gutter) const { return text[p_line].gutters[p_gutter].text; } - void set_line_gutter_icon(int p_line, int p_gutter, Ref<Texture2D> p_icon) { text.write[p_line].gutters.write[p_gutter].icon = p_icon; } + void set_line_gutter_icon(int p_line, int p_gutter, const Ref<Texture2D> &p_icon) { text.write[p_line].gutters.write[p_gutter].icon = p_icon; } const Ref<Texture2D> &get_line_gutter_icon(int p_line, int p_gutter) const { return text[p_line].gutters[p_gutter].icon; } void set_line_gutter_item_color(int p_line, int p_gutter, const Color &p_color) { text.write[p_line].gutters.write[p_gutter].color = p_color; } @@ -172,36 +240,48 @@ private: const Color get_line_background_color(int p_line) const { return text[p_line].background_color; } }; - struct Cursor { - int last_fit_x = 0; - int line = 0; - int column = 0; ///< cursor - int x_ofs = 0; - int line_ofs = 0; - int wrap_ofs = 0; - } cursor; + /* Text */ + Text text; - struct Selection { - SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; - int selecting_line = 0; - int selecting_column = 0; - int selected_word_beg = 0; - int selected_word_end = 0; - int selected_word_origin = 0; - bool selecting_text = false; + bool setting_text = false; - bool active = false; + // Text properties. + String ime_text = ""; + Point2 ime_selection; - int from_line = 0; - int from_column = 0; - int to_line = 0; - int to_column = 0; + /* Initialise to opposite first, so we get past the early-out in set_editable. */ + bool editable = false; - bool shiftclick_left = false; - } selection; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + TextDirection input_direction = TEXT_DIRECTION_LTR; - Map<int, Dictionary> syntax_highlighting_cache; + Dictionary opentype_features; + String language = ""; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; + + void _clear(); + void _update_caches(); + + // User control. + bool overtype_mode = false; + bool context_menu_enabled = true; + bool shortcut_keys_enabled = true; + bool virtual_keyboard_enabled = true; + + // Overridable actions + String cut_copy_line = ""; + + // Context menu. + PopupMenu *menu = nullptr; + PopupMenu *menu_dir = nullptr; + PopupMenu *menu_ctl = nullptr; + + void _generate_context_menu(); + int _get_menu_action_accelerator(const String &p_action); + + /* Versioning */ struct TextOperation { enum Type { TYPE_NONE, @@ -221,295 +301,251 @@ private: bool chain_backward = false; }; - String ime_text; - Point2 ime_selection; + bool undo_enabled = true; + int undo_stack_max_size = 50; - TextOperation current_op; + int complex_operation_count = 0; + bool next_operation_is_complex = false; + TextOperation current_op; List<TextOperation> undo_stack; List<TextOperation>::Element *undo_stack_pos = nullptr; - int undo_stack_max_size; - void _clear_redo(); + Timer *idle_detect; + + uint32_t version = 0; + uint32_t saved_version = 0; + + void _push_current_op(); void _do_text_op(const TextOperation &p_op, bool p_reverse); + void _clear_redo(); - //syntax coloring - Ref<SyntaxHighlighter> syntax_highlighter; - Set<String> keywords; + /* Search */ + Color search_result_color = Color(1, 1, 1); + Color search_result_border_color = Color(1, 1, 1); - Dictionary _get_line_syntax_highlighting(int p_line); + String search_text = ""; + uint32_t search_flags = 0; - Set<String> completion_prefixes; - bool completion_enabled = false; - List<ScriptCodeCompletionOption> completion_sources; - Vector<ScriptCodeCompletionOption> completion_options; - bool completion_active = false; - bool completion_forced = false; - ScriptCodeCompletionOption completion_current; - String completion_base; - int completion_index = 0; - Rect2i completion_rect; - int completion_line_ofs = 0; - String completion_hint; - int completion_hint_offset = 0; + int _get_column_pos_of_word(const String &p_key, const String &p_search, uint32_t p_search_flags, int p_from_column) const; - bool setting_text = false; + /* Tooltip. */ + Object *tooltip_obj = nullptr; + StringName tooltip_func; + Variant tooltip_ud; - // data - Text text; + /* Mouse */ + int _get_char_pos_for_line(int p_px, int p_line, int p_wrap_index = 0) const; - Dictionary opentype_features; - String language; - TextDirection text_direction = TEXT_DIRECTION_AUTO; - TextDirection input_direction = TEXT_DIRECTION_LTR; - Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; - Array st_args; - bool draw_control_chars = false; + /* Caret. */ + struct Caret { + Point2 draw_pos; + bool visible = false; + int last_fit_x = 0; + int line = 0; + int column = 0; + int x_ofs = 0; + int line_ofs = 0; + int wrap_ofs = 0; + } caret; - uint32_t version = 0; - uint32_t saved_version = 0; + bool setting_caret_line = false; + bool caret_pos_dirty = false; - int max_chars = 0; - bool readonly = true; // Initialise to opposite first, so we get past the early-out in set_readonly. - bool indent_using_spaces = false; - int indent_size = 4; - String space_indent = " "; + Color caret_color = Color(1, 1, 1); + Color caret_background_color = Color(0, 0, 0); - Timer *caret_blink_timer; - bool caret_blink_enabled = false; - bool draw_caret = true; - bool window_has_focus = true; - bool block_caret = false; - bool right_click_moves_caret = true; - bool mid_grapheme_caret_enabled = false; + CaretType caret_type = CaretType::CARET_TYPE_LINE; - bool wrap_enabled = false; - int wrap_at = 0; - int wrap_right_offset = 10; + bool draw_caret = true; - bool first_draw = true; - bool setting_row = false; - bool draw_tabs = false; - bool draw_spaces = false; - bool override_selected_font_color = false; - bool cursor_changed_dirty = false; - bool text_changed_dirty = false; - bool undo_enabled = true; - bool line_length_guidelines = false; - int line_length_guideline_soft_col = 80; - int line_length_guideline_hard_col = 100; - bool hiding_enabled = false; - bool draw_minimap = false; - int minimap_width = 80; - Point2 minimap_char_size = Point2(1, 2); - int minimap_line_spacing = 1; + bool caret_blink_enabled = false; + Timer *caret_blink_timer; - bool highlight_all_occurrences = false; - bool scroll_past_end_of_file_enabled = false; - bool auto_brace_completion_enabled = false; - bool brace_matching_enabled = false; - bool highlight_current_line = false; - bool auto_indent = false; - String cut_copy_line; - bool insert_mode = false; - bool select_identifiers_enabled = false; + bool move_caret_on_right_click = true; - bool smooth_scroll_enabled = false; - bool scrolling = false; - bool dragging_selection = false; - bool dragging_minimap = false; - bool can_drag_minimap = false; - bool minimap_clicked = false; - double minimap_scroll_ratio = 0.0; - double minimap_scroll_click_pos = 0.0; - float target_v_scroll = 0.0; - float v_scroll_speed = 80.0; + bool caret_mid_grapheme_enabled = false; - String highlighted_word; + void _emit_caret_changed(); - uint64_t last_dblclk = 0; + void _reset_caret_blink_timer(); + void _toggle_draw_caret(); - Timer *idle_detect; - Timer *click_select_held; - HScrollBar *h_scroll; - VScrollBar *v_scroll; - bool updating_scrolls = false; + int _get_column_x_offset_for_line(int p_char, int p_line) const; - Object *tooltip_obj = nullptr; - StringName tooltip_func; - Variant tooltip_ud; + /* Selection. */ + struct Selection { + SelectionMode selecting_mode = SelectionMode::SELECTION_MODE_NONE; + int selecting_line = 0; + int selecting_column = 0; + int selected_word_beg = 0; + int selected_word_end = 0; + int selected_word_origin = 0; + bool selecting_text = false; - bool next_operation_is_complex = false; + bool active = false; - bool callhint_below = false; - Vector2 callhint_offset; + int from_line = 0; + int from_column = 0; + int to_line = 0; + int to_column = 0; - String search_text; - uint32_t search_flags = 0; - int search_result_line = 0; - int search_result_col = 0; + bool shiftclick_left = false; + } selection; bool selecting_enabled = true; - bool context_menu_enabled = true; - bool shortcut_keys_enabled = true; - bool virtual_keyboard_enabled = true; + Color font_selected_color = Color(1, 1, 1); + Color selection_color = Color(1, 1, 1); + bool override_selected_font_color = false; - void _generate_context_menu(); + bool dragging_selection = false; - int get_visible_rows() const; - int get_total_visible_rows() const; + Timer *click_select_held; + uint64_t last_dblclk = 0; + Vector2 last_dblclk_pos; + void _click_selection_held(); - int _get_minimap_visible_rows() const; + void _update_selection_mode_pointer(); + void _update_selection_mode_word(); + void _update_selection_mode_line(); - void update_cursor_wrap_offset(); - void _update_wrap_at(bool p_force = false); - bool line_wraps(int line) const; - int times_line_wraps(int line) const; - Vector<String> get_wrap_rows_text(int p_line) const; - int get_cursor_wrap_index() const; - int get_line_wrap_index_at_col(int p_line, int p_column) const; - int get_char_count(); + void _pre_shift_selection(); + void _post_shift_selection(); - double get_scroll_pos_for_line(int p_line, int p_wrap_index = 0) const; - void set_line_as_first_visible(int p_line, int p_wrap_index = 0); - void set_line_as_center_visible(int p_line, int p_wrap_index = 0); - void set_line_as_last_visible(int p_line, int p_wrap_index = 0); - int get_first_visible_line() const; - int get_last_full_visible_line() const; - int get_last_full_visible_line_wrap_index() const; - double get_visible_rows_offset() const; - double get_v_scroll_offset() const; + /* line wrapping. */ + LineWrappingMode line_wrapping_mode = LineWrappingMode::LINE_WRAPPING_NONE; + + int wrap_at_column = 0; + int wrap_right_offset = 10; + + void _update_wrap_at_column(bool p_force = false); - int get_char_pos_for_line(int p_px, int p_line, int p_wrap_index = 0) const; - int get_column_x_offset_for_line(int p_char, int p_line) const; + void _update_caret_wrap_offset(); + + /* Viewport. */ + HScrollBar *h_scroll; + VScrollBar *v_scroll; + + bool scroll_past_end_of_file_enabled = false; + + // Smooth scrolling. + bool smooth_scroll_enabled = false; + float target_v_scroll = 0.0; + float v_scroll_speed = 80.0; + + // Scrolling. + bool scrolling = false; + bool updating_scrolls = false; - void adjust_viewport_to_cursor(); - double get_scroll_line_diff() const; - void _scroll_moved(double); void _update_scrollbars(); + int _get_control_height() const; + void _v_scroll_input(); - void _click_selection_held(); + void _scroll_moved(double p_to_val); - void _update_selection_mode_pointer(); - void _update_selection_mode_word(); - void _update_selection_mode_line(); + double _get_visible_lines_offset() const; + double _get_v_scroll_offset() const; - void _update_minimap_click(); - void _update_minimap_drag(); void _scroll_up(real_t p_delta); void _scroll_down(real_t p_delta); - void _pre_shift_selection(); - void _post_shift_selection(); - void _scroll_lines_up(); void _scroll_lines_down(); - //void mouse_motion(const Point& p_pos, const Point& p_rel, int p_button_mask); - Size2 get_minimum_size() const override; - int _get_control_height() const; + // Minimap + bool draw_minimap = false; - Point2 _get_local_mouse_pos() const; - int _get_menu_action_accelerator(const String &p_action); + int minimap_width = 80; + Point2 minimap_char_size = Point2(1, 2); + int minimap_line_spacing = 1; - void _reset_caret_blink_timer(); - void _toggle_draw_caret(); + // minimap scroll + bool minimap_clicked = false; + bool hovering_minimap = false; + bool dragging_minimap = false; + bool can_drag_minimap = false; - void _update_caches(); - void _cursor_changed_emit(); - void _text_changed_emit(); + double minimap_scroll_ratio = 0.0; + double minimap_scroll_click_pos = 0.0; - void _push_current_op(); + void _update_minimap_hover(); + void _update_minimap_click(); + void _update_minimap_drag(); - /* super internal api, undo/redo builds on it */ + /* Gutters. */ + Vector<GutterInfo> gutters; + int gutters_width = 0; + int gutter_padding = 0; - void _base_insert_text(int p_line, int p_char, const String &p_text, int &r_end_line, int &r_end_column); - String _base_get_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) const; - void _base_remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); + void _update_gutter_width(); - int _get_column_pos_of_word(const String &p_key, const String &p_search, uint32_t p_search_flags, int p_from_column); + /* Syntax highlighting. */ + Ref<SyntaxHighlighter> syntax_highlighter; + Map<int, Dictionary> syntax_highlighting_cache; - Dictionary _search_bind(const String &p_key, uint32_t p_search_flags, int p_from_line, int p_from_column) const; + Dictionary _get_line_syntax_highlighting(int p_line); - PopupMenu *menu; - PopupMenu *menu_dir; - PopupMenu *menu_ctl; + /* Visual. */ + Ref<StyleBox> style_normal; + Ref<StyleBox> style_focus; + Ref<StyleBox> style_readonly; - void _clear(); - void _cancel_completion(); - void _cancel_code_hint(); - void _confirm_completion(); - void _update_completion_candidates(); + Ref<Texture2D> tab_icon; + Ref<Texture2D> space_icon; - int _calculate_spaces_till_next_left_indent(int column); - int _calculate_spaces_till_next_right_indent(int column); + Ref<Font> font; + int font_size = 16; + Color font_color = Color(1, 1, 1); + Color font_readonly_color = Color(1, 1, 1); - // Methods used in shortcuts - void _swap_current_input_direction(); - void _new_line(bool p_split_current = true, bool p_above = false); - void _indent_right(); - void _indent_left(); - void _move_cursor_left(bool p_select, bool p_move_by_word = false); - void _move_cursor_right(bool p_select, bool p_move_by_word = false); - void _move_cursor_up(bool p_select); - void _move_cursor_down(bool p_select); - void _move_cursor_to_line_start(bool p_select); - void _move_cursor_to_line_end(bool p_select); - void _move_cursor_page_up(bool p_select); - void _move_cursor_page_down(bool p_select); - void _backspace(bool p_word = false, bool p_all_to_left = false); - void _delete(bool p_word = false, bool p_all_to_right = false); - void _delete_selection(); - void _move_cursor_document_start(bool p_select); - void _move_cursor_document_end(bool p_select); - void _handle_unicode_character(uint32_t unicode, bool p_had_selection, bool p_update_auto_complete); + int outline_size = 0; + Color outline_color = Color(1, 1, 1); -protected: - struct Cache { - Ref<Texture2D> tab_icon; - Ref<Texture2D> space_icon; - Ref<Texture2D> folded_eol_icon; - Ref<StyleBox> style_normal; - Ref<StyleBox> style_focus; - Ref<StyleBox> style_readonly; - Ref<Font> font; - int font_size = 16; - int outline_size = 0; - Color outline_color; - Color completion_background_color; - Color completion_selected_color; - Color completion_existing_color; - Color completion_font_color; - Color caret_color; - Color caret_background_color; - Color font_color; - Color font_selected_color; - Color font_readonly_color; - Color selection_color; - Color code_folding_color; - Color current_line_color; - Color line_length_guideline_color; - Color brace_mismatch_color; - Color word_highlighted_color; - Color search_result_color; - Color search_result_border_color; - Color background_color; - - int line_spacing = 1; - int minimap_width = 0; - } cache; + int line_spacing = 1; - virtual String get_tooltip(const Point2 &p_pos) const override; + Color background_color = Color(1, 1, 1); + Color current_line_color = Color(1, 1, 1); + Color word_highlighted_color = Color(1, 1, 1); + + bool window_has_focus = true; + bool first_draw = true; + + bool highlight_current_line = false; + bool highlight_all_occurrences = false; + bool draw_control_chars = false; + bool draw_tabs = false; + bool draw_spaces = false; + + /*** Super internal Core API. Everything builds on it. ***/ + bool text_changed_dirty = false; + void _text_changed_emit(); void _insert_text(int p_line, int p_char, const String &p_text, int *r_end_line = nullptr, int *r_end_char = nullptr); void _remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); - void _insert_text_at_cursor(const String &p_text); - void _gui_input(const Ref<InputEvent> &p_gui_input); - void _notification(int p_what); - void _consume_pair_symbol(char32_t ch); - void _consume_backspace_for_pair_symbol(int prev_line, int prev_column); + void _base_insert_text(int p_line, int p_char, const String &p_text, int &r_end_line, int &r_end_column); + String _base_get_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column) const; + void _base_remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); + + /* Input actions. */ + void _swap_current_input_direction(); + void _new_line(bool p_split_current = true, bool p_above = false); + void _move_caret_left(bool p_select, bool p_move_by_word = false); + void _move_caret_right(bool p_select, bool p_move_by_word = false); + void _move_caret_up(bool p_select); + void _move_caret_down(bool p_select); + void _move_caret_to_line_start(bool p_select); + void _move_caret_to_line_end(bool p_select); + void _move_caret_page_up(bool p_select); + void _move_caret_page_down(bool p_select); + void _do_backspace(bool p_word = false, bool p_all_to_left = false); + void _delete(bool p_word = false, bool p_all_to_right = false); + void _move_caret_document_start(bool p_select); + void _move_caret_document_end(bool p_select); + +protected: + void _notification(int p_what); static void _bind_methods(); @@ -517,107 +553,60 @@ protected: bool _get(const StringName &p_name, Variant &r_ret) const; void _get_property_list(List<PropertyInfo> *p_list) const; -public: - /* Syntax Highlighting. */ - Ref<SyntaxHighlighter> get_syntax_highlighter(); - void set_syntax_highlighter(Ref<SyntaxHighlighter> p_syntax_highlighter); + /* Internal API for CodeEdit, pending public API. */ + // brace matching + bool highlight_matching_braces_enabled = false; + Color brace_mismatch_color; - /* Gutters. */ - void add_gutter(int p_at = -1); - void remove_gutter(int p_gutter); - int get_gutter_count() const; - - void set_gutter_name(int p_gutter, const String &p_name); - String get_gutter_name(int p_gutter) const; - - void set_gutter_type(int p_gutter, GutterType p_type); - GutterType get_gutter_type(int p_gutter) const; + // Line hiding. + Color code_folding_color = Color(1, 1, 1); + Ref<Texture2D> folded_eol_icon; - void set_gutter_width(int p_gutter, int p_width); - int get_gutter_width(int p_gutter) const; - - void set_gutter_draw(int p_gutter, bool p_draw); - bool is_gutter_drawn(int p_gutter) const; - - void set_gutter_clickable(int p_gutter, bool p_clickable); - bool is_gutter_clickable(int p_gutter) const; - - void set_gutter_overwritable(int p_gutter, bool p_overwritable); - bool is_gutter_overwritable(int p_gutter) const; - - void set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback); - - // Line gutters. - void set_line_gutter_metadata(int p_line, int p_gutter, const Variant &p_metadata); - Variant get_line_gutter_metadata(int p_line, int p_gutter) const; + bool hiding_enabled = false; - void set_line_gutter_text(int p_line, int p_gutter, const String &p_text); - String get_line_gutter_text(int p_line, int p_gutter) const; + void _set_hiding_enabled(bool p_enabled); + bool _is_hiding_enabled() const; - void set_line_gutter_icon(int p_line, int p_gutter, Ref<Texture2D> p_icon); - Ref<Texture2D> get_line_gutter_icon(int p_line, int p_gutter) const; + void _set_line_as_hidden(int p_line, bool p_hidden); + bool _is_line_hidden(int p_line) const; - void set_line_gutter_item_color(int p_line, int p_gutter, const Color &p_color); - Color get_line_gutter_item_color(int p_line, int p_gutter); + void _unhide_all_lines(); - void set_line_gutter_clickable(int p_line, int p_gutter, bool p_clickable); - bool is_line_gutter_clickable(int p_line, int p_gutter) const; + // Symbol lookup. + String lookup_symbol_word; + void _set_symbol_lookup_word(const String &p_symbol); - // Line style - void set_line_background_color(int p_line, const Color &p_color); - Color get_line_background_color(int p_line); + /* Text manipulation */ - enum MenuItems { - MENU_CUT, - MENU_COPY, - MENU_PASTE, - MENU_CLEAR, - MENU_SELECT_ALL, - MENU_UNDO, - MENU_REDO, - MENU_DIR_INHERITED, - MENU_DIR_AUTO, - MENU_DIR_LTR, - MENU_DIR_RTL, - MENU_DISPLAY_UCC, - MENU_INSERT_LRM, - MENU_INSERT_RLM, - MENU_INSERT_LRE, - MENU_INSERT_RLE, - MENU_INSERT_LRO, - MENU_INSERT_RLO, - MENU_INSERT_PDF, - MENU_INSERT_ALM, - MENU_INSERT_LRI, - MENU_INSERT_RLI, - MENU_INSERT_FSI, - MENU_INSERT_PDI, - MENU_INSERT_ZWJ, - MENU_INSERT_ZWNJ, - MENU_INSERT_WJ, - MENU_INSERT_SHY, - MENU_MAX + // Overridable actions + virtual void _handle_unicode_input_internal(const uint32_t p_unicode); + virtual void _backspace_internal(); - }; + virtual void _cut_internal(); + virtual void _copy_internal(); + virtual void _paste_internal(); - enum SearchFlags { - SEARCH_MATCH_CASE = 1, - SEARCH_WHOLE_WORDS = 2, - SEARCH_BACKWARDS = 4 - }; + GDVIRTUAL1(_handle_unicode_input, int) + GDVIRTUAL0(_backspace) + GDVIRTUAL0(_cut) + GDVIRTUAL0(_copy) + GDVIRTUAL0(_paste) +public: + /* General overrides. */ + virtual void gui_input(const Ref<InputEvent> &p_gui_input) override; + virtual Size2 get_minimum_size() const override; + virtual bool is_text_field() const override; virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override; + virtual String get_tooltip(const Point2 &p_pos) const override; + void set_tooltip_request_func(Object *p_obj, const StringName &p_function, const Variant &p_udata); - void _get_mouse_pos(const Point2i &p_mouse, int &r_row, int &r_col) const; - void _get_minimap_mouse_row(const Point2i &p_mouse, int &r_row) const; - - //void delete_char(); - //void delete_line(); + /* Text */ + // Text properties. + bool has_ime_text() const; - void begin_complex_operation(); - void end_complex_operation(); - - bool is_insert_text_operation(); + void set_editable(const bool p_editable); + bool is_editable() const; void set_text_direction(TextDirection p_text_direction); TextDirection get_text_direction() const; @@ -629,220 +618,288 @@ public: void set_language(const String &p_language); String get_language() const; - void set_draw_control_chars(bool p_draw_control_chars); - bool get_draw_control_chars() const; - void set_structured_text_bidi_override(Control::StructuredTextParser p_parser); Control::StructuredTextParser get_structured_text_bidi_override() const; - void set_structured_text_bidi_override_options(Array p_args); Array get_structured_text_bidi_override_options() const; - void set_highlighted_word(const String &new_word); - void set_text(String p_text); - void insert_text_at_cursor(const String &p_text); - void insert_at(const String &p_text, int at); + void set_tab_size(const int p_size); + int get_tab_size() const; + + // User controls + void set_overtype_mode_enabled(const bool p_enabled); + bool is_overtype_mode_enabled() const; + + void set_context_menu_enabled(bool p_enable); + bool is_context_menu_enabled() const; + + void set_shortcut_keys_enabled(bool p_enabled); + bool is_shortcut_keys_enabled() const; + + void set_virtual_keyboard_enabled(bool p_enable); + bool is_virtual_keyboard_enabled() const; + + // Text manipulation + void clear(); + + void set_text(const String &p_text); + String get_text() const; int get_line_count() const; - void set_line_as_hidden(int p_line, bool p_hidden); - bool is_line_hidden(int p_line) const; - void fold_all_lines(); - void unhide_all_lines(); - int num_lines_from(int p_line_from, int visible_amount) const; - int num_lines_from_rows(int p_line_from, int p_wrap_index_from, int visible_amount, int &wrap_index) const; - int get_last_unhidden_line() const; + void set_line(int p_line, const String &p_new_text); + String get_line(int p_line) const; + + int get_line_width(int p_line, int p_wrap_index = -1) const; + int get_line_height() const; - bool can_fold(int p_line) const; - bool is_folded(int p_line) const; - Vector<int> get_folded_lines() const; - void fold_line(int p_line); - void unfold_line(int p_line); - void toggle_fold_line(int p_line); - - String get_text(); - String get_line(int line) const; - void set_line(int line, String new_text); - int get_row_height() const; - void backspace_at_cursor(); - - void indent_selected_lines_left(); - void indent_selected_lines_right(); int get_indent_level(int p_line) const; - bool is_line_comment(int p_line) const; + int get_first_non_whitespace_column(int p_line) const; - inline void set_scroll_pass_end_of_file(bool p_enabled) { - scroll_past_end_of_file_enabled = p_enabled; - update(); - } - inline void set_auto_brace_completion(bool p_enabled) { - auto_brace_completion_enabled = p_enabled; - } - inline void set_brace_matching(bool p_enabled) { - brace_matching_enabled = p_enabled; - update(); - } - inline void set_callhint_settings(bool below, Vector2 offset) { - callhint_below = below; - callhint_offset = offset; - } - void set_auto_indent(bool p_auto_indent); + void swap_lines(int p_from_line, int p_to_line); - void center_viewport_to_cursor(); + void insert_line_at(int p_at, const String &p_text); + void insert_text_at_caret(const String &p_text); - void set_mid_grapheme_caret_enabled(const bool p_enabled); - bool get_mid_grapheme_caret_enabled() const; + void remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); - void cursor_set_column(int p_col, bool p_adjust_viewport = true); - void cursor_set_line(int p_row, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0); + int get_last_unhidden_line() const; + int get_next_visible_line_offset_from(int p_line_from, int p_visible_amount) const; + Point2i get_next_visible_line_index_offset_from(int p_line_from, int p_wrap_index_from, int p_visible_amount) const; - int cursor_get_column() const; - int cursor_get_line() const; - Vector2i _get_cursor_pixel_pos(bool p_adjust_viewport = true); + // Overridable actions + void handle_unicode_input(const uint32_t p_unicode); + void backspace(); - bool cursor_get_blink_enabled() const; - void cursor_set_blink_enabled(const bool p_enabled); + void cut(); + void copy(); + void paste(); - float cursor_get_blink_speed() const; - void cursor_set_blink_speed(const float p_speed); + // Context menu. + PopupMenu *get_menu() const; + bool is_menu_visible() const; + void menu_option(int p_option); - void cursor_set_block_mode(const bool p_enable); - bool cursor_is_block_mode() const; + /* Versioning */ + void begin_complex_operation(); + void end_complex_operation(); - void set_right_click_moves_caret(bool p_enable); - bool is_right_click_moving_caret() const; + bool has_undo() const; + bool has_redo() const; + void undo(); + void redo(); + void clear_undo_history(); - SelectionMode get_selection_mode() const; - void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1); - int get_selection_line() const; - int get_selection_column() const; + bool is_insert_text_operation() const; - void set_readonly(bool p_readonly); - bool is_readonly() const; + void tag_saved_version(); - void set_max_chars(int p_max_chars); - int get_max_chars() const; + uint32_t get_version() const; + uint32_t get_saved_version() const; - void set_wrap_enabled(bool p_wrap_enabled); - bool is_wrap_enabled() const; + /* Search */ + void set_search_text(const String &p_search_text); + void set_search_flags(uint32_t p_flags); - void clear(); + Point2i search(const String &p_key, uint32_t p_search_flags, int p_from_line, int p_from_column) const; + + /* Mouse */ + Point2 get_local_mouse_pos() const; + + String get_word_at_pos(const Vector2 &p_pos) const; + + Point2i get_line_column_at_pos(const Point2i &p_pos) const; + int get_minimap_line_at_pos(const Point2i &p_pos) const; + + bool is_dragging_cursor() const; + + /* Caret */ + void set_caret_type(CaretType p_type); + CaretType get_caret_type() const; + + void set_caret_blink_enabled(const bool p_enabled); + bool is_caret_blink_enabled() const; + + void set_caret_blink_speed(const float p_speed); + float get_caret_blink_speed() const; + + void set_move_caret_on_right_click_enabled(const bool p_enable); + bool is_move_caret_on_right_click_enabled() const; + + void set_caret_mid_grapheme_enabled(const bool p_enabled); + bool is_caret_mid_grapheme_enabled() const; + + bool is_caret_visible() const; + Point2 get_caret_draw_pos() const; + + void set_caret_line(int p_line, bool p_adjust_viewport = true, bool p_can_be_hidden = true, int p_wrap_index = 0); + int get_caret_line() const; + + void set_caret_column(int p_col, bool p_adjust_viewport = true); + int get_caret_column() const; + + int get_caret_wrap_index() const; + + String get_word_under_caret() const; + + /* Selection. */ + void set_selecting_enabled(const bool p_enabled); + bool is_selecting_enabled() const; + + void set_override_selected_font_color(bool p_override_selected_font_color); + bool is_overriding_selected_font_color() const; + + void set_selection_mode(SelectionMode p_mode, int p_line = -1, int p_column = -1); + SelectionMode get_selection_mode() const; - void cut(); - void copy(); - void paste(); void select_all(); void select_word_under_caret(); void select(int p_from_line, int p_from_column, int p_to_line, int p_to_column); - void deselect(); - void swap_lines(int line1, int line2); - void set_search_text(const String &p_search_text); - void set_search_flags(uint32_t p_flags); - void set_current_search_result(int line, int col); + bool has_selection() const; + + String get_selected_text() const; + + int get_selection_line() const; + int get_selection_column() const; - void set_highlight_all_occurrences(const bool p_enabled); - bool is_highlight_all_occurrences_enabled() const; - bool is_selection_active() const; int get_selection_from_line() const; int get_selection_from_column() const; int get_selection_to_line() const; int get_selection_to_column() const; - String get_selection_text() const; - String get_word_under_cursor() const; - String get_word_at_pos(const Vector2 &p_pos) const; + void deselect(); + void delete_selection(); - bool search(const String &p_key, uint32_t p_search_flags, int p_from_line, int p_from_column, int &r_line, int &r_column) const; + /* line wrapping. */ + void set_line_wrapping_mode(LineWrappingMode p_wrapping_mode); + LineWrappingMode get_line_wrapping_mode() const; - void undo(); - void redo(); - void clear_undo_history(); + bool is_line_wrapped(int p_line) const; + int get_line_wrap_count(int p_line) const; + int get_line_wrap_index_at_column(int p_line, int p_column) const; - void set_indent_using_spaces(const bool p_use_spaces); - bool is_indent_using_spaces() const; - void set_indent_size(const int p_size); - int get_indent_size(); - void set_draw_tabs(bool p_draw); - bool is_drawing_tabs() const; - void set_draw_spaces(bool p_draw); - bool is_drawing_spaces() const; - void set_override_selected_font_color(bool p_override_selected_font_color); - bool is_overriding_selected_font_color() const; + Vector<String> get_line_wrapped_text(int p_line) const; - void set_insert_mode(bool p_enabled); - bool is_insert_mode() const; + /* Viewport. */ + // Scrolling. + void set_smooth_scroll_enabled(const bool p_enable); + bool is_smooth_scroll_enabled() const; - void add_keyword(const String &p_keyword); - void clear_keywords(); + void set_scroll_past_end_of_file_enabled(const bool p_enabled); + bool is_scroll_past_end_of_file_enabled() const; - double get_v_scroll() const; void set_v_scroll(double p_scroll); + double get_v_scroll() const; - int get_h_scroll() const; void set_h_scroll(int p_scroll); - - void set_smooth_scroll_enabled(bool p_enable); - bool is_smooth_scroll_enabled() const; + int get_h_scroll() const; void set_v_scroll_speed(float p_speed); float get_v_scroll_speed() const; - uint32_t get_version() const; - uint32_t get_saved_version() const; - void tag_saved_version(); + double get_scroll_pos_for_line(int p_line, int p_wrap_index = 0) const; - void menu_option(int p_option); + // Visible lines. + void set_line_as_first_visible(int p_line, int p_wrap_index = 0); + int get_first_visible_line() const; - void set_highlight_current_line(bool p_enabled); - bool is_highlight_current_line_enabled() const; + void set_line_as_center_visible(int p_line, int p_wrap_index = 0); - void set_show_line_length_guidelines(bool p_show); - void set_line_length_guideline_soft_column(int p_column); - void set_line_length_guideline_hard_column(int p_column); + void set_line_as_last_visible(int p_line, int p_wrap_index = 0); + int get_last_full_visible_line() const; + int get_last_full_visible_line_wrap_index() const; + + int get_visible_line_count() const; + int get_total_visible_line_count() const; + + // Auto Adjust + void adjust_viewport_to_caret(); + void center_viewport_to_caret(); + // Minimap void set_draw_minimap(bool p_draw); bool is_drawing_minimap() const; void set_minimap_width(int p_minimap_width); int get_minimap_width() const; - void set_hiding_enabled(bool p_enabled); - bool is_hiding_enabled() const; + int get_minimap_visible_lines() const; - void set_tooltip_request_func(Object *p_obj, const StringName &p_function, const Variant &p_udata); + /* Gutters. */ + void add_gutter(int p_at = -1); + void remove_gutter(int p_gutter); + int get_gutter_count() const; - void set_completion(bool p_enabled, const Vector<String> &p_prefixes); - void code_complete(const List<ScriptCodeCompletionOption> &p_strings, bool p_forced = false); - void set_code_hint(const String &p_hint); - void query_code_comple(); + void set_gutter_name(int p_gutter, const String &p_name); + String get_gutter_name(int p_gutter) const; - void set_select_identifiers_on_hover(bool p_enable); - bool is_selecting_identifiers_on_hover_enabled() const; + void set_gutter_type(int p_gutter, GutterType p_type); + GutterType get_gutter_type(int p_gutter) const; - void set_context_menu_enabled(bool p_enable); - bool is_context_menu_enabled(); + void set_gutter_width(int p_gutter, int p_width); + int get_gutter_width(int p_gutter) const; + int get_total_gutter_width() const; - void set_selecting_enabled(bool p_enabled); - bool is_selecting_enabled() const; + void set_gutter_draw(int p_gutter, bool p_draw); + bool is_gutter_drawn(int p_gutter) const; - void set_shortcut_keys_enabled(bool p_enabled); - bool is_shortcut_keys_enabled() const; + void set_gutter_clickable(int p_gutter, bool p_clickable); + bool is_gutter_clickable(int p_gutter) const; - void set_virtual_keyboard_enabled(bool p_enable); - bool is_virtual_keyboard_enabled() const; + void set_gutter_overwritable(int p_gutter, bool p_overwritable); + bool is_gutter_overwritable(int p_gutter) const; - PopupMenu *get_menu() const; + void merge_gutters(int p_from_line, int p_to_line); - String get_text_for_completion(); - String get_text_for_lookup_completion(); + void set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback); + + // Line gutters. + void set_line_gutter_metadata(int p_line, int p_gutter, const Variant &p_metadata); + Variant get_line_gutter_metadata(int p_line, int p_gutter) const; + + void set_line_gutter_text(int p_line, int p_gutter, const String &p_text); + String get_line_gutter_text(int p_line, int p_gutter) const; + + void set_line_gutter_icon(int p_line, int p_gutter, const Ref<Texture2D> &p_icon); + Ref<Texture2D> get_line_gutter_icon(int p_line, int p_gutter) const; + + void set_line_gutter_item_color(int p_line, int p_gutter, const Color &p_color); + Color get_line_gutter_item_color(int p_line, int p_gutter) const; + + void set_line_gutter_clickable(int p_line, int p_gutter, bool p_clickable); + bool is_line_gutter_clickable(int p_line, int p_gutter) const; + + // Line style + void set_line_background_color(int p_line, const Color &p_color); + Color get_line_background_color(int p_line) const; + + /* Syntax Highlighting. */ + void set_syntax_highlighter(Ref<SyntaxHighlighter> p_syntax_highlighter); + Ref<SyntaxHighlighter> get_syntax_highlighter() const; + + /* Visual. */ + void set_highlight_current_line(bool p_enabled); + bool is_highlight_current_line_enabled() const; + + void set_highlight_all_occurrences(const bool p_enabled); + bool is_highlight_all_occurrences_enabled() const; + + void set_draw_control_chars(bool p_draw_control_chars); + bool get_draw_control_chars() const; + + void set_draw_tabs(bool p_draw); + bool is_drawing_tabs() const; + + void set_draw_spaces(bool p_draw); + bool is_drawing_spaces() const; - virtual bool is_text_field() const override; TextEdit(); - ~TextEdit(); }; -VARIANT_ENUM_CAST(TextEdit::GutterType); +VARIANT_ENUM_CAST(TextEdit::CaretType); +VARIANT_ENUM_CAST(TextEdit::LineWrappingMode); VARIANT_ENUM_CAST(TextEdit::SelectionMode); +VARIANT_ENUM_CAST(TextEdit::GutterType); VARIANT_ENUM_CAST(TextEdit::MenuItems); VARIANT_ENUM_CAST(TextEdit::SearchFlags); diff --git a/scene/gui/texture_button.cpp b/scene/gui/texture_button.cpp index f43e3d1a9d..8659ea06a2 100644 --- a/scene/gui/texture_button.cpp +++ b/scene/gui/texture_button.cpp @@ -295,11 +295,13 @@ void TextureButton::set_normal_texture(const Ref<Texture2D> &p_normal) { void TextureButton::set_pressed_texture(const Ref<Texture2D> &p_pressed) { pressed = p_pressed; update(); + minimum_size_changed(); } void TextureButton::set_hover_texture(const Ref<Texture2D> &p_hover) { hover = p_hover; update(); + minimum_size_changed(); } void TextureButton::set_disabled_texture(const Ref<Texture2D> &p_disabled) { @@ -310,6 +312,7 @@ void TextureButton::set_disabled_texture(const Ref<Texture2D> &p_disabled) { void TextureButton::set_click_mask(const Ref<BitMap> &p_click_mask) { click_mask = p_click_mask; update(); + minimum_size_changed(); } Ref<Texture2D> TextureButton::get_normal_texture() const { diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp index 46ce9d5ca9..286f01ee33 100644 --- a/scene/gui/texture_progress_bar.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -100,6 +100,15 @@ Ref<Texture2D> TextureProgressBar::get_progress_texture() const { return progress; } +void TextureProgressBar::set_progress_offset(Point2 p_offset) { + progress_offset = p_offset; + update(); +} + +Point2 TextureProgressBar::get_progress_offset() const { + return progress_offset; +} + void TextureProgressBar::set_tint_under(const Color &p_tint) { tint_under = p_tint; update(); @@ -221,43 +230,87 @@ void TextureProgressBar::draw_nine_patch_stretched(const Ref<Texture2D> &p_textu double width_texture = 0.0; double first_section_size = 0.0; double last_section_size = 0.0; - switch (mode) { - case FILL_LEFT_TO_RIGHT: - case FILL_RIGHT_TO_LEFT: { + switch (p_mode) { + case FILL_LEFT_TO_RIGHT: { width_total = dst_rect.size.x; width_texture = texture_size.x; first_section_size = topleft.x; last_section_size = bottomright.x; } break; - case FILL_TOP_TO_BOTTOM: - case FILL_BOTTOM_TO_TOP: { + case FILL_RIGHT_TO_LEFT: { + width_total = dst_rect.size.x; + width_texture = texture_size.x; + // In contrast to `FILL_LEFT_TO_RIGHT`, `first_section_size` and `last_section_size` should switch value. + first_section_size = bottomright.x; + last_section_size = topleft.x; + } break; + case FILL_TOP_TO_BOTTOM: { width_total = dst_rect.size.y; width_texture = texture_size.y; first_section_size = topleft.y; last_section_size = bottomright.y; } break; + case FILL_BOTTOM_TO_TOP: { + width_total = dst_rect.size.y; + width_texture = texture_size.y; + // Similar to `FILL_RIGHT_TO_LEFT`. + first_section_size = bottomright.y; + last_section_size = topleft.y; + } break; case FILL_BILINEAR_LEFT_AND_RIGHT: { - // TODO: Implement + width_total = dst_rect.size.x; + width_texture = texture_size.x; + first_section_size = topleft.x; + last_section_size = bottomright.x; } break; case FILL_BILINEAR_TOP_AND_BOTTOM: { - // TODO: Implement + width_total = dst_rect.size.y; + width_texture = texture_size.y; + first_section_size = topleft.y; + last_section_size = bottomright.y; } break; case FILL_CLOCKWISE: case FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE: case FILL_COUNTER_CLOCKWISE: { - // Those modes are circular, not relevant for nine patch + // Those modes are circular, not relevant for nine patch. } break; + case FILL_MODE_MAX: + break; } double width_filled = width_total * p_ratio; double middle_section_size = MAX(0.0, width_texture - first_section_size - last_section_size); - middle_section_size *= MIN(1.0, (MAX(0.0, width_filled - first_section_size) / MAX(1.0, width_total - first_section_size - last_section_size))); - last_section_size = MAX(0.0, last_section_size - (width_total - width_filled)); - first_section_size = MIN(first_section_size, width_filled); - width_texture = MIN(width_texture, first_section_size + middle_section_size + last_section_size); + // Maximum middle texture size. + double max_middle_texture_size = middle_section_size; + + // Maximum real middle texture size. + double max_middle_real_size = MAX(0.0, width_total - (first_section_size + last_section_size)); + + switch (p_mode) { + case FILL_BILINEAR_LEFT_AND_RIGHT: + case FILL_BILINEAR_TOP_AND_BOTTOM: { + last_section_size = MAX(0.0, last_section_size - (width_total - width_filled) * 0.5); + first_section_size = MAX(0.0, first_section_size - (width_total - width_filled) * 0.5); - switch (mode) { + // When `width_filled` increases, `middle_section_size` only increases when either of `first_section_size` and `last_section_size` is zero. + // Also, it should always be smaller than or equal to `(width_total - (first_section_size + last_section_size))`. + double real_middle_size = width_filled - first_section_size - last_section_size; + middle_section_size *= MIN(max_middle_real_size, real_middle_size) / max_middle_real_size; + + width_texture = MIN(width_texture, first_section_size + middle_section_size + last_section_size); + } break; + case FILL_MODE_MAX: + break; + default: { + middle_section_size *= MIN(1.0, (MAX(0.0, width_filled - first_section_size) / MAX(1.0, width_total - first_section_size - last_section_size))); + last_section_size = MAX(0.0, last_section_size - (width_total - width_filled)); + first_section_size = MIN(first_section_size, width_filled); + width_texture = MIN(width_texture, first_section_size + middle_section_size + last_section_size); + } + } + + switch (p_mode) { case FILL_LEFT_TO_RIGHT: { src_rect.size.x = width_texture; dst_rect.size.x = width_filled; @@ -287,19 +340,38 @@ void TextureProgressBar::draw_nine_patch_stretched(const Ref<Texture2D> &p_textu bottomright.y = first_section_size; } break; case FILL_BILINEAR_LEFT_AND_RIGHT: { - // TODO: Implement + double center_mapped_from_real_width = (width_total * 0.5 - topleft.x) / max_middle_real_size * max_middle_texture_size + topleft.x; + double drift_from_unscaled_center = (src_rect.size.x * 0.5 - center_mapped_from_real_width) * (last_section_size - first_section_size) / (bottomright.x - topleft.x); + src_rect.position.x += center_mapped_from_real_width + drift_from_unscaled_center - width_texture * 0.5; + src_rect.size.x = width_texture; + dst_rect.position.x += (width_total - width_filled) * 0.5; + dst_rect.size.x = width_filled; + topleft.x = first_section_size; + bottomright.x = last_section_size; } break; case FILL_BILINEAR_TOP_AND_BOTTOM: { - // TODO: Implement + double center_mapped_from_real_width = (width_total * 0.5 - topleft.y) / max_middle_real_size * max_middle_texture_size + topleft.y; + double drift_from_unscaled_center = (src_rect.size.y * 0.5 - center_mapped_from_real_width) * (last_section_size - first_section_size) / (bottomright.y - topleft.y); + src_rect.position.y += center_mapped_from_real_width + drift_from_unscaled_center - width_texture * 0.5; + src_rect.size.y = width_texture; + dst_rect.position.y += (width_total - width_filled) * 0.5; + dst_rect.size.y = width_filled; + topleft.y = first_section_size; + bottomright.y = last_section_size; } break; case FILL_CLOCKWISE: case FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE: case FILL_COUNTER_CLOCKWISE: { - // Those modes are circular, not relevant for nine patch + // Those modes are circular, not relevant for nine patch. } break; + case FILL_MODE_MAX: + break; } } + if (p_texture == progress) { + dst_rect.position += progress_offset; + } p_texture->get_rect_region(dst_rect, src_rect, dst_rect, src_rect); RID ci = get_canvas_item(); @@ -310,38 +382,57 @@ void TextureProgressBar::_notification(int p_what) { const float corners[12] = { -0.125, -0.375, -0.625, -0.875, 0.125, 0.375, 0.625, 0.875, 1.125, 1.375, 1.625, 1.875 }; switch (p_what) { case NOTIFICATION_DRAW: { - if (nine_patch_stretch && (mode == FILL_LEFT_TO_RIGHT || mode == FILL_RIGHT_TO_LEFT || mode == FILL_TOP_TO_BOTTOM || mode == FILL_BOTTOM_TO_TOP)) { + if (nine_patch_stretch && (mode == FILL_LEFT_TO_RIGHT || mode == FILL_RIGHT_TO_LEFT || mode == FILL_TOP_TO_BOTTOM || mode == FILL_BOTTOM_TO_TOP || mode == FILL_BILINEAR_LEFT_AND_RIGHT || mode == FILL_BILINEAR_TOP_AND_BOTTOM)) { if (under.is_valid()) { - draw_nine_patch_stretched(under, FILL_LEFT_TO_RIGHT, 1.0, tint_under); + draw_nine_patch_stretched(under, mode, 1.0, tint_under); } if (progress.is_valid()) { draw_nine_patch_stretched(progress, mode, get_as_ratio(), tint_progress); } if (over.is_valid()) { - draw_nine_patch_stretched(over, FILL_LEFT_TO_RIGHT, 1.0, tint_over); + draw_nine_patch_stretched(over, mode, 1.0, tint_over); } } else { if (under.is_valid()) { - draw_texture(under, Point2(), tint_under); + switch (mode) { + case FILL_CLOCKWISE: + case FILL_COUNTER_CLOCKWISE: + case FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE: { + if (nine_patch_stretch) { + Rect2 region = Rect2(Point2(), get_size()); + draw_texture_rect(under, region, false, tint_under); + } else { + draw_texture(under, Point2(), tint_under); + } + } break; + case FILL_MODE_MAX: + break; + default: + draw_texture(under, Point2(), tint_under); + } } if (progress.is_valid()) { Size2 s = progress->get_size(); switch (mode) { case FILL_LEFT_TO_RIGHT: { - Rect2 region = Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)); - draw_texture_rect_region(progress, region, region, tint_progress); + Rect2 region = Rect2(progress_offset, Size2(s.x * get_as_ratio(), s.y)); + Rect2 source = Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)); + draw_texture_rect_region(progress, region, source, tint_progress); } break; case FILL_RIGHT_TO_LEFT: { - Rect2 region = Rect2(Point2(s.x - s.x * get_as_ratio(), 0), Size2(s.x * get_as_ratio(), s.y)); - draw_texture_rect_region(progress, region, region, tint_progress); + Rect2 region = Rect2(progress_offset + Point2(s.x - s.x * get_as_ratio(), 0), Size2(s.x * get_as_ratio(), s.y)); + Rect2 source = Rect2(Point2(s.x - s.x * get_as_ratio(), 0), Size2(s.x * get_as_ratio(), s.y)); + draw_texture_rect_region(progress, region, source, tint_progress); } break; case FILL_TOP_TO_BOTTOM: { - Rect2 region = Rect2(Point2(), Size2(s.x, s.y * get_as_ratio())); - draw_texture_rect_region(progress, region, region, tint_progress); + Rect2 region = Rect2(progress_offset + Point2(), Size2(s.x, s.y * get_as_ratio())); + Rect2 source = Rect2(Point2(), Size2(s.x, s.y * get_as_ratio())); + draw_texture_rect_region(progress, region, source, tint_progress); } break; case FILL_BOTTOM_TO_TOP: { - Rect2 region = Rect2(Point2(0, s.y - s.y * get_as_ratio()), Size2(s.x, s.y * get_as_ratio())); - draw_texture_rect_region(progress, region, region, tint_progress); + Rect2 region = Rect2(progress_offset + Point2(0, s.y - s.y * get_as_ratio()), Size2(s.x, s.y * get_as_ratio())); + Rect2 source = Rect2(Point2(0, s.y - s.y * get_as_ratio()), Size2(s.x, s.y * get_as_ratio())); + draw_texture_rect_region(progress, region, source, tint_progress); } break; case FILL_CLOCKWISE: case FILL_COUNTER_CLOCKWISE: @@ -352,8 +443,9 @@ void TextureProgressBar::_notification(int p_what) { float val = get_as_ratio() * rad_max_degrees / 360; if (val == 1) { - Rect2 region = Rect2(Point2(), s); - draw_texture_rect_region(progress, region, region, tint_progress); + Rect2 region = Rect2(progress_offset, s); + Rect2 source = Rect2(Point2(), s); + draw_texture_rect_region(progress, region, source, tint_progress); } else if (val != 0) { Array pts; float direction = mode == FILL_COUNTER_CLOCKWISE ? -1 : 1; @@ -379,14 +471,14 @@ void TextureProgressBar::_notification(int p_what) { Vector<Point2> uvs; Vector<Point2> points; uvs.push_back(get_relative_center()); - points.push_back(Point2(s.x * get_relative_center().x, s.y * get_relative_center().y)); + points.push_back(progress_offset + Point2(s.x * get_relative_center().x, s.y * get_relative_center().y)); for (int i = 0; i < pts.size(); i++) { Point2 uv = unit_val_to_uv(pts[i]); if (uvs.find(uv) >= 0) { continue; } uvs.push_back(uv); - points.push_back(Point2(uv.x * s.x, uv.y * s.y)); + points.push_back(progress_offset + Point2(uv.x * s.x, uv.y * s.y)); } Vector<Color> colors; colors.push_back(tint_progress); @@ -409,19 +501,38 @@ void TextureProgressBar::_notification(int p_what) { } } break; case FILL_BILINEAR_LEFT_AND_RIGHT: { - Rect2 region = Rect2(Point2(s.x / 2 - s.x * get_as_ratio() / 2, 0), Size2(s.x * get_as_ratio(), s.y)); - draw_texture_rect_region(progress, region, region, tint_progress); + Rect2 region = Rect2(progress_offset + Point2(s.x / 2 - s.x * get_as_ratio() / 2, 0), Size2(s.x * get_as_ratio(), s.y)); + Rect2 source = Rect2(Point2(s.x / 2 - s.x * get_as_ratio() / 2, 0), Size2(s.x * get_as_ratio(), s.y)); + draw_texture_rect_region(progress, region, source, tint_progress); } break; case FILL_BILINEAR_TOP_AND_BOTTOM: { - Rect2 region = Rect2(Point2(0, s.y / 2 - s.y * get_as_ratio() / 2), Size2(s.x, s.y * get_as_ratio())); - draw_texture_rect_region(progress, region, region, tint_progress); + Rect2 region = Rect2(progress_offset + Point2(0, s.y / 2 - s.y * get_as_ratio() / 2), Size2(s.x, s.y * get_as_ratio())); + Rect2 source = Rect2(Point2(0, s.y / 2 - s.y * get_as_ratio() / 2), Size2(s.x, s.y * get_as_ratio())); + draw_texture_rect_region(progress, region, source, tint_progress); } break; + case FILL_MODE_MAX: + break; default: - draw_texture_rect_region(progress, Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)), Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)), tint_progress); + draw_texture_rect_region(progress, Rect2(progress_offset, Size2(s.x * get_as_ratio(), s.y)), Rect2(Point2(), Size2(s.x * get_as_ratio(), s.y)), tint_progress); } } if (over.is_valid()) { - draw_texture(over, Point2(), tint_over); + switch (mode) { + case FILL_CLOCKWISE: + case FILL_COUNTER_CLOCKWISE: + case FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE: { + if (nine_patch_stretch) { + Rect2 region = Rect2(Point2(), get_size()); + draw_texture_rect(over, region, false, tint_over); + } else { + draw_texture(over, Point2(), tint_over); + } + } break; + case FILL_MODE_MAX: + break; + default: + draw_texture(over, Point2(), tint_over); + } } } } break; @@ -429,7 +540,7 @@ void TextureProgressBar::_notification(int p_what) { } void TextureProgressBar::set_fill_mode(int p_fill) { - ERR_FAIL_INDEX(p_fill, 9); + ERR_FAIL_INDEX(p_fill, FILL_MODE_MAX); mode = (FillMode)p_fill; update(); } @@ -493,6 +604,9 @@ void TextureProgressBar::_bind_methods() { ClassDB::bind_method(D_METHOD("set_tint_over", "tint"), &TextureProgressBar::set_tint_over); ClassDB::bind_method(D_METHOD("get_tint_over"), &TextureProgressBar::get_tint_over); + ClassDB::bind_method(D_METHOD("set_texture_progress_offset", "offset"), &TextureProgressBar::set_progress_offset); + ClassDB::bind_method(D_METHOD("get_texture_progress_offset"), &TextureProgressBar::get_progress_offset); + ClassDB::bind_method(D_METHOD("set_radial_initial_angle", "mode"), &TextureProgressBar::set_radial_initial_angle); ClassDB::bind_method(D_METHOD("get_radial_initial_angle"), &TextureProgressBar::get_radial_initial_angle); @@ -512,7 +626,8 @@ void TextureProgressBar::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_under", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_under_texture", "get_under_texture"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_over", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_over_texture", "get_over_texture"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_progress", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_progress_texture", "get_progress_texture"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "fill_mode", PROPERTY_HINT_ENUM, "Left to Right,Right to Left,Top to Bottom,Bottom to Top,Clockwise,Counter Clockwise,Bilinear (Left and Right),Bilinear (Top and Bottom), Clockwise and Counter Clockwise"), "set_fill_mode", "get_fill_mode"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "texture_progress_offset"), "set_texture_progress_offset", "get_texture_progress_offset"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "fill_mode", PROPERTY_HINT_ENUM, "Left to Right,Right to Left,Top to Bottom,Bottom to Top,Clockwise,Counter Clockwise,Bilinear (Left and Right),Bilinear (Top and Bottom),Clockwise and Counter Clockwise"), "set_fill_mode", "get_fill_mode"); ADD_GROUP("Tint", "tint_"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_under"), "set_tint_under", "get_tint_under"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_over"), "set_tint_over", "get_tint_over"); diff --git a/scene/gui/texture_progress_bar.h b/scene/gui/texture_progress_bar.h index a3883a7017..c508f41387 100644 --- a/scene/gui/texture_progress_bar.h +++ b/scene/gui/texture_progress_bar.h @@ -54,12 +54,16 @@ public: FILL_COUNTER_CLOCKWISE, FILL_BILINEAR_LEFT_AND_RIGHT, FILL_BILINEAR_TOP_AND_BOTTOM, - FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE + FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE, + FILL_MODE_MAX, }; void set_fill_mode(int p_fill); int get_fill_mode(); + void set_progress_offset(Point2 p_offset); + Point2 get_progress_offset() const; + void set_radial_initial_angle(float p_angle); float get_radial_initial_angle(); @@ -99,6 +103,7 @@ public: private: FillMode mode = FILL_LEFT_TO_RIGHT; + Point2 progress_offset; float rad_init_angle = 0.0; float rad_max_degrees = 360.0; Point2 rad_center_off; diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index d2b98eb0fa..f62c09925d 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -99,39 +99,44 @@ void TreeItem::_change_tree(Tree *p_tree) { c = c->next; } - if (tree && tree->root == this) { - tree->root = nullptr; - } + if (tree) { + if (tree->root == this) { + tree->root = nullptr; + } - if (tree && tree->popup_edited_item == this) { - tree->popup_edited_item = nullptr; - tree->pressing_for_editor = false; - } + if (tree->popup_edited_item == this) { + tree->popup_edited_item = nullptr; + tree->pressing_for_editor = false; + } - if (tree && tree->cache.hover_item == this) { - tree->cache.hover_item = nullptr; - } + if (tree->cache.hover_item == this) { + tree->cache.hover_item = nullptr; + } - if (tree && tree->selected_item == this) { - tree->selected_item = nullptr; - } + if (tree->selected_item == this) { + tree->selected_item = nullptr; + } - if (tree && tree->drop_mode_over == this) { - tree->drop_mode_over = nullptr; - } + if (tree->drop_mode_over == this) { + tree->drop_mode_over = nullptr; + } - if (tree && tree->single_select_defer == this) { - tree->single_select_defer = nullptr; - } + if (tree->single_select_defer == this) { + tree->single_select_defer = nullptr; + } + + if (tree->edited_item == this) { + tree->edited_item = nullptr; + tree->pressing_for_editor = false; + } - if (tree && tree->edited_item == this) { - tree->edited_item = nullptr; - tree->pressing_for_editor = false; + tree->update(); } tree = p_tree; if (tree) { + tree->update(); cells.resize(tree->columns.size()); } } @@ -151,6 +156,7 @@ void TreeItem::set_cell_mode(int p_column, TreeCellMode p_mode) { c.dirty = true; c.icon_max_w = 0; _changed_notify(p_column); + cached_minimum_size_dirty = true; } TreeItem::TreeCellMode TreeItem::get_cell_mode(int p_column) const { @@ -162,7 +168,21 @@ TreeItem::TreeCellMode TreeItem::get_cell_mode(int p_column) const { void TreeItem::set_checked(int p_column, bool p_checked) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].checked = p_checked; + cells.write[p_column].indeterminate = false; + _changed_notify(p_column); + cached_minimum_size_dirty = true; +} + +void TreeItem::set_indeterminate(int p_column, bool p_indeterminate) { + ERR_FAIL_INDEX(p_column, cells.size()); + // Prevent uncheck if indeterminate set to false twice + if (p_indeterminate == cells[p_column].indeterminate) { + return; + } + cells.write[p_column].indeterminate = p_indeterminate; + cells.write[p_column].checked = false; _changed_notify(p_column); + cached_minimum_size_dirty = true; } bool TreeItem::is_checked(int p_column) const { @@ -170,6 +190,11 @@ bool TreeItem::is_checked(int p_column) const { return cells[p_column].checked; } +bool TreeItem::is_indeterminate(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), false); + return cells[p_column].indeterminate; +} + void TreeItem::set_text(int p_column, String p_text) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].text = p_text; @@ -190,6 +215,7 @@ void TreeItem::set_text(int p_column, String p_text) { cells.write[p_column].step = 0; } _changed_notify(p_column); + cached_minimum_size_dirty = true; } String TreeItem::get_text(int p_column) const { @@ -205,6 +231,7 @@ void TreeItem::set_text_direction(int p_column, Control::TextDirection p_text_di cells.write[p_column].dirty = true; _changed_notify(p_column); } + cached_minimum_size_dirty = true; } Control::TextDirection TreeItem::get_text_direction(int p_column) const { @@ -217,6 +244,7 @@ void TreeItem::clear_opentype_features(int p_column) { cells.write[p_column].opentype_features.clear(); cells.write[p_column].dirty = true; _changed_notify(p_column); + cached_minimum_size_dirty = true; } void TreeItem::set_opentype_feature(int p_column, const String &p_name, int p_value) { @@ -226,6 +254,7 @@ void TreeItem::set_opentype_feature(int p_column, const String &p_name, int p_va cells.write[p_column].opentype_features[tag] = p_value; cells.write[p_column].dirty = true; _changed_notify(p_column); + cached_minimum_size_dirty = true; } } @@ -244,6 +273,7 @@ void TreeItem::set_structured_text_bidi_override(int p_column, Control::Structur cells.write[p_column].st_parser = p_parser; cells.write[p_column].dirty = true; _changed_notify(p_column); + cached_minimum_size_dirty = true; } } @@ -257,6 +287,7 @@ void TreeItem::set_structured_text_bidi_override_options(int p_column, Array p_a cells.write[p_column].st_args = p_args; cells.write[p_column].dirty = true; _changed_notify(p_column); + cached_minimum_size_dirty = true; } Array TreeItem::get_structured_text_bidi_override_options(int p_column) const { @@ -270,6 +301,7 @@ void TreeItem::set_language(int p_column, const String &p_language) { cells.write[p_column].language = p_language; cells.write[p_column].dirty = true; _changed_notify(p_column); + cached_minimum_size_dirty = true; } } @@ -283,6 +315,7 @@ void TreeItem::set_suffix(int p_column, String p_suffix) { cells.write[p_column].suffix = p_suffix; _changed_notify(p_column); + cached_minimum_size_dirty = true; } String TreeItem::get_suffix(int p_column) const { @@ -294,6 +327,7 @@ void TreeItem::set_icon(int p_column, const Ref<Texture2D> &p_icon) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].icon = p_icon; _changed_notify(p_column); + cached_minimum_size_dirty = true; } Ref<Texture2D> TreeItem::get_icon(int p_column) const { @@ -305,6 +339,7 @@ void TreeItem::set_icon_region(int p_column, const Rect2 &p_icon_region) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].icon_region = p_icon_region; _changed_notify(p_column); + cached_minimum_size_dirty = true; } Rect2 TreeItem::get_icon_region(int p_column) const { @@ -327,6 +362,7 @@ void TreeItem::set_icon_max_width(int p_column, int p_max) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].icon_max_w = p_max; _changed_notify(p_column); + cached_minimum_size_dirty = true; } int TreeItem::get_icon_max_width(int p_column) const { @@ -411,7 +447,7 @@ void TreeItem::set_collapsed(bool p_collapsed) { if (tree->select_mode == Tree::SELECT_MULTI) { tree->selected_item = this; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); } else { select(tree->selected_col); } @@ -421,7 +457,7 @@ void TreeItem::set_collapsed(bool p_collapsed) { } _changed_notify(); - tree->emit_signal("item_collapsed", this); + tree->emit_signal(SNAME("item_collapsed"), this); } bool TreeItem::is_collapsed() { @@ -439,6 +475,7 @@ void TreeItem::uncollapse_tree() { void TreeItem::set_custom_minimum_height(int p_height) { custom_min_height = p_height; _changed_notify(); + cached_minimum_size_dirty = true; } int TreeItem::get_custom_minimum_height() const { @@ -451,6 +488,7 @@ TreeItem *TreeItem::create_child(int p_idx) { TreeItem *ti = memnew(TreeItem(tree)); if (tree) { ti->cells.resize(tree->columns.size()); + tree->update(); } TreeItem *l_prev = nullptr; @@ -489,11 +527,11 @@ TreeItem *TreeItem::create_child(int p_idx) { return ti; } -Tree *TreeItem::get_tree() { +Tree *TreeItem::get_tree() const { return tree; } -TreeItem *TreeItem::get_next() { +TreeItem *TreeItem::get_next() const { return next; } @@ -516,11 +554,11 @@ TreeItem *TreeItem::get_prev() { return prev; } -TreeItem *TreeItem::get_parent() { +TreeItem *TreeItem::get_parent() const { return parent; } -TreeItem *TreeItem::get_first_child() { +TreeItem *TreeItem::get_first_child() const { return first_child; } @@ -654,11 +692,7 @@ void TreeItem::move_before(TreeItem *p_item) { next = p_item; p_item->prev = this; - if (old_tree && old_tree != tree) { - old_tree->update(); - } - - if (tree) { + if (tree && old_tree == tree) { tree->update(); } } @@ -696,11 +730,7 @@ void TreeItem::move_after(TreeItem *p_item) { parent->children_cache.append(this); } - if (old_tree && old_tree != tree) { - old_tree->update(); - } - - if (tree) { + if (tree && old_tree == tree) { tree->update(); } } @@ -770,6 +800,7 @@ void TreeItem::add_button(int p_column, const Ref<Texture2D> &p_button, int p_id button.tooltip = p_tooltip; cells.write[p_column].buttons.push_back(button); _changed_notify(p_column); + cached_minimum_size_dirty = true; } int TreeItem::get_button_count(int p_column) const { @@ -813,6 +844,7 @@ void TreeItem::set_button(int p_column, int p_idx, const Ref<Texture2D> &p_butto ERR_FAIL_INDEX(p_idx, cells[p_column].buttons.size()); cells.write[p_column].buttons.write[p_idx].texture = p_button; _changed_notify(p_column); + cached_minimum_size_dirty = true; } void TreeItem::set_button_color(int p_column, int p_idx, const Color &p_color) { @@ -828,6 +860,7 @@ void TreeItem::set_button_disabled(int p_column, int p_idx, bool p_disabled) { cells.write[p_column].buttons.write[p_idx].disabled = p_disabled; _changed_notify(p_column); + cached_minimum_size_dirty = true; } bool TreeItem::is_button_disabled(int p_column, int p_idx) const { @@ -841,6 +874,7 @@ void TreeItem::set_editable(int p_column, bool p_editable) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].editable = p_editable; _changed_notify(p_column); + cached_minimum_size_dirty = true; } bool TreeItem::is_editable(int p_column) { @@ -870,6 +904,28 @@ void TreeItem::clear_custom_color(int p_column) { _changed_notify(p_column); } +void TreeItem::set_custom_font(int p_column, const Ref<Font> &p_font) { + ERR_FAIL_INDEX(p_column, cells.size()); + cells.write[p_column].custom_font = p_font; + cached_minimum_size_dirty = true; +} + +Ref<Font> TreeItem::get_custom_font(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Ref<Font>()); + return cells[p_column].custom_font; +} + +void TreeItem::set_custom_font_size(int p_column, int p_font_size) { + ERR_FAIL_INDEX(p_column, cells.size()); + cells.write[p_column].custom_font_size = p_font_size; + cached_minimum_size_dirty = true; +} + +int TreeItem::get_custom_font_size(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), -1); + return cells[p_column].custom_font_size; +} + void TreeItem::set_tooltip(int p_column, const String &p_tooltip) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].tooltip = p_tooltip; @@ -906,6 +962,7 @@ Color TreeItem::get_custom_bg_color(int p_column) const { void TreeItem::set_custom_as_button(int p_column, bool p_button) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].custom_button = p_button; + cached_minimum_size_dirty = true; } bool TreeItem::is_custom_set_as_button(int p_column) const { @@ -917,6 +974,7 @@ void TreeItem::set_text_align(int p_column, TextAlign p_align) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].text_align = p_align; _changed_notify(p_column); + cached_minimum_size_dirty = true; } TreeItem::TextAlign TreeItem::get_text_align(int p_column) const { @@ -928,6 +986,7 @@ void TreeItem::set_expand_right(int p_column, bool p_enable) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].expand_right = p_enable; _changed_notify(p_column); + cached_minimum_size_dirty = true; } bool TreeItem::get_expand_right(int p_column) const { @@ -938,12 +997,68 @@ bool TreeItem::get_expand_right(int p_column) const { void TreeItem::set_disable_folding(bool p_disable) { disable_folding = p_disable; _changed_notify(0); + cached_minimum_size_dirty = true; } bool TreeItem::is_folding_disabled() const { return disable_folding; } +Size2 TreeItem::get_minimum_size(int p_column) { + ERR_FAIL_INDEX_V(p_column, cells.size(), Size2()); + Tree *tree = get_tree(); + ERR_FAIL_COND_V(!tree, Size2()); + + if (cached_minimum_size_dirty) { + Size2 size; + + // Default offset? + //size.width += (disable_folding || tree->hide_folding) ? tree->cache.hseparation : tree->cache.item_margin; + + // Text. + const TreeItem::Cell &cell = cells[p_column]; + if (!cell.text.is_empty()) { + if (cell.dirty) { + tree->update_item_cell(this, p_column); + } + Size2 text_size = cell.text_buf->get_size(); + size.width += text_size.width; + size.height = MAX(size.height, text_size.height); + } + + // Icon. + if (cell.mode == CELL_MODE_CHECK) { + size.width += tree->cache.checked->get_width() + tree->cache.hseparation; + } + if (cell.icon.is_valid()) { + Size2i icon_size = cell.get_icon_size(); + if (cell.icon_max_w > 0 && icon_size.width > cell.icon_max_w) { + icon_size.width = cell.icon_max_w; + } + size.width += icon_size.width + tree->cache.hseparation; + size.height = MAX(size.height, icon_size.height); + } + + // Buttons. + for (int i = 0; i < cell.buttons.size(); i++) { + Ref<Texture2D> texture = cell.buttons[i].texture; + if (texture.is_valid()) { + Size2 button_size = texture->get_size() + tree->cache.button_pressed->get_minimum_size(); + size.width += button_size.width; + size.height = MAX(size.height, button_size.height); + } + } + if (cell.buttons.size() >= 2) { + size.width += (cell.buttons.size() - 1) * tree->cache.button_margin; + } + + cached_minimum_size = size; + cached_minimum_size_dirty = false; + } + + return cached_minimum_size; +} + Variant TreeItem::_call_recursive_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error) { if (p_argcount < 1) { r_error.error = Callable::CallError::CALL_ERROR_TOO_FEW_ARGUMENTS; @@ -985,7 +1100,9 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("get_cell_mode", "column"), &TreeItem::get_cell_mode); ClassDB::bind_method(D_METHOD("set_checked", "column", "checked"), &TreeItem::set_checked); + ClassDB::bind_method(D_METHOD("set_indeterminate", "column", "indeterminate"), &TreeItem::set_indeterminate); ClassDB::bind_method(D_METHOD("is_checked", "column"), &TreeItem::is_checked); + ClassDB::bind_method(D_METHOD("is_indeterminate", "column"), &TreeItem::is_indeterminate); ClassDB::bind_method(D_METHOD("set_text", "column", "text"), &TreeItem::set_text); ClassDB::bind_method(D_METHOD("get_text", "column"), &TreeItem::get_text); @@ -1050,8 +1167,14 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("is_editable", "column"), &TreeItem::is_editable); ClassDB::bind_method(D_METHOD("set_custom_color", "column", "color"), &TreeItem::set_custom_color); - ClassDB::bind_method(D_METHOD("clear_custom_color", "column"), &TreeItem::clear_custom_color); ClassDB::bind_method(D_METHOD("get_custom_color", "column"), &TreeItem::get_custom_color); + ClassDB::bind_method(D_METHOD("clear_custom_color", "column"), &TreeItem::clear_custom_color); + + ClassDB::bind_method(D_METHOD("set_custom_font", "column", "font"), &TreeItem::set_custom_font); + ClassDB::bind_method(D_METHOD("get_custom_font", "column"), &TreeItem::get_custom_font); + + ClassDB::bind_method(D_METHOD("set_custom_font_size", "column", "font_size"), &TreeItem::set_custom_font_size); + ClassDB::bind_method(D_METHOD("get_custom_font_size", "column"), &TreeItem::get_custom_font_size); ClassDB::bind_method(D_METHOD("set_custom_bg_color", "column", "color", "just_outline"), &TreeItem::set_custom_bg_color, DEFVAL(false)); ClassDB::bind_method(D_METHOD("clear_custom_bg_color", "column"), &TreeItem::clear_custom_bg_color); @@ -1155,61 +1278,69 @@ TreeItem::~TreeItem() { /**********************************************/ void Tree::update_cache() { - cache.font = get_theme_font("font"); - cache.font_size = get_theme_font_size("font_size"); - cache.tb_font = get_theme_font("title_button_font"); - cache.tb_font_size = get_theme_font_size("title_button_font_size"); - cache.bg = get_theme_stylebox("bg"); - cache.selected = get_theme_stylebox("selected"); - cache.selected_focus = get_theme_stylebox("selected_focus"); - cache.cursor = get_theme_stylebox("cursor"); - cache.cursor_unfocus = get_theme_stylebox("cursor_unfocused"); - cache.button_pressed = get_theme_stylebox("button_pressed"); - - cache.checked = get_theme_icon("checked"); - cache.unchecked = get_theme_icon("unchecked"); + cache.font = get_theme_font(SNAME("font")); + cache.font_size = get_theme_font_size(SNAME("font_size")); + cache.tb_font = get_theme_font(SNAME("title_button_font")); + cache.tb_font_size = get_theme_font_size(SNAME("title_button_font_size")); + cache.bg = get_theme_stylebox(SNAME("bg")); + cache.selected = get_theme_stylebox(SNAME("selected")); + cache.selected_focus = get_theme_stylebox(SNAME("selected_focus")); + cache.cursor = get_theme_stylebox(SNAME("cursor")); + cache.cursor_unfocus = get_theme_stylebox(SNAME("cursor_unfocused")); + cache.button_pressed = get_theme_stylebox(SNAME("button_pressed")); + + cache.checked = get_theme_icon(SNAME("checked")); + cache.unchecked = get_theme_icon(SNAME("unchecked")); + cache.indeterminate = get_theme_icon(SNAME("indeterminate")); if (is_layout_rtl()) { - cache.arrow_collapsed = get_theme_icon("arrow_collapsed_mirrored"); + cache.arrow_collapsed = get_theme_icon(SNAME("arrow_collapsed_mirrored")); } else { - cache.arrow_collapsed = get_theme_icon("arrow_collapsed"); - } - cache.arrow = get_theme_icon("arrow"); - cache.select_arrow = get_theme_icon("select_arrow"); - cache.updown = get_theme_icon("updown"); - - cache.custom_button = get_theme_stylebox("custom_button"); - cache.custom_button_hover = get_theme_stylebox("custom_button_hover"); - cache.custom_button_pressed = get_theme_stylebox("custom_button_pressed"); - cache.custom_button_font_highlight = get_theme_color("custom_button_font_highlight"); - - cache.font_color = get_theme_color("font_color"); - cache.font_selected_color = get_theme_color("font_selected_color"); - cache.drop_position_color = get_theme_color("drop_position_color"); - cache.hseparation = get_theme_constant("hseparation"); - cache.vseparation = get_theme_constant("vseparation"); - cache.item_margin = get_theme_constant("item_margin"); - cache.button_margin = get_theme_constant("button_margin"); - - cache.draw_guides = get_theme_constant("draw_guides"); - cache.guide_color = get_theme_color("guide_color"); - cache.draw_relationship_lines = get_theme_constant("draw_relationship_lines"); - cache.relationship_line_width = get_theme_constant("relationship_line_width"); - cache.parent_hl_line_width = get_theme_constant("parent_hl_line_width"); - cache.children_hl_line_width = get_theme_constant("children_hl_line_width"); - cache.parent_hl_line_margin = get_theme_constant("parent_hl_line_margin"); - cache.relationship_line_color = get_theme_color("relationship_line_color"); - cache.parent_hl_line_color = get_theme_color("parent_hl_line_color"); - cache.children_hl_line_color = get_theme_color("children_hl_line_color"); - - cache.scroll_border = get_theme_constant("scroll_border"); - cache.scroll_speed = get_theme_constant("scroll_speed"); - - cache.title_button = get_theme_stylebox("title_button_normal"); - cache.title_button_pressed = get_theme_stylebox("title_button_pressed"); - cache.title_button_hover = get_theme_stylebox("title_button_hover"); - cache.title_button_color = get_theme_color("title_button_color"); + cache.arrow_collapsed = get_theme_icon(SNAME("arrow_collapsed")); + } + cache.arrow = get_theme_icon(SNAME("arrow")); + cache.select_arrow = get_theme_icon(SNAME("select_arrow")); + cache.updown = get_theme_icon(SNAME("updown")); + + cache.custom_button = get_theme_stylebox(SNAME("custom_button")); + cache.custom_button_hover = get_theme_stylebox(SNAME("custom_button_hover")); + cache.custom_button_pressed = get_theme_stylebox(SNAME("custom_button_pressed")); + cache.custom_button_font_highlight = get_theme_color(SNAME("custom_button_font_highlight")); + + cache.font_color = get_theme_color(SNAME("font_color")); + cache.font_selected_color = get_theme_color(SNAME("font_selected_color")); + cache.drop_position_color = get_theme_color(SNAME("drop_position_color")); + cache.hseparation = get_theme_constant(SNAME("hseparation")); + cache.vseparation = get_theme_constant(SNAME("vseparation")); + cache.item_margin = get_theme_constant(SNAME("item_margin")); + cache.button_margin = get_theme_constant(SNAME("button_margin")); + + cache.font_outline_color = get_theme_color(SNAME("font_outline_color")); + cache.font_outline_size = get_theme_constant(SNAME("outline_size")); + + cache.draw_guides = get_theme_constant(SNAME("draw_guides")); + cache.guide_color = get_theme_color(SNAME("guide_color")); + cache.draw_relationship_lines = get_theme_constant(SNAME("draw_relationship_lines")); + cache.relationship_line_width = get_theme_constant(SNAME("relationship_line_width")); + cache.parent_hl_line_width = get_theme_constant(SNAME("parent_hl_line_width")); + cache.children_hl_line_width = get_theme_constant(SNAME("children_hl_line_width")); + cache.parent_hl_line_margin = get_theme_constant(SNAME("parent_hl_line_margin")); + cache.relationship_line_color = get_theme_color(SNAME("relationship_line_color")); + cache.parent_hl_line_color = get_theme_color(SNAME("parent_hl_line_color")); + cache.children_hl_line_color = get_theme_color(SNAME("children_hl_line_color")); + + cache.scroll_border = get_theme_constant(SNAME("scroll_border")); + cache.scroll_speed = get_theme_constant(SNAME("scroll_speed")); + + cache.title_button = get_theme_stylebox(SNAME("title_button_normal")); + cache.title_button_pressed = get_theme_stylebox(SNAME("title_button_pressed")); + cache.title_button_hover = get_theme_stylebox(SNAME("title_button_hover")); + cache.title_button_color = get_theme_color(SNAME("title_button_color")); v_scroll->set_custom_step(cache.font->get_height(cache.font_size)); + + for (TreeItem *item = get_root(); item; item = item->get_next()) { + item->cached_minimum_size_dirty = true; + } } int Tree::compute_item_height(TreeItem *p_item) const { @@ -1372,6 +1503,7 @@ void Tree::update_column(int p_col) { } else { columns.write[p_col].text_buf->set_direction((TextServer::Direction)columns[p_col].text_direction); } + columns.write[p_col].text_buf->add_string(columns[p_col].title, cache.font, cache.font_size, columns[p_col].opentype_features, (columns[p_col].language != "") ? columns[p_col].language : TranslationServer::get_singleton()->get_tool_locale()); } @@ -1416,7 +1548,21 @@ void Tree::update_item_cell(TreeItem *p_item, int p_col) { } else { p_item->cells.write[p_col].text_buf->set_direction((TextServer::Direction)p_item->cells[p_col].text_direction); } - p_item->cells.write[p_col].text_buf->add_string(valtext, cache.font, cache.font_size, p_item->cells[p_col].opentype_features, (p_item->cells[p_col].language != "") ? p_item->cells[p_col].language : TranslationServer::get_singleton()->get_tool_locale()); + + Ref<Font> font; + if (p_item->cells[p_col].custom_font.is_valid()) { + font = p_item->cells[p_col].custom_font; + } else { + font = cache.font; + } + + int font_size; + if (p_item->cells[p_col].custom_font_size > 0) { + font_size = p_item->cells[p_col].custom_font_size; + } else { + font_size = cache.font_size; + } + p_item->cells.write[p_col].text_buf->add_string(valtext, font, font_size, p_item->cells[p_col].opentype_features, (p_item->cells[p_col].language != "") ? p_item->cells[p_col].language : TranslationServer::get_singleton()->get_tool_locale()); TS->shaped_text_set_bidi_override(p_item->cells[p_col].text_buf->get_rid(), structured_text_parser(p_item->cells[p_col].st_parser, p_item->cells[p_col].st_args, valtext)); p_item->cells.write[p_col].dirty = false; } @@ -1443,7 +1589,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 int htotal = 0; int label_h = compute_item_height(p_item); - bool rtl = is_layout_rtl(); + bool rtl = cache.rtl; /* Calculate height of the label part */ label_h += cache.vseparation; @@ -1489,6 +1635,20 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } } + if (!rtl && p_item->cells[i].buttons.size()) { + int button_w = 0; + for (int j = p_item->cells[i].buttons.size() - 1; j >= 0; j--) { + Ref<Texture2D> b = p_item->cells[i].buttons[j].texture; + button_w += b->get_size().width + cache.button_pressed->get_minimum_size().width + cache.button_margin; + } + + int total_ofs = ofs - cache.offset.x; + + if (total_ofs + w > p_draw_size.width) { + w = MAX(button_w, p_draw_size.width - total_ofs); + } + } + int bw = 0; for (int j = p_item->cells[i].buttons.size() - 1; j >= 0; j--) { Ref<Texture2D> b = p_item->cells[i].buttons[j].texture; @@ -1591,30 +1751,36 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } } - if (drop_mode_flags && drop_mode_over == p_item) { + if (drop_mode_flags && drop_mode_over) { Rect2 r = cell_rect; - bool has_parent = p_item->get_first_child() != nullptr; if (rtl) { r.position.x = get_size().width - r.position.x - r.size.x; } - - if (drop_mode_section == -1 || has_parent || drop_mode_section == 0) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y, r.size.x, 1), cache.drop_position_color); - } - - if (drop_mode_section == 0) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y, 1, r.size.y), cache.drop_position_color); - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x + r.size.x - 1, r.position.y, 1, r.size.y), cache.drop_position_color); - } - - if ((drop_mode_section == 1 && !has_parent) || drop_mode_section == 0) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y + r.size.y, r.size.x, 1), cache.drop_position_color); + if (drop_mode_over == p_item) { + if (drop_mode_section == 0 || drop_mode_section == -1) { + // Line above. + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y, r.size.x, 1), cache.drop_position_color); + } + if (drop_mode_section == 0) { + // Side lines. + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y, 1, r.size.y), cache.drop_position_color); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x + r.size.x - 1, r.position.y, 1, r.size.y), cache.drop_position_color); + } + if (drop_mode_section == 0 || (drop_mode_section == 1 && (!p_item->get_first_child() || p_item->is_collapsed()))) { + // Line below. + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y + r.size.y, r.size.x, 1), cache.drop_position_color); + } + } else if (drop_mode_over == p_item->get_parent()) { + if (drop_mode_section == 1 && !p_item->get_prev() /* && !drop_mode_over->is_collapsed() */) { // The drop_mode_over shouldn't ever be collapsed in here, otherwise we would be drawing a child of a collapsed item. + // Line above. + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y, r.size.x, 1), cache.drop_position_color); + } } } Color col = p_item->cells[i].custom_color ? p_item->cells[i].color : get_theme_color(p_item->cells[i].selected ? "font_selected_color" : "font_color"); - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + Color font_outline_color = cache.font_outline_color; + int outline_size = cache.font_outline_size; Color icon_col = p_item->cells[i].icon_color; if (p_item->cells[i].dirty) { @@ -1636,10 +1802,13 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 case TreeItem::CELL_MODE_CHECK: { Ref<Texture2D> checked = cache.checked; Ref<Texture2D> unchecked = cache.unchecked; + Ref<Texture2D> indeterminate = cache.indeterminate; Point2i check_ofs = item_rect.position; check_ofs.y += Math::floor((real_t)(item_rect.size.y - checked->get_height()) / 2); - if (p_item->cells[i].checked) { + if (p_item->cells[i].indeterminate) { + indeterminate->draw(ci, check_ofs); + } else if (p_item->cells[i].checked) { checked->draw(ci, check_ofs); } else { unchecked->draw(ci, check_ofs); @@ -1826,78 +1995,76 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 int prev_hl_ofs = base_ofs; while (c) { - if (cache.draw_relationship_lines > 0 && (!hide_root || c->parent != root)) { - int root_ofs = children_pos.x + ((p_item->disable_folding || hide_folding) ? cache.hseparation : cache.item_margin); - int parent_ofs = p_pos.x + cache.item_margin; - Point2i root_pos = Point2i(root_ofs, children_pos.y + label_h / 2) - cache.offset + p_draw_ofs; + if (htotal >= 0) { + int child_h = draw_item(children_pos, p_draw_ofs, p_draw_size, c); - if (c->get_first_child() != nullptr) { - root_pos -= Point2i(cache.arrow->get_width(), 0); - } + // Draw relationship lines. + if (cache.draw_relationship_lines > 0 && (!hide_root || c->parent != root)) { + int root_ofs = children_pos.x + ((p_item->disable_folding || hide_folding) ? cache.hseparation : cache.item_margin); + int parent_ofs = p_pos.x + cache.item_margin; + Point2i root_pos = Point2i(root_ofs, children_pos.y + label_h / 2) - cache.offset + p_draw_ofs; - float line_width = cache.relationship_line_width; - float parent_line_width = cache.parent_hl_line_width; - float children_line_width = cache.children_hl_line_width; + if (c->get_first_child() != nullptr) { + root_pos -= Point2i(cache.arrow->get_width(), 0); + } + + float line_width = cache.relationship_line_width; + float parent_line_width = cache.parent_hl_line_width; + float children_line_width = cache.children_hl_line_width; #ifdef TOOLS_ENABLED - line_width *= Math::round(EDSCALE); - parent_line_width *= Math::round(EDSCALE); - children_line_width *= Math::round(EDSCALE); + line_width *= Math::round(EDSCALE); + parent_line_width *= Math::round(EDSCALE); + children_line_width *= Math::round(EDSCALE); #endif - Point2i parent_pos = Point2i(parent_ofs - cache.arrow->get_width() / 2, p_pos.y + label_h / 2 + cache.arrow->get_height() / 2) - cache.offset + p_draw_ofs; + Point2i parent_pos = Point2i(parent_ofs - cache.arrow->get_width() / 2, p_pos.y + label_h / 2 + cache.arrow->get_height() / 2) - cache.offset + p_draw_ofs; - int more_prev_ofs = 0; + int more_prev_ofs = 0; - if (root_pos.y + line_width >= 0) { - if (rtl) { - root_pos.x = get_size().width - root_pos.x; - parent_pos.x = get_size().width - parent_pos.x; - } + if (root_pos.y + line_width >= 0) { + if (rtl) { + root_pos.x = get_size().width - root_pos.x; + parent_pos.x = get_size().width - parent_pos.x; + } - // Order of parts on this bend: the horizontal line first, then the vertical line. - if (_is_branch_selected(c)) { - // If this item or one of its children is selected, we draw the line using parent highlight style. - RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + Math::floor(parent_line_width / 2), root_pos.y), cache.parent_hl_line_color, parent_line_width); - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(parent_line_width / 2)), Point2i(parent_pos.x, prev_hl_ofs), cache.parent_hl_line_color, parent_line_width); - - more_prev_ofs = cache.parent_hl_line_margin; - prev_hl_ofs = root_pos.y + Math::floor(parent_line_width / 2); - } else if (p_item->is_selected(0)) { - // If parent item is selected (but this item is not), we draw the line using children highlight style. - // Siblings of the selected branch can be drawn with a slight offset and their vertical line must appear as highlighted. - if (_is_sibling_branch_selected(c)) { - RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + Math::floor(parent_line_width / 2), root_pos.y), cache.children_hl_line_color, children_line_width); + // Order of parts on this bend: the horizontal line first, then the vertical line. + if (_is_branch_selected(c)) { + // If this item or one of its children is selected, we draw the line using parent highlight style. + RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + Math::floor(parent_line_width / 2), root_pos.y), cache.parent_hl_line_color, parent_line_width); RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(parent_line_width / 2)), Point2i(parent_pos.x, prev_hl_ofs), cache.parent_hl_line_color, parent_line_width); + more_prev_ofs = cache.parent_hl_line_margin; prev_hl_ofs = root_pos.y + Math::floor(parent_line_width / 2); + } else if (p_item->is_selected(0)) { + // If parent item is selected (but this item is not), we draw the line using children highlight style. + // Siblings of the selected branch can be drawn with a slight offset and their vertical line must appear as highlighted. + if (_is_sibling_branch_selected(c)) { + RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + Math::floor(parent_line_width / 2), root_pos.y), cache.children_hl_line_color, children_line_width); + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(parent_line_width / 2)), Point2i(parent_pos.x, prev_hl_ofs), cache.parent_hl_line_color, parent_line_width); + + prev_hl_ofs = root_pos.y + Math::floor(parent_line_width / 2); + } else { + RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + Math::floor(children_line_width / 2), root_pos.y), cache.children_hl_line_color, children_line_width); + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(children_line_width / 2)), Point2i(parent_pos.x, prev_ofs + Math::floor(children_line_width / 2)), cache.children_hl_line_color, children_line_width); + } } else { - RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + Math::floor(children_line_width / 2), root_pos.y), cache.children_hl_line_color, children_line_width); - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(children_line_width / 2)), Point2i(parent_pos.x, prev_ofs + Math::floor(children_line_width / 2)), cache.children_hl_line_color, children_line_width); - } - } else { - // If nothing of the above is true, we draw the line using normal style. - // Siblings of the selected branch can be drawn with a slight offset and their vertical line must appear as highlighted. - if (_is_sibling_branch_selected(c)) { - RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + cache.parent_hl_line_margin, root_pos.y), cache.relationship_line_color, line_width); - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(parent_line_width / 2)), Point2i(parent_pos.x, prev_hl_ofs), cache.parent_hl_line_color, parent_line_width); + // If nothing of the above is true, we draw the line using normal style. + // Siblings of the selected branch can be drawn with a slight offset and their vertical line must appear as highlighted. + if (_is_sibling_branch_selected(c)) { + RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + cache.parent_hl_line_margin, root_pos.y), cache.relationship_line_color, line_width); + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(parent_line_width / 2)), Point2i(parent_pos.x, prev_hl_ofs), cache.parent_hl_line_color, parent_line_width); - prev_hl_ofs = root_pos.y + Math::floor(parent_line_width / 2); - } else { - RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + Math::floor(line_width / 2), root_pos.y), cache.relationship_line_color, line_width); - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(line_width / 2)), Point2i(parent_pos.x, prev_ofs + Math::floor(line_width / 2)), cache.relationship_line_color, line_width); + prev_hl_ofs = root_pos.y + Math::floor(parent_line_width / 2); + } else { + RenderingServer::get_singleton()->canvas_item_add_line(ci, root_pos, Point2i(parent_pos.x + Math::floor(line_width / 2), root_pos.y), cache.relationship_line_color, line_width); + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(parent_pos.x, root_pos.y + Math::floor(line_width / 2)), Point2i(parent_pos.x, prev_ofs + Math::floor(line_width / 2)), cache.relationship_line_color, line_width); + } } } - } - if (htotal < 0) { - return -1; + prev_ofs = root_pos.y + more_prev_ofs; } - prev_ofs = root_pos.y + more_prev_ofs; - } - - if (htotal >= 0) { - int child_h = draw_item(children_pos, p_draw_ofs, p_draw_size, c); if (child_h < 0) { if (cache.draw_relationship_lines == 0) { @@ -1992,7 +2159,7 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c selected_item = p_selected; selected_col = 0; if (!emitted_row) { - emit_signal("item_selected"); + emit_signal(SNAME("item_selected")); emitted_row = true; } /* @@ -2012,28 +2179,28 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c selected_item = p_selected; selected_col = i; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); if (select_mode == SELECT_MULTI) { - emit_signal("multi_selected", p_current, i, true); + emit_signal(SNAME("multi_selected"), p_current, i, true); } else if (select_mode == SELECT_SINGLE) { - emit_signal("item_selected"); + emit_signal(SNAME("item_selected")); } } else if (select_mode == SELECT_MULTI && (selected_item != p_selected || selected_col != i)) { selected_item = p_selected; selected_col = i; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); } } else { if (r_in_range && *r_in_range && !p_force_deselect) { if (!c.selected && c.selectable) { c.selected = true; - emit_signal("multi_selected", p_current, i, true); + emit_signal(SNAME("multi_selected"), p_current, i, true); } } else if (!r_in_range || p_force_deselect) { if (select_mode == SELECT_MULTI && c.selected) { - emit_signal("multi_selected", p_current, i, false); + emit_signal(SNAME("multi_selected"), p_current, i, false); } c.selected = false; } @@ -2070,13 +2237,24 @@ void Tree::_range_click_timeout() { } } + if (!root) { + return; + } + click_handled = false; Ref<InputEventMouseButton> mb; - mb.instance(); + mb.instantiate(); + + int x_limit = get_size().width - cache.bg->get_minimum_size().width; + if (h_scroll->is_visible()) { + x_limit -= h_scroll->get_minimum_size().width; + } + + cache.rtl = is_layout_rtl(); propagate_mouse_activated = false; // done from outside, so signal handler can't clear the tree in the middle of emit (which is a common case) blocked++; - propagate_mouse_event(pos + cache.offset, 0, 0, false, root, MOUSE_BUTTON_LEFT, mb); + propagate_mouse_event(pos + cache.offset, 0, 0, x_limit + cache.offset.width, false, root, MOUSE_BUTTON_LEFT, mb); blocked--; if (range_click_timer->is_one_shot()) { @@ -2090,7 +2268,7 @@ void Tree::_range_click_timeout() { } if (propagate_mouse_activated) { - emit_signal("item_activated"); + emit_signal(SNAME("item_activated")); propagate_mouse_activated = false; } @@ -2099,7 +2277,7 @@ void Tree::_range_click_timeout() { } } -int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool p_double_click, TreeItem *p_item, int p_button, const Ref<InputEventWithModifiers> &p_mod) { +int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int x_limit, bool p_double_click, TreeItem *p_item, int p_button, const Ref<InputEventWithModifiers> &p_mod) { int item_h = compute_item_height(p_item) + cache.vseparation; bool skip = (p_item == root && hide_root); @@ -2124,6 +2302,9 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool int col = -1; int col_ofs = 0; int col_width = 0; + + int limit_w = x_limit; + for (int i = 0; i < columns.size(); i++) { col_width = get_column_width(i); @@ -2139,6 +2320,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool if (x > col_width) { col_ofs += col_width; x -= col_width; + limit_w -= col_width; continue; } @@ -2152,10 +2334,12 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool int margin = x_ofs + cache.item_margin; //-cache.hseparation; //int lm = cache.bg->get_margin(SIDE_LEFT); col_width -= margin; + limit_w -= margin; col_ofs += margin; x -= margin; } else { col_width -= cache.hseparation; + limit_w -= cache.hseparation; x -= cache.hseparation; } @@ -2169,6 +2353,16 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool bool already_selected = c.selected; bool already_cursor = (p_item == selected_item) && col == selected_col; + if (!cache.rtl && p_item->cells[col].buttons.size()) { + int button_w = 0; + for (int j = p_item->cells[col].buttons.size() - 1; j >= 0; j--) { + Ref<Texture2D> b = p_item->cells[col].buttons[j].texture; + button_w += b->get_size().width + cache.button_pressed->get_minimum_size().width + cache.button_margin; + } + + col_width = MAX(button_w, MIN(limit_w, col_width)); + } + for (int j = c.buttons.size() - 1; j >= 0; j--) { Ref<Texture2D> b = c.buttons[j].texture; int w = b->get_size().width + cache.button_pressed->get_minimum_size().width; @@ -2179,17 +2373,27 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool cache.click_type = Cache::CLICK_NONE; return -1; } + + // Make sure the click is correct. + Point2 click_pos = get_global_mouse_position() - get_global_position(); + if (!get_item_at_position(click_pos)) { + pressed_button = -1; + cache.click_type = Cache::CLICK_NONE; + return -1; + } + pressed_button = j; cache.click_type = Cache::CLICK_BUTTON; cache.click_index = j; cache.click_id = c.buttons[j].id; cache.click_item = p_item; cache.click_column = col; - cache.click_pos = get_global_mouse_position() - get_global_position(); + cache.click_pos = click_pos; update(); - //emit_signal("button_pressed"); + //emit_signal(SNAME("button_pressed")); return -1; } + col_width -= w + cache.button_margin; } @@ -2207,15 +2411,15 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool if (select_mode == SELECT_MULTI && p_mod->is_command_pressed() && c.selectable) { if (!c.selected || p_button == MOUSE_BUTTON_RIGHT) { p_item->select(col); - emit_signal("multi_selected", p_item, col, true); + emit_signal(SNAME("multi_selected"), p_item, col, true); if (p_button == MOUSE_BUTTON_RIGHT) { - emit_signal("item_rmb_selected", get_local_mouse_position()); + emit_signal(SNAME("item_rmb_selected"), get_local_mouse_position()); } //p_item->selected_signal.call(col); } else { p_item->deselect(col); - emit_signal("multi_selected", p_item, col, false); + emit_signal(SNAME("multi_selected"), p_item, col, false); //p_item->deselected_signal.call(col); } @@ -2226,7 +2430,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool select_single_item(p_item, root, col, selected_item, &inrange); if (p_button == MOUSE_BUTTON_RIGHT) { - emit_signal("item_rmb_selected", get_local_mouse_position()); + emit_signal(SNAME("item_rmb_selected"), get_local_mouse_position()); } } else { int icount = _count_selected_items(root); @@ -2240,14 +2444,14 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool } if (p_button == MOUSE_BUTTON_RIGHT) { - emit_signal("item_rmb_selected", get_local_mouse_position()); + emit_signal(SNAME("item_rmb_selected"), get_local_mouse_position()); } } } /* if (!c.selected && select_mode==SELECT_MULTI) { - emit_signal("multi_selected",p_item,col,true); + emit_signal(SNAME("multi_selected"),p_item,col,true); } */ update(); @@ -2361,7 +2565,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool custom_popup_rect = Rect2i(get_global_position() + Point2i(col_ofs, _get_title_button_height() + y_ofs + item_h - cache.offset.y), Size2(get_column_width(col), item_h)); if (on_arrow || !p_item->cells[col].custom_button) { - emit_signal("custom_popup_edited", ((bool)(x >= (col_width - item_h / 2)))); + emit_signal(SNAME("custom_popup_edited"), ((bool)(x >= (col_width - item_h / 2)))); } if (!p_item->cells[col].custom_button || !on_arrow) { @@ -2400,7 +2604,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool TreeItem *c = p_item->first_child; while (c) { - int child_h = propagate_mouse_event(new_pos, x_ofs, y_ofs, p_double_click, c, p_button, p_mod); + int child_h = propagate_mouse_event(new_pos, x_ofs, y_ofs, x_limit, p_double_click, c, p_button, p_mod); if (child_h < 0) { return -1; // break, stop propagating, no need to anymore @@ -2413,7 +2617,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool } } if (p_item == root && p_button == MOUSE_BUTTON_RIGHT) { - emit_signal("empty_rmb", get_local_mouse_position()); + emit_signal(SNAME("empty_rmb"), get_local_mouse_position()); } } @@ -2431,10 +2635,10 @@ void Tree::_text_editor_modal_close() { return; } - _text_editor_enter(text_editor->get_text()); + _text_editor_submit(text_editor->get_text()); } -void Tree::_text_editor_enter(String p_text) { +void Tree::_text_editor_submit(String p_text) { popup_editor->hide(); if (!popup_edited_item) { @@ -2519,7 +2723,7 @@ void Tree::_go_left() { } else { if (select_mode == SELECT_MULTI) { selected_col--; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); } else { selected_item->select(selected_col - 1); } @@ -2540,7 +2744,7 @@ void Tree::_go_right() { } else { if (select_mode == SELECT_MULTI) { selected_col++; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); } else { selected_item->select(selected_col + 1); } @@ -2573,7 +2777,7 @@ void Tree::_go_up() { return; } selected_item = prev; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); update(); } else { int col = selected_col < 0 ? 0 : selected_col; @@ -2616,7 +2820,7 @@ void Tree::_go_down() { } selected_item = next; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); update(); } else { int col = selected_col < 0 ? 0 : selected_col; @@ -2634,7 +2838,7 @@ void Tree::_go_down() { accept_event(); } -void Tree::_gui_input(Ref<InputEvent> p_event) { +void Tree::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventKey> k = p_event; @@ -2717,7 +2921,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (select_mode == SELECT_MULTI) { selected_item = next; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); update(); } else { while (next && !next->cells[selected_col].selectable) { @@ -2755,7 +2959,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (select_mode == SELECT_MULTI) { selected_item = prev; - emit_signal("cell_selected"); + emit_signal(SNAME("cell_selected")); update(); } else { while (prev && !prev->cells[selected_col].selectable) { @@ -2771,7 +2975,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (selected_item) { //bring up editor if possible if (!edit_selected()) { - emit_signal("item_activated"); + emit_signal(SNAME("item_activated")); incr_search.clear(); } } @@ -2783,10 +2987,10 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } if (selected_item->is_selected(selected_col)) { selected_item->deselect(selected_col); - emit_signal("multi_selected", selected_item, selected_col, false); + emit_signal(SNAME("multi_selected"), selected_item, selected_col, false); } else if (selected_item->is_selectable(selected_col)) { selected_item->select(selected_col); - emit_signal("multi_selected", selected_item, selected_col, true); + emit_signal(SNAME("multi_selected"), selected_item, selected_col, true); } } accept_event(); @@ -2970,7 +3174,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { for (int i = 0; i < columns.size(); i++) { len += get_column_width(i); if (pos.x < len) { - emit_signal("column_title_pressed", i); + emit_signal(SNAME("column_title_pressed"), i); break; } } @@ -2997,10 +3201,10 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } if (rect.has_point(mpos)) { if (!edit_selected()) { - emit_signal("item_double_clicked"); + emit_signal(SNAME("item_double_clicked")); } } else { - emit_signal("item_double_clicked"); + emit_signal(SNAME("item_double_clicked")); } } pressing_for_editor = false; @@ -3009,7 +3213,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (cache.click_type == Cache::CLICK_BUTTON && cache.click_item != nullptr) { // make sure in case of wrong reference after reconstructing whole TreeItems cache.click_item = get_item_at_position(cache.click_pos); - emit_signal("button_pressed", cache.click_item, cache.click_column, cache.click_id); + emit_signal(SNAME("button_pressed"), cache.click_item, cache.click_column, cache.click_id); } cache.click_type = Cache::CLICK_NONE; cache.click_index = -1; @@ -3069,7 +3273,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } if (!root || (!root->get_first_child() && hide_root)) { if (b->get_button_index() == MOUSE_BUTTON_RIGHT && allow_rmb_select) { - emit_signal("empty_tree_rmb_selected", get_local_mouse_position()); + emit_signal(SNAME("empty_tree_rmb_selected"), get_local_mouse_position()); } break; } @@ -3078,8 +3282,14 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { pressing_for_editor = false; propagate_mouse_activated = false; + int x_limit = get_size().width - cache.bg->get_minimum_size().width; + if (h_scroll->is_visible()) { + x_limit -= h_scroll->get_minimum_size().width; + } + + cache.rtl = is_layout_rtl(); blocked++; - propagate_mouse_event(pos + cache.offset, 0, 0, b->is_double_click(), root, b->get_button_index(), b); + propagate_mouse_event(pos + cache.offset, 0, 0, x_limit + cache.offset.width, b->is_double_click(), root, b->get_button_index(), b); blocked--; if (pressing_for_editor) { @@ -3106,7 +3316,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { drag_accum = 0; //last_drag_accum=0; drag_from = v_scroll->get_value(); - drag_touching = !DisplayServer::get_singleton()->screen_is_touchscreen(DisplayServer::get_singleton()->window_get_current_screen(get_viewport()->get_window_id())); + drag_touching = DisplayServer::get_singleton()->screen_is_touchscreen(DisplayServer::get_singleton()->window_get_current_screen(get_viewport()->get_window_id())); drag_touching_deaccel = false; if (drag_touching) { set_physics_process_internal(true); @@ -3114,13 +3324,13 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (b->get_button_index() == MOUSE_BUTTON_LEFT) { if (get_item_at_position(b->get_position()) == nullptr && !b->is_shift_pressed() && !b->is_ctrl_pressed() && !b->is_command_pressed()) { - emit_signal("nothing_selected"); + emit_signal(SNAME("nothing_selected")); } } } if (propagate_mouse_activated) { - emit_signal("item_activated"); + emit_signal(SNAME("item_activated")); propagate_mouse_activated = false; } @@ -3141,6 +3351,8 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } } break; + default: + break; } } @@ -3187,7 +3399,7 @@ bool Tree::edit_selected() { edited_item = s; edited_col = col; custom_popup_rect = Rect2i(get_global_position() + rect.position, rect.size); - emit_signal("custom_popup_edited", false); + emit_signal(SNAME("custom_popup_edited"), false); item_edited(col, s); return true; @@ -3256,7 +3468,7 @@ Size2 Tree::get_internal_min_size() const { size.height += get_item_height(root); } for (int i = 0; i < columns.size(); i++) { - size.width += columns[i].min_width; + size.width += get_column_minimum_width(i); } return size; @@ -3280,26 +3492,38 @@ void Tree::update_scrollbars() { h_scroll->set_begin(Point2(0, size.height - hmin.height)); h_scroll->set_end(Point2(size.width - vmin.width, size.height)); - Size2 min = get_internal_min_size(); + Size2 internal_min_size = get_internal_min_size(); - if (min.height < size.height - hmin.height) { - v_scroll->hide(); - cache.offset.y = 0; - } else { + bool display_vscroll = internal_min_size.height + cache.bg->get_margin(SIDE_TOP) > size.height; + bool display_hscroll = internal_min_size.width + cache.bg->get_margin(SIDE_LEFT) > size.width; + for (int i = 0; i < 2; i++) { + // Check twice, as both values are dependent on each other. + if (display_hscroll) { + display_vscroll = internal_min_size.height + cache.bg->get_margin(SIDE_TOP) + hmin.height > size.height; + } + if (display_vscroll) { + display_hscroll = internal_min_size.width + cache.bg->get_margin(SIDE_LEFT) + vmin.width > size.width; + } + } + + if (display_vscroll) { v_scroll->show(); - v_scroll->set_max(min.height); + v_scroll->set_max(internal_min_size.height); v_scroll->set_page(size.height - hmin.height - tbh); cache.offset.y = v_scroll->get_value(); + } else { + v_scroll->hide(); + cache.offset.y = 0; } - if (min.width < size.width - vmin.width) { - h_scroll->hide(); - cache.offset.x = 0; - } else { + if (display_hscroll) { h_scroll->show(); - h_scroll->set_max(min.width); + h_scroll->set_max(internal_min_size.width); h_scroll->set_page(size.width - vmin.width); cache.offset.x = h_scroll->get_value(); + } else { + h_scroll->hide(); + cache.offset.x = 0; } } @@ -3411,12 +3635,15 @@ void Tree::_notification(int p_what) { RID ci = get_canvas_item(); Ref<StyleBox> bg = cache.bg; - Color font_outline_color = get_theme_color("font_outline_color"); - int outline_size = get_theme_constant("outline_size"); + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); Point2 draw_ofs; draw_ofs += bg->get_offset(); Size2 draw_size = get_size() - bg->get_minimum_size(); + if (h_scroll->is_visible()) { + draw_size.width -= h_scroll->get_minimum_size().width; + } bg->draw(ci, Rect2(Point2(), get_size())); @@ -3425,7 +3652,9 @@ void Tree::_notification(int p_what) { draw_ofs.y += tbh; draw_size.y -= tbh; - if (root) { + cache.rtl = is_layout_rtl(); + + if (root && get_size().x > 0 && get_size().y > 0) { draw_item(Point2(), draw_ofs, draw_size, root); } @@ -3436,7 +3665,7 @@ void Tree::_notification(int p_what) { Ref<StyleBox> sb = (cache.click_type == Cache::CLICK_TITLE && cache.click_index == i) ? cache.title_button_pressed : ((cache.hover_type == Cache::CLICK_TITLE && cache.hover_index == i) ? cache.title_button_hover : cache.title_button); Ref<Font> f = cache.tb_font; Rect2 tbrect = Rect2(ofs2 - cache.offset.x, bg->get_margin(SIDE_TOP), get_column_width(i), tbh); - if (is_layout_rtl()) { + if (cache.rtl) { tbrect.position.x = get_size().width - tbrect.size.x - tbrect.position.x; } sb->draw(ci, tbrect); @@ -3457,7 +3686,7 @@ void Tree::_notification(int p_what) { // Otherwise, section heading backgrounds can appear to be in front of the focus outline when scrolling. if (has_focus()) { RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, true); - const Ref<StyleBox> bg_focus = get_theme_stylebox("bg_focus"); + const Ref<StyleBox> bg_focus = get_theme_stylebox(SNAME("bg_focus")); bg_focus->draw(ci, Rect2(Point2(), get_size())); RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, false); } @@ -3493,7 +3722,17 @@ void Tree::_update_all() { } Size2 Tree::get_minimum_size() const { - return Size2(1, 1); + if (h_scroll_enabled && v_scroll_enabled) { + return Size2(); + } else { + Vector2 min_size = get_internal_min_size(); + Ref<StyleBox> bg = cache.bg; + if (bg.is_valid()) { + min_size.x += bg->get_margin(SIDE_LEFT) + bg->get_margin(SIDE_RIGHT); + min_size.y += bg->get_margin(SIDE_TOP) + bg->get_margin(SIDE_BOTTOM); + } + return Vector2(h_scroll_enabled ? 0 : min_size.x, v_scroll_enabled ? 0 : min_size.y); + } } TreeItem *Tree::create_item(TreeItem *p_parent, int p_idx) { @@ -3521,11 +3760,11 @@ TreeItem *Tree::create_item(TreeItem *p_parent, int p_idx) { return ti; } -TreeItem *Tree::get_root() { +TreeItem *Tree::get_root() const { return root; } -TreeItem *Tree::get_last_item() { +TreeItem *Tree::get_last_item() const { TreeItem *last = root; while (last) { @@ -3548,9 +3787,9 @@ void Tree::item_edited(int p_column, TreeItem *p_item, bool p_lmb) { edited_item->cells.write[p_column].dirty = true; } if (p_lmb) { - emit_signal("item_edited"); + emit_signal(SNAME("item_edited")); } else { - emit_signal("item_rmb_edited"); + emit_signal(SNAME("item_rmb_edited")); } } @@ -3568,7 +3807,7 @@ void Tree::item_selected(int p_column, TreeItem *p_item) { } p_item->cells.write[p_column].selected = true; - //emit_signal("multi_selected",p_item,p_column,true); - NO this is for TreeItem::select + //emit_signal(SNAME("multi_selected"),p_item,p_column,true); - NO this is for TreeItem::select selected_col = p_column; if (!selected_item) { @@ -3655,13 +3894,13 @@ bool Tree::is_root_hidden() const { return hide_root; } -void Tree::set_column_min_width(int p_column, int p_min_width) { +void Tree::set_column_custom_minimum_width(int p_column, int p_min_width) { ERR_FAIL_INDEX(p_column, columns.size()); - if (p_min_width < 1) { + if (p_min_width < 0) { return; } - columns.write[p_column].min_width = p_min_width; + columns.write[p_column].custom_min_width = p_min_width; update(); } @@ -3672,6 +3911,36 @@ void Tree::set_column_expand(int p_column, bool p_expand) { update(); } +void Tree::set_column_expand_ratio(int p_column, int p_ratio) { + ERR_FAIL_INDEX(p_column, columns.size()); + columns.write[p_column].expand_ratio = p_ratio; + update(); +} + +void Tree::set_column_clip_content(int p_column, bool p_fit) { + ERR_FAIL_INDEX(p_column, columns.size()); + + columns.write[p_column].clip_content = p_fit; + update(); +} + +bool Tree::is_column_expanding(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), false); + + return columns[p_column].expand; +} +int Tree::get_column_expand_ratio(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), 1); + + return columns[p_column].expand_ratio; +} + +bool Tree::is_column_clipping_content(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), false); + + return columns[p_column].clip_content; +} + TreeItem *Tree::get_selected() const { return selected_item; } @@ -3728,44 +3997,81 @@ TreeItem *Tree::get_next_selected(TreeItem *p_item) { return nullptr; } -int Tree::get_column_width(int p_column) const { +int Tree::get_column_minimum_width(int p_column) const { ERR_FAIL_INDEX_V(p_column, columns.size(), -1); - if (!columns[p_column].expand) { - return columns[p_column].min_width; + int min_width = columns[p_column].custom_min_width; + + if (show_column_titles) { + min_width = MAX(cache.font->get_string_size(columns[p_column].title).width, min_width); + } + + if (!columns[p_column].clip_content) { + int depth = 0; + TreeItem *next; + for (TreeItem *item = get_root(); item; item = next) { + next = item->get_next_visible(); + // Compute the depth in tree. + if (next && p_column == 0) { + if (next->get_parent() == item) { + depth += 1; + } else { + TreeItem *common_parent = item->get_parent(); + while (common_parent != next->get_parent()) { + common_parent = common_parent->get_parent(); + depth -= 1; + } + } + } + + // Get the item minimum size. + Size2 item_size = item->get_minimum_size(p_column); + if (p_column == 0) { + item_size.width += cache.item_margin * depth; + } + min_width = MAX(min_width, item_size.width); + } } - int expand_area = get_size().width; + return min_width; +} - Ref<StyleBox> bg = cache.bg; +int Tree::get_column_width(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), -1); - if (bg.is_valid()) { - expand_area -= bg->get_margin(SIDE_LEFT) + bg->get_margin(SIDE_RIGHT); - } + int column_width = get_column_minimum_width(p_column); - if (v_scroll->is_visible_in_tree()) { - expand_area -= v_scroll->get_combined_minimum_size().width; - } + if (columns[p_column].expand) { + int expand_area = get_size().width; - int expanding_columns = 0; - int expanding_total = 0; + Ref<StyleBox> bg = cache.bg; - for (int i = 0; i < columns.size(); i++) { - if (!columns[i].expand) { - expand_area -= columns[i].min_width; - } else { - expanding_total += columns[i].min_width; - expanding_columns++; + if (bg.is_valid()) { + expand_area -= bg->get_margin(SIDE_LEFT) + bg->get_margin(SIDE_RIGHT); } - } - if (expand_area < expanding_total) { - return columns[p_column].min_width; - } + if (v_scroll->is_visible_in_tree()) { + expand_area -= v_scroll->get_combined_minimum_size().width; + } - ERR_FAIL_COND_V(expanding_columns == 0, -1); // shouldn't happen + int expanding_total = 0; + + for (int i = 0; i < columns.size(); i++) { + expand_area -= get_column_minimum_width(i); + if (columns[i].expand) { + expanding_total += columns[i].expand_ratio; + } + } + + if (expand_area >= expanding_total && expanding_total > 0) { + column_width += expand_area * columns[p_column].expand_ratio / expanding_total; + } + } - return expand_area * columns[p_column].min_width / expanding_total; + if (p_column < columns.size() - 1) { + column_width += cache.hseparation; + } + return column_width; } void Tree::propagate_set_columns(TreeItem *p_item) { @@ -3862,7 +4168,7 @@ void Tree::ensure_cursor_is_visible() { if (cell_h > screen_h) { // Screen size is too small, maybe it was not resized yet. v_scroll->set_value(y_offset); } else if (y_offset + cell_h > v_scroll->get_value() + screen_h) { - v_scroll->call_deferred("set_value", y_offset - screen_h + cell_h); + v_scroll->call_deferred(SNAME("set_value"), y_offset - screen_h + cell_h); } else if (y_offset < v_scroll->get_value()) { v_scroll->set_value(y_offset); } @@ -3880,7 +4186,7 @@ void Tree::ensure_cursor_is_visible() { if (cell_w > screen_w) { h_scroll->set_value(x_offset); } else if (x_offset + cell_w > h_scroll->get_value() + screen_w) { - h_scroll->call_deferred("set_value", x_offset - screen_w + cell_w); + h_scroll->call_deferred(SNAME("set_value"), x_offset - screen_w + cell_w); } else if (x_offset < h_scroll->get_value()) { h_scroll->set_value(x_offset); } @@ -4027,6 +4333,24 @@ void Tree::scroll_to_item(TreeItem *p_item) { } } +void Tree::set_h_scroll_enabled(bool p_enable) { + h_scroll_enabled = p_enable; + minimum_size_changed(); +} + +bool Tree::is_h_scroll_enabled() const { + return h_scroll_enabled; +} + +void Tree::set_v_scroll_enabled(bool p_enable) { + v_scroll_enabled = p_enable; + minimum_size_changed(); +} + +bool Tree::is_v_scroll_enabled() const { + return v_scroll_enabled; +} + TreeItem *Tree::_search_item_text(TreeItem *p_at, const String &p_find, int *r_col, bool p_selectable, bool p_backwards) { TreeItem *from = p_at; TreeItem *loop = nullptr; // Safe-guard against infinite loop. @@ -4396,14 +4720,18 @@ bool Tree::get_allow_reselect() const { } void Tree::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &Tree::_gui_input); - ClassDB::bind_method(D_METHOD("clear"), &Tree::clear); ClassDB::bind_method(D_METHOD("create_item", "parent", "idx"), &Tree::_create_item, DEFVAL(Variant()), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_root"), &Tree::get_root); - ClassDB::bind_method(D_METHOD("set_column_min_width", "column", "min_width"), &Tree::set_column_min_width); + ClassDB::bind_method(D_METHOD("set_column_custom_minimum_width", "column", "min_width"), &Tree::set_column_custom_minimum_width); ClassDB::bind_method(D_METHOD("set_column_expand", "column", "expand"), &Tree::set_column_expand); + ClassDB::bind_method(D_METHOD("set_column_expand_ratio", "column", "ratio"), &Tree::set_column_expand_ratio); + ClassDB::bind_method(D_METHOD("set_column_clip_content", "column", "enable"), &Tree::set_column_clip_content); + ClassDB::bind_method(D_METHOD("is_column_expanding", "column"), &Tree::is_column_expanding); + ClassDB::bind_method(D_METHOD("is_column_clipping_content", "column"), &Tree::is_column_clipping_content); + ClassDB::bind_method(D_METHOD("get_column_expand_ratio", "column"), &Tree::get_column_expand_ratio); + ClassDB::bind_method(D_METHOD("get_column_width", "column"), &Tree::get_column_width); ClassDB::bind_method(D_METHOD("set_hide_root", "enable"), &Tree::set_hide_root); @@ -4448,6 +4776,12 @@ void Tree::_bind_methods() { ClassDB::bind_method(D_METHOD("get_scroll"), &Tree::get_scroll); ClassDB::bind_method(D_METHOD("scroll_to_item", "item"), &Tree::_scroll_to_item); + ClassDB::bind_method(D_METHOD("set_h_scroll_enabled", "h_scroll"), &Tree::set_h_scroll_enabled); + ClassDB::bind_method(D_METHOD("is_h_scroll_enabled"), &Tree::is_h_scroll_enabled); + + ClassDB::bind_method(D_METHOD("set_v_scroll_enabled", "h_scroll"), &Tree::set_v_scroll_enabled); + ClassDB::bind_method(D_METHOD("is_v_scroll_enabled"), &Tree::is_v_scroll_enabled); + ClassDB::bind_method(D_METHOD("set_hide_folding", "hide"), &Tree::set_hide_folding); ClassDB::bind_method(D_METHOD("is_folding_hidden"), &Tree::is_folding_hidden); @@ -4467,6 +4801,8 @@ void Tree::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hide_root"), "set_hide_root", "is_root_hidden"); ADD_PROPERTY(PropertyInfo(Variant::INT, "drop_mode_flags", PROPERTY_HINT_FLAGS, "On Item,In Between"), "set_drop_mode_flags", "get_drop_mode_flags"); ADD_PROPERTY(PropertyInfo(Variant::INT, "select_mode", PROPERTY_HINT_ENUM, "Single,Row,Multi"), "set_select_mode", "get_select_mode"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_horizontal_enabled"), "set_h_scroll_enabled", "is_h_scroll_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_vertical_enabled"), "set_v_scroll_enabled", "is_v_scroll_enabled"); ADD_SIGNAL(MethodInfo("item_selected")); ADD_SIGNAL(MethodInfo("cell_selected")); @@ -4502,12 +4838,11 @@ Tree::Tree() { popup_menu = memnew(PopupMenu); popup_menu->hide(); - add_child(popup_menu); - // popup_menu->set_as_top_level(true); + add_child(popup_menu, false, INTERNAL_MODE_FRONT); popup_editor = memnew(Popup); popup_editor->set_wrap_controls(true); - add_child(popup_editor); + add_child(popup_editor, false, INTERNAL_MODE_FRONT); popup_editor_vb = memnew(VBoxContainer); popup_editor->add_child(popup_editor_vb); popup_editor_vb->add_theme_constant_override("separation", 0); @@ -4525,16 +4860,16 @@ Tree::Tree() { h_scroll = memnew(HScrollBar); v_scroll = memnew(VScrollBar); - add_child(h_scroll); - add_child(v_scroll); + add_child(h_scroll, false, INTERNAL_MODE_FRONT); + add_child(v_scroll, false, INTERNAL_MODE_FRONT); range_click_timer = memnew(Timer); range_click_timer->connect("timeout", callable_mp(this, &Tree::_range_click_timeout)); - add_child(range_click_timer); + add_child(range_click_timer, false, INTERNAL_MODE_FRONT); h_scroll->connect("value_changed", callable_mp(this, &Tree::_scroll_moved)); v_scroll->connect("value_changed", callable_mp(this, &Tree::_scroll_moved)); - text_editor->connect("text_entered", callable_mp(this, &Tree::_text_editor_enter)); + text_editor->connect("text_submitted", callable_mp(this, &Tree::_text_editor_submit)); popup_editor->connect("popup_hide", callable_mp(this, &Tree::_text_editor_modal_close)); popup_menu->connect("id_pressed", callable_mp(this, &Tree::popup_select)); value_editor->connect("value_changed", callable_mp(this, &Tree::value_editor_changed)); diff --git a/scene/gui/tree.h b/scene/gui/tree.h index c0948f1b80..85fed941dc 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -82,6 +82,7 @@ private: int icon_max_w = 0; bool expr = false; bool checked = false; + bool indeterminate = false; bool editable = false; bool selected = false; bool selectable = true; @@ -112,8 +113,11 @@ private: Vector<Button> buttons; + Ref<Font> custom_font; + int custom_font_size = -1; + Cell() { - text_buf.instance(); + text_buf.instantiate(); } Size2 get_icon_size() const; @@ -126,6 +130,9 @@ private: bool disable_folding = false; int custom_min_height = 0; + Size2i cached_minimum_size; + bool cached_minimum_size_dirty = true; + TreeItem *parent = nullptr; // parent item TreeItem *prev = nullptr; // previous in list TreeItem *next = nullptr; // next in list @@ -207,7 +214,9 @@ public: /* check mode */ void set_checked(int p_column, bool p_checked); + void set_indeterminate(int p_column, bool p_indeterminate); bool is_checked(int p_column) const; + bool is_indeterminate(int p_column) const; void set_text(int p_column, String p_text); String get_text(int p_column) const; @@ -291,6 +300,12 @@ public: Color get_custom_color(int p_column) const; void clear_custom_color(int p_column); + void set_custom_font(int p_column, const Ref<Font> &p_font); + Ref<Font> get_custom_font(int p_column) const; + + void set_custom_font_size(int p_column, int p_font_size); + int get_custom_font_size(int p_column) const; + void set_custom_bg_color(int p_column, const Color &p_color, bool p_bg_outline = false); void clear_custom_bg_color(int p_column); Color get_custom_bg_color(int p_column) const; @@ -310,16 +325,18 @@ public: void set_disable_folding(bool p_disable); bool is_folding_disabled() const; + Size2 get_minimum_size(int p_column); + /* Item manipulation */ TreeItem *create_child(int p_idx = -1); - Tree *get_tree(); + Tree *get_tree() const; TreeItem *get_prev(); - TreeItem *get_next(); - TreeItem *get_parent(); - TreeItem *get_first_child(); + TreeItem *get_next() const; + TreeItem *get_parent() const; + TreeItem *get_first_child() const; TreeItem *get_prev_visible(bool p_wrap = false); TreeItem *get_next_visible(bool p_wrap = false); @@ -403,15 +420,17 @@ private: int drop_mode_flags = 0; struct ColumnInfo { - int min_width = 1; + int custom_min_width = 0; + int expand_ratio = 1; bool expand = true; + bool clip_content = false; String title; Ref<TextLine> text_buf; Dictionary opentype_features; String language; Control::TextDirection text_direction = Control::TEXT_DIRECTION_INHERITED; ColumnInfo() { - text_buf.instance(); + text_buf.instantiate(); } }; @@ -443,18 +462,15 @@ private: void draw_item_rect(TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color, int p_ol_size, const Color &p_ol_color); int draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 &p_draw_size, TreeItem *p_item); void select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_col, TreeItem *p_prev = nullptr, bool *r_in_range = nullptr, bool p_force_deselect = false); - int propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool p_double_click, TreeItem *p_item, int p_button, const Ref<InputEventWithModifiers> &p_mod); - void _text_editor_enter(String p_text); + int propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int x_limit, bool p_double_click, TreeItem *p_item, int p_button, const Ref<InputEventWithModifiers> &p_mod); + void _text_editor_submit(String p_text); void _text_editor_modal_close(); void value_editor_changed(double p_value); void popup_select(int p_option); - void _gui_input(Ref<InputEvent> p_event); void _notification(int p_what); - Size2 get_minimum_size() const override; - void item_edited(int p_column, TreeItem *p_item, bool p_lmb = true); void item_changed(int p_column, TreeItem *p_item); void item_selected(int p_column, TreeItem *p_item); @@ -484,6 +500,7 @@ private: Ref<Texture2D> checked; Ref<Texture2D> unchecked; + Ref<Texture2D> indeterminate; Ref<Texture2D> arrow_collapsed; Ref<Texture2D> arrow; Ref<Texture2D> select_arrow; @@ -497,6 +514,7 @@ private: Color parent_hl_line_color; Color children_hl_line_color; Color custom_button_font_highlight; + Color font_outline_color; int hseparation = 0; int vseparation = 0; @@ -511,6 +529,7 @@ private: int draw_guides = 0; int scroll_border = 0; int scroll_speed = 0; + int font_outline_size = 0; enum ClickType { CLICK_NONE, @@ -533,6 +552,8 @@ private: Point2i text_editor_position; + bool rtl = false; + } cache; int _get_title_button_height() const; @@ -541,6 +562,9 @@ private: HScrollBar *h_scroll; VScrollBar *v_scroll; + bool h_scroll_enabled = true; + bool v_scroll_enabled = true; + Size2 get_internal_min_size() const; void update_cache(); void update_scrollbars(); @@ -608,6 +632,8 @@ protected: } public: + virtual void gui_input(const Ref<InputEvent> &p_event) override; + virtual String get_tooltip(const Point2 &p_pos) const override; TreeItem *get_item_at_position(const Point2 &p_pos) const; @@ -618,12 +644,19 @@ public: void clear(); TreeItem *create_item(TreeItem *p_parent = nullptr, int p_idx = -1); - TreeItem *get_root(); - TreeItem *get_last_item(); + TreeItem *get_root() const; + TreeItem *get_last_item() const; - void set_column_min_width(int p_column, int p_min_width); + void set_column_custom_minimum_width(int p_column, int p_min_width); void set_column_expand(int p_column, bool p_expand); + void set_column_expand_ratio(int p_column, int p_ratio); + void set_column_clip_content(int p_column, bool p_fit); + int get_column_minimum_width(int p_column) const; int get_column_width(int p_column) const; + int get_column_expand_ratio(int p_column) const; + + bool is_column_expanding(int p_column) const; + bool is_column_clipping_content(int p_column) const; void set_hide_root(bool p_enabled); bool is_root_hidden() const; @@ -674,6 +707,10 @@ public: Point2 get_scroll() const; void scroll_to_item(TreeItem *p_item); + void set_h_scroll_enabled(bool p_enable); + bool is_h_scroll_enabled() const; + void set_v_scroll_enabled(bool p_enable); + bool is_v_scroll_enabled() const; void set_cursor_can_exit_tree(bool p_enable); @@ -694,6 +731,8 @@ public: void set_allow_reselect(bool p_allow); bool get_allow_reselect() const; + Size2 get_minimum_size() const override; + Tree(); ~Tree(); }; diff --git a/scene/gui/video_player.cpp b/scene/gui/video_player.cpp index 0590ae2415..8734037a57 100644 --- a/scene/gui/video_player.cpp +++ b/scene/gui/video_player.cpp @@ -129,7 +129,7 @@ void VideoPlayer::_mix_audio() { void VideoPlayer::_notification(int p_notification) { switch (p_notification) { case NOTIFICATION_ENTER_TREE: { - AudioServer::get_singleton()->add_callback(_mix_audios, this); + AudioServer::get_singleton()->add_mix_callback(_mix_audios, this); if (stream.is_valid() && autoplay && !Engine::get_singleton()->is_editor_hint()) { play(); @@ -138,8 +138,7 @@ void VideoPlayer::_notification(int p_notification) { } break; case NOTIFICATION_EXIT_TREE: { - AudioServer::get_singleton()->remove_callback(_mix_audios, this); - + AudioServer::get_singleton()->remove_mix_callback(_mix_audios, this); } break; case NOTIFICATION_INTERNAL_PROCESS: { @@ -452,12 +451,12 @@ void VideoPlayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "stream", PROPERTY_HINT_RESOURCE_TYPE, "VideoStream"), "set_stream", "get_stream"); //ADD_PROPERTY( PropertyInfo(Variant::BOOL, "stream/loop"), "set_loop", "has_loop") ; ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume_db", PROPERTY_HINT_RANGE, "-80,24,0.01"), "set_volume_db", "get_volume_db"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume", PROPERTY_HINT_EXP_RANGE, "0,15,0.01", 0), "set_volume", "get_volume"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "volume", PROPERTY_HINT_RANGE, "0,15,0.01,exp", PROPERTY_USAGE_NONE), "set_volume", "get_volume"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autoplay"), "set_autoplay", "has_autoplay"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "paused"), "set_paused", "is_paused"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand"), "set_expand", "has_expand"); ADD_PROPERTY(PropertyInfo(Variant::INT, "buffering_msec", PROPERTY_HINT_RANGE, "10,1000"), "set_buffering_msec", "get_buffering_msec"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "stream_position", PROPERTY_HINT_RANGE, "0,1280000,0.1", 0), "set_stream_position", "get_stream_position"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "stream_position", PROPERTY_HINT_RANGE, "0,1280000,0.1", PROPERTY_USAGE_NONE), "set_stream_position", "get_stream_position"); ADD_PROPERTY(PropertyInfo(Variant::STRING_NAME, "bus", PROPERTY_HINT_ENUM, ""), "set_bus", "get_bus"); } |