diff options
Diffstat (limited to 'scene/gui')
101 files changed, 21491 insertions, 12549 deletions
diff --git a/scene/gui/aspect_ratio_container.cpp b/scene/gui/aspect_ratio_container.cpp new file mode 100644 index 0000000000..fb6fa9dec9 --- /dev/null +++ b/scene/gui/aspect_ratio_container.cpp @@ -0,0 +1,172 @@ +/*************************************************************************/ +/* aspect_ratio_container.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 "aspect_ratio_container.h" + +Size2 AspectRatioContainer::get_minimum_size() const { + Size2 ms; + for (int i = 0; i < get_child_count(); i++) { + Control *c = Object::cast_to<Control>(get_child(i)); + if (!c) { + continue; + } + if (c->is_set_as_top_level()) { + continue; + } + if (!c->is_visible()) { + continue; + } + Size2 minsize = c->get_combined_minimum_size(); + ms.width = MAX(ms.width, minsize.width); + ms.height = MAX(ms.height, minsize.height); + } + return ms; +} + +void AspectRatioContainer::set_ratio(float p_ratio) { + ratio = p_ratio; + queue_sort(); +} + +void AspectRatioContainer::set_stretch_mode(StretchMode p_mode) { + stretch_mode = p_mode; + queue_sort(); +} + +void AspectRatioContainer::set_alignment_horizontal(AlignMode p_alignment_horizontal) { + alignment_horizontal = p_alignment_horizontal; + queue_sort(); +} + +void AspectRatioContainer::set_alignment_vertical(AlignMode p_alignment_vertical) { + alignment_vertical = p_alignment_vertical; + queue_sort(); +} + +void AspectRatioContainer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_SORT_CHILDREN: { + bool rtl = is_layout_rtl(); + Size2 size = get_size(); + for (int i = 0; i < get_child_count(); i++) { + Control *c = Object::cast_to<Control>(get_child(i)); + if (!c) { + continue; + } + if (c->is_set_as_top_level()) { + continue; + } + Size2 child_minsize = c->get_combined_minimum_size(); + Size2 child_size = Size2(ratio, 1.0); + float scale_factor = 1.0; + + switch (stretch_mode) { + case STRETCH_WIDTH_CONTROLS_HEIGHT: { + scale_factor = size.x / child_size.x; + } break; + case STRETCH_HEIGHT_CONTROLS_WIDTH: { + scale_factor = size.y / child_size.y; + } break; + case STRETCH_FIT: { + scale_factor = MIN(size.x / child_size.x, size.y / child_size.y); + } break; + case STRETCH_COVER: { + scale_factor = MAX(size.x / child_size.x, size.y / child_size.y); + } break; + } + child_size *= scale_factor; + child_size.x = MAX(child_size.x, child_minsize.x); + child_size.y = MAX(child_size.y, child_minsize.y); + + float align_x = 0.5; + switch (alignment_horizontal) { + case ALIGN_BEGIN: { + align_x = 0.0; + } break; + case ALIGN_CENTER: { + align_x = 0.5; + } break; + case ALIGN_END: { + align_x = 1.0; + } break; + } + float align_y = 0.5; + switch (alignment_vertical) { + case ALIGN_BEGIN: { + align_y = 0.0; + } break; + case ALIGN_CENTER: { + align_y = 0.5; + } break; + case ALIGN_END: { + align_y = 1.0; + } break; + } + Vector2 offset = (size - child_size) * Vector2(align_x, align_y); + + if (rtl) { + fit_child_in_rect(c, Rect2(Vector2(size.x - offset.x - child_size.x, offset.y), child_size)); + } else { + fit_child_in_rect(c, Rect2(offset, child_size)); + } + } + } break; + } +} + +void AspectRatioContainer::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_ratio", "ratio"), &AspectRatioContainer::set_ratio); + ClassDB::bind_method(D_METHOD("get_ratio"), &AspectRatioContainer::get_ratio); + + ClassDB::bind_method(D_METHOD("set_stretch_mode", "stretch_mode"), &AspectRatioContainer::set_stretch_mode); + ClassDB::bind_method(D_METHOD("get_stretch_mode"), &AspectRatioContainer::get_stretch_mode); + + ClassDB::bind_method(D_METHOD("set_alignment_horizontal", "alignment_horizontal"), &AspectRatioContainer::set_alignment_horizontal); + ClassDB::bind_method(D_METHOD("get_alignment_horizontal"), &AspectRatioContainer::get_alignment_horizontal); + + ClassDB::bind_method(D_METHOD("set_alignment_vertical", "alignment_vertical"), &AspectRatioContainer::set_alignment_vertical); + ClassDB::bind_method(D_METHOD("get_alignment_vertical"), &AspectRatioContainer::get_alignment_vertical); + + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ratio"), "set_ratio", "get_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "stretch_mode", PROPERTY_HINT_ENUM, "Width Controls Height,Height Controls Width,Fit,Cover"), "set_stretch_mode", "get_stretch_mode"); + + ADD_GROUP("Alignment", "alignment_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "alignment_horizontal", PROPERTY_HINT_ENUM, "Begin,Center,End"), "set_alignment_horizontal", "get_alignment_horizontal"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "alignment_vertical", PROPERTY_HINT_ENUM, "Begin,Center,End"), "set_alignment_vertical", "get_alignment_vertical"); + + BIND_ENUM_CONSTANT(STRETCH_WIDTH_CONTROLS_HEIGHT); + BIND_ENUM_CONSTANT(STRETCH_HEIGHT_CONTROLS_WIDTH); + BIND_ENUM_CONSTANT(STRETCH_FIT); + BIND_ENUM_CONSTANT(STRETCH_COVER); + + BIND_ENUM_CONSTANT(ALIGN_BEGIN); + BIND_ENUM_CONSTANT(ALIGN_CENTER); + BIND_ENUM_CONSTANT(ALIGN_END); +} diff --git a/scene/gui/shortcut.h b/scene/gui/aspect_ratio_container.h index 176958b397..c95c6a7274 100644 --- a/scene/gui/shortcut.h +++ b/scene/gui/aspect_ratio_container.h @@ -1,12 +1,12 @@ /*************************************************************************/ -/* shortcut.h */ +/* aspect_ratio_container.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -28,29 +28,53 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef SHORTCUT_H -#define SHORTCUT_H +#ifndef ASPECT_RATIO_CONTAINER_H +#define ASPECT_RATIO_CONTAINER_H -#include "core/input/input_event.h" -#include "core/io/resource.h" +#include "scene/gui/container.h" -class Shortcut : public Resource { - GDCLASS(Shortcut, Resource); - - Ref<InputEvent> shortcut; +class AspectRatioContainer : public Container { + GDCLASS(AspectRatioContainer, Container); protected: + void _notification(int p_what); static void _bind_methods(); + virtual Size2 get_minimum_size() const override; + +public: + enum StretchMode { + STRETCH_WIDTH_CONTROLS_HEIGHT, + STRETCH_HEIGHT_CONTROLS_WIDTH, + STRETCH_FIT, + STRETCH_COVER, + }; + enum AlignMode { + ALIGN_BEGIN, + ALIGN_CENTER, + ALIGN_END, + }; + +private: + float ratio = 1.0; + StretchMode stretch_mode = STRETCH_FIT; + AlignMode alignment_horizontal = ALIGN_CENTER; + AlignMode alignment_vertical = ALIGN_CENTER; 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; + void set_ratio(float p_ratio); + float get_ratio() const { return ratio; } + + void set_stretch_mode(StretchMode p_mode); + StretchMode get_stretch_mode() const { return stretch_mode; } - String get_as_text() const; + void set_alignment_horizontal(AlignMode p_alignment_horizontal); + AlignMode get_alignment_horizontal() const { return alignment_horizontal; } - Shortcut(); + void set_alignment_vertical(AlignMode p_alignment_vertical); + AlignMode get_alignment_vertical() const { return alignment_vertical; } }; -#endif // SHORTCUT_H +VARIANT_ENUM_CAST(AspectRatioContainer::StretchMode); +VARIANT_ENUM_CAST(AspectRatioContainer::AlignMode); + +#endif // ASPECT_RATIO_CONTAINER_H diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index 1a19c75d27..bef1011364 100644 --- a/scene/gui/base_button.cpp +++ b/scene/gui/base_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -52,7 +52,9 @@ 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 return; } @@ -96,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(); } } @@ -122,37 +121,40 @@ 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; } status.pressed = !status.pressed; _unpress_group(); + if (button_group.is_valid()) { + button_group->emit_signal(SNAME("pressed"), this); + } _toggled(status.pressed); _pressed(); } @@ -170,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(); @@ -199,7 +200,6 @@ void BaseButton::set_disabled(bool p_disabled) { status.pressing_inside = false; } update(); - _change_notify("disabled"); } bool BaseButton::is_disabled() const { @@ -213,17 +213,31 @@ void BaseButton::set_pressed(bool p_pressed) { if (status.pressed == p_pressed) { return; } - _change_notify("pressed"); status.pressed = p_pressed; if (p_pressed) { _unpress_group(); + if (button_group.is_valid()) { + button_group->emit_signal(SNAME("pressed"), this); + } } _toggled(status.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; } @@ -317,22 +331,29 @@ bool BaseButton::is_keep_pressed_outside() const { void BaseButton::set_shortcut(const Ref<Shortcut> &p_shortcut) { shortcut = p_shortcut; - set_process_unhandled_input(shortcut.is_valid()); + set_process_unhandled_key_input(shortcut.is_valid()); } Ref<Shortcut> BaseButton::get_shortcut() const { return shortcut; } -void BaseButton::_unhandled_input(Ref<InputEvent> p_event) { - if (!is_disabled() && is_visible_in_tree() && !p_event->is_echo() && shortcut.is_valid() && shortcut->is_shortcut(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->matches_event(p_event)) { on_action_event(p_event); + accept_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; @@ -360,11 +381,35 @@ Ref<ButtonGroup> BaseButton::get_button_group() const { return button_group; } +void BaseButton::set_shortcut_context(Node *p_node) { + ERR_FAIL_NULL_MSG(p_node, "Shortcut context node can't be null."); + shortcut_context = p_node->get_instance_id(); +} + +Node *BaseButton::get_shortcut_context() const { + Object *ctx_obj = ObjectDB::get_instance(shortcut_context); + Node *ctx_node = Object::cast_to<Node>(ctx_obj); + + return ctx_node; +} + +bool BaseButton::_is_focus_owner_in_shorcut_context() const { + if (shortcut_context == ObjectID()) { + // No context, therefore global - always "in" context. + return true; + } + + Node *ctx_node = get_shortcut_context(); + 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_ancestor_of(vp_focus)); +} + void BaseButton::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &BaseButton::_gui_input); - ClassDB::bind_method(D_METHOD("_unhandled_input"), &BaseButton::_unhandled_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); @@ -386,8 +431,11 @@ void BaseButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_button_group", "button_group"), &BaseButton::set_button_group); ClassDB::bind_method(D_METHOD("get_button_group"), &BaseButton::get_button_group); - BIND_VMETHOD(MethodInfo("_pressed")); - BIND_VMETHOD(MethodInfo("_toggled", PropertyInfo(Variant::BOOL, "button_pressed"))); + 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); + + GDVIRTUAL_BIND(_pressed); + GDVIRTUAL_BIND(_toggled, "button_pressed"); ADD_SIGNAL(MethodInfo("pressed")); ADD_SIGNAL(MethodInfo("button_up")); @@ -402,6 +450,7 @@ void BaseButton::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "keep_pressed_outside"), "set_keep_pressed_outside", "is_keep_pressed_outside"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shortcut", PROPERTY_HINT_RESOURCE_TYPE, "Shortcut"), "set_shortcut", "get_shortcut"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "button_group", PROPERTY_HINT_RESOURCE_TYPE, "ButtonGroup"), "set_button_group", "get_button_group"); + ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "shortcut_context", PROPERTY_HINT_RESOURCE_TYPE, "Node"), "set_shortcut_context", "get_shortcut_context"); BIND_ENUM_CONSTANT(DRAW_NORMAL); BIND_ENUM_CONSTANT(DRAW_PRESSED); @@ -414,17 +463,7 @@ void BaseButton::_bind_methods() { } BaseButton::BaseButton() { - toggle_mode = false; - shortcut_in_tooltip = true; - keep_pressed_outside = false; - status.pressed = false; - status.press_attempt = false; - status.hovering = false; - status.pressing_inside = false; - status.disabled = false; set_focus_mode(FOCUS_ALL); - action_mode = ACTION_MODE_BUTTON_RELEASE; - button_mask = BUTTON_MASK_LEFT; } BaseButton::~BaseButton() { @@ -461,6 +500,7 @@ BaseButton *ButtonGroup::get_pressed_button() { void ButtonGroup::_bind_methods() { ClassDB::bind_method(D_METHOD("get_pressed_button"), &ButtonGroup::get_pressed_button); ClassDB::bind_method(D_METHOD("get_buttons"), &ButtonGroup::_get_buttons); + ADD_SIGNAL(MethodInfo("pressed", PropertyInfo(Variant::OBJECT, "button"))); } ButtonGroup::ButtonGroup() { diff --git a/scene/gui/base_button.h b/scene/gui/base_button.h index 33f19949cd..cd6e78464f 100644 --- a/scene/gui/base_button.h +++ b/scene/gui/base_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -45,20 +45,21 @@ public: }; private: - int button_mask; - bool toggle_mode; - bool shortcut_in_tooltip; - bool keep_pressed_outside; + int button_mask = MOUSE_BUTTON_MASK_LEFT; + bool toggle_mode = false; + bool shortcut_in_tooltip = true; + bool keep_pressed_outside = false; Ref<Shortcut> shortcut; + ObjectID shortcut_context; - ActionMode action_mode; + ActionMode action_mode = ACTION_MODE_BUTTON_RELEASE; struct Status { - bool pressed; - bool hovering; - bool press_attempt; - bool pressing_inside; + bool pressed = false; + bool hovering = false; + bool press_attempt = false; + bool pressing_inside = false; - bool disabled; + bool disabled = false; } status; @@ -74,10 +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_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, @@ -95,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; @@ -122,6 +129,9 @@ public: void set_button_group(const Ref<ButtonGroup> &p_group); Ref<ButtonGroup> get_button_group() const; + void set_shortcut_context(Node *p_node); + Node *get_shortcut_context() const; + BaseButton(); ~BaseButton(); }; diff --git a/scene/gui/box_container.cpp b/scene/gui/box_container.cpp index 33e030a573..a2f1d2b15a 100644 --- a/scene/gui/box_container.cpp +++ b/scene/gui/box_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -33,9 +33,9 @@ #include "margin_container.h" struct _MinSizeCache { - int min_size; - bool will_stretch; - int final_size; + int min_size = 0; + bool will_stretch = false; + int final_size = 0; }; void BoxContainer::_resort() { @@ -43,13 +43,14 @@ 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; int children_count = 0; int stretch_min = 0; int stretch_avail = 0; - float stretch_ratio_total = 0; + float stretch_ratio_total = 0.0; Map<Control *, _MinSizeCache> min_size_cache; for (int i = 0; i < get_child_count(); i++) { @@ -104,7 +105,7 @@ void BoxContainer::_resort() { has_stretched = true; bool refit_successful = true; //assume refit-test will go well - float error = 0; // Keep track of accumulated error in pixels + float error = 0.0; // Keep track of accumulated error in pixels for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); @@ -152,22 +153,53 @@ void BoxContainer::_resort() { int ofs = 0; if (!has_stretched) { - switch (align) { - case ALIGN_BEGIN: - break; - case ALIGN_CENTER: - ofs = stretch_diff / 2; - break; - case ALIGN_END: - ofs = stretch_diff; - break; + if (!vertical) { + switch (align) { + case ALIGN_BEGIN: + if (rtl) { + ofs = stretch_diff; + } + break; + case ALIGN_CENTER: + ofs = stretch_diff / 2; + break; + case ALIGN_END: + if (!rtl) { + ofs = stretch_diff; + } + break; + } + } else { + switch (align) { + case ALIGN_BEGIN: + break; + case ALIGN_CENTER: + ofs = stretch_diff / 2; + break; + case ALIGN_END: + ofs = stretch_diff; + break; + } } } first = true; int idx = 0; - for (int i = 0; i < get_child_count(); i++) { + int start; + int end; + int delta; + if (!rtl || vertical) { + start = 0; + end = get_child_count(); + delta = +1; + } else { + start = get_child_count() - 1; + end = -1; + delta = -1; + } + + for (int i = start; i != end; i += delta) { Control *c = Object::cast_to<Control>(get_child(i)); if (!c || !c->is_visible_in_tree()) { continue; @@ -215,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; @@ -265,6 +297,10 @@ void BoxContainer::_notification(int p_what) { case NOTIFICATION_THEME_CHANGED: { minimum_size_changed(); } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + queue_sort(); + } break; } } @@ -277,7 +313,7 @@ BoxContainer::AlignMode BoxContainer::get_alignment() const { return align; } -void BoxContainer::add_spacer(bool p_begin) { +Control *BoxContainer::add_spacer(bool p_begin) { Control *c = memnew(Control); c->set_mouse_filter(MOUSE_FILTER_PASS); //allow spacer to pass mouse events @@ -291,11 +327,12 @@ void BoxContainer::add_spacer(bool p_begin) { if (p_begin) { move_child(c, 0); } + + return c; } BoxContainer::BoxContainer(bool p_vertical) { vertical = p_vertical; - align = ALIGN_BEGIN; } void BoxContainer::_bind_methods() { @@ -312,6 +349,7 @@ 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); MarginContainer *mc = memnew(MarginContainer); diff --git a/scene/gui/box_container.h b/scene/gui/box_container.h index c4d75c3cf1..23feea565c 100644 --- a/scene/gui/box_container.h +++ b/scene/gui/box_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -44,8 +44,8 @@ public: }; private: - bool vertical; - AlignMode align; + bool vertical = false; + AlignMode align = ALIGN_BEGIN; void _resort(); @@ -55,7 +55,7 @@ protected: static void _bind_methods(); public: - void add_spacer(bool p_begin = false); + Control *add_spacer(bool p_begin = false); void set_alignment(AlignMode p_align); AlignMode get_alignment() const; diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp index e86ad09aa6..9cdf3bf210 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -34,39 +34,60 @@ #include "servers/rendering_server.h" Size2 Button::get_minimum_size() const { - Size2 minsize = get_theme_font("font")->get_string_size(xl_text); + Size2 minsize = text_buf->get_size(); if (clip_text) { minsize.width = 0; } 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()); } } } - return get_theme_stylebox("normal")->get_minimum_size() + minsize; + 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(SNAME("normal"))->get_minimum_size() + minsize; } -void Button::_set_internal_margin(Margin p_margin, float p_value) { - _internal_margin[p_margin] = p_value; +void Button::_set_internal_margin(Side p_side, float p_value) { + _internal_margin[p_side] = p_value; } void Button::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + update(); + } break; case NOTIFICATION_TRANSLATION_CHANGED: { - xl_text = tr(text); + xl_text = atr(text); + _shape(); + + minimum_size_changed(); + update(); + } break; + case NOTIFICATION_THEME_CHANGED: { + _shape(); + minimum_size_changed(); update(); } break; @@ -76,32 +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: { - style = get_theme_stylebox("normal"); + if (rtl && has_theme_stylebox(SNAME("normal_mirrored"))) { + style = get_theme_stylebox(SNAME("normal_mirrored")); + } else { + 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_color_normal")) { - color_icon = get_theme_color("icon_color_normal"); + 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")) { - style = get_theme_stylebox("hover_pressed"); + 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(SNAME("hover_pressed")); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - if (has_theme_color("font_color_hover_pressed")) { - color = get_theme_color("font_color_hover_pressed"); + 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_color_hover_pressed")) { - color_icon = get_theme_color("icon_color_hover_pressed"); + if (has_theme_color(SNAME("icon_hover_pressed_color"))) { + color_icon = get_theme_color(SNAME("icon_hover_pressed_color")); } break; @@ -109,74 +141,116 @@ void Button::_notification(int p_what) { [[fallthrough]]; } case DRAW_PRESSED: { - style = get_theme_stylebox("pressed"); + if (rtl && has_theme_stylebox(SNAME("pressed_mirrored"))) { + style = get_theme_stylebox(SNAME("pressed_mirrored")); + } else { + style = get_theme_stylebox(SNAME("pressed")); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - if (has_theme_color("font_color_pressed")) { - color = get_theme_color("font_color_pressed"); + 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_color_pressed")) { - color_icon = get_theme_color("icon_color_pressed"); + if (has_theme_color(SNAME("icon_pressed_color"))) { + color_icon = get_theme_color(SNAME("icon_pressed_color")); } } break; case DRAW_HOVER: { - style = get_theme_stylebox("hover"); + if (rtl && has_theme_stylebox(SNAME("hover_mirrored"))) { + style = get_theme_stylebox(SNAME("hover_mirrored")); + } else { + style = get_theme_stylebox(SNAME("hover")); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - color = get_theme_color("font_color_hover"); - if (has_theme_color("icon_color_hover")) { - color_icon = get_theme_color("icon_color_hover"); + 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: { - style = get_theme_stylebox("disabled"); + if (rtl && has_theme_stylebox(SNAME("disabled_mirrored"))) { + style = get_theme_stylebox(SNAME("disabled_mirrored")); + } else { + style = get_theme_stylebox(SNAME("disabled")); + } + if (!flat) { style->draw(ci, Rect2(Point2(0, 0), size)); } - color = get_theme_color("font_color_disabled"); - if (has_theme_color("icon_color_disabled")) { - color_icon = get_theme_color("icon_color_disabled"); + 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<Font> font = get_theme_font("font"); 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()) { color_icon.a = 0.4; } - float icon_ofs_region = 0; - if (_internal_margin[MARGIN_LEFT] > 0) { - icon_ofs_region = _internal_margin[MARGIN_LEFT] + get_theme_constant("hseparation"); + float icon_ofs_region = 0.0; + 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(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_font("font")->get_string_size(xl_text).width; + _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(); float icon_height = _size.height; @@ -186,29 +260,49 @@ void Button::_notification(int p_what) { icon_height = _icon->get_height() * icon_width / _icon->get_width(); } - 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 { - 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; - if (_internal_margin[MARGIN_LEFT] > 0) { - text_clip -= _internal_margin[MARGIN_LEFT] + get_theme_constant("hseparation"); + 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(SNAME("hseparation")); } - if (_internal_margin[MARGIN_RIGHT] > 0) { - text_clip -= _internal_margin[MARGIN_RIGHT] + get_theme_constant("hseparation"); + if (_internal_margin[SIDE_RIGHT] > 0) { + text_clip -= _internal_margin[SIDE_RIGHT] + get_theme_constant(SNAME("hseparation")); } - Point2 text_ofs = (size - style->get_minimum_size() - icon_ofs - font->get_string_size(xl_text) - Point2(_internal_margin[MARGIN_RIGHT] - _internal_margin[MARGIN_LEFT], 0)) / 2.0; + 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 (_internal_margin[MARGIN_LEFT] > 0) { - text_ofs.x = style->get_margin(MARGIN_LEFT) + icon_ofs.x + _internal_margin[MARGIN_LEFT] + get_theme_constant("hseparation"); + 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 { - text_ofs.x = style->get_margin(MARGIN_LEFT) + icon_ofs.x; + text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x; } text_ofs.y += style->get_offset().y; } break; @@ -216,52 +310,117 @@ 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 (_internal_margin[MARGIN_RIGHT] > 0) { - text_ofs.x = size.x - style->get_margin(MARGIN_RIGHT) - font->get_string_size(xl_text).x - _internal_margin[MARGIN_RIGHT] - get_theme_constant("hseparation"); + 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 { - text_ofs.x = size.x - style->get_margin(MARGIN_RIGHT) - font->get_string_size(xl_text).x; + 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; } - text_ofs.y += font->get_ascent(); - font->draw(ci, text_ofs.floor(), xl_text, color, clip_text ? text_clip : -1); - - if (!_icon.is_null() && icon_region.size.width > 0) { - draw_texture_rect_region(_icon, icon_region, Rect2(Point2(), _icon->get_size()), color_icon); + 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); } break; } } +void Button::_shape() { + 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) { + text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + text_buf->set_direction((TextServer::Direction)text_direction); + } + text_buf->add_string(xl_text, font, font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); +} + void Button::set_text(const String &p_text) { - if (text == p_text) { - return; + if (text != p_text) { + text = p_text; + xl_text = atr(text); + _shape(); + + update(); + minimum_size_changed(); } - text = p_text; - xl_text = tr(p_text); - update(); - _change_notify("text"); - minimum_size_changed(); } String Button::get_text() const { return text; } -void Button::set_icon(const Ref<Texture2D> &p_icon) { - if (icon == p_icon) { - return; +void Button::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; + _shape(); + update(); } - icon = p_icon; +} + +Control::TextDirection Button::get_text_direction() const { + return text_direction; +} + +void Button::clear_opentype_features() { + opentype_features.clear(); + _shape(); update(); - _change_notify("icon"); - minimum_size_changed(); +} + +void Button::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; + _shape(); + update(); + } +} + +int Button::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]; +} + +void Button::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + _shape(); + update(); + } +} + +String Button::get_language() const { + return language; +} + +void Button::set_icon(const Ref<Texture2D> &p_icon) { + if (icon != p_icon) { + icon = p_icon; + update(); + minimum_size_changed(); + } } Ref<Texture2D> Button::get_icon() const { @@ -269,9 +428,11 @@ Ref<Texture2D> Button::get_icon() const { } void Button::set_expand_icon(bool p_expand_icon) { - expand_icon = p_expand_icon; - update(); - minimum_size_changed(); + if (expand_icon != p_expand_icon) { + expand_icon = p_expand_icon; + update(); + minimum_size_changed(); + } } bool Button::is_expand_icon() const { @@ -279,9 +440,10 @@ bool Button::is_expand_icon() const { } void Button::set_flat(bool p_flat) { - flat = p_flat; - update(); - _change_notify("flat"); + if (flat != p_flat) { + flat = p_flat; + update(); + } } bool Button::is_flat() const { @@ -289,9 +451,11 @@ bool Button::is_flat() const { } void Button::set_clip_text(bool p_clip_text) { - clip_text = p_clip_text; - update(); - minimum_size_changed(); + if (clip_text != p_clip_text) { + clip_text = p_clip_text; + update(); + minimum_size_changed(); + } } bool Button::get_clip_text() const { @@ -299,51 +463,120 @@ bool Button::get_clip_text() const { } void Button::set_text_align(TextAlign p_align) { - align = p_align; - update(); + if (align != p_align) { + align = p_align; + update(); + } } 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/")) { + 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); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + notify_property_list_changed(); + return true; + } + + return false; +} + +bool Button::_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 Button::_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 Button::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text", "text"), &Button::set_text); ClassDB::bind_method(D_METHOD("get_text"), &Button::get_text); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &Button::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &Button::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &Button::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &Button::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &Button::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &Button::set_language); + 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); BIND_ENUM_CONSTANT(ALIGN_RIGHT); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text"); + 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::OBJECT, "icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_button_icon", "get_button_icon"); 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) { - flat = false; - clip_text = false; - expand_icon = false; + text_buf.instantiate(); + text_buf->set_flags(TextServer::BREAK_MANDATORY); + set_mouse_filter(MOUSE_FILTER_STOP); set_text(p_text); - align = ALIGN_CENTER; - - for (int i = 0; i < 4; i++) { - _internal_margin[i] = 0; - } } Button::~Button() { diff --git a/scene/gui/button.h b/scene/gui/button.h index 5b44b1322e..253d079680 100644 --- a/scene/gui/button.h +++ b/scene/gui/button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -32,6 +32,7 @@ #define BUTTON_H #include "scene/gui/base_button.h" +#include "scene/resources/text_paragraph.h" class Button : public BaseButton { GDCLASS(Button, BaseButton); @@ -44,26 +45,49 @@ public: }; private: - bool flat; + bool flat = false; String text; String xl_text; + Ref<TextParagraph> text_buf; + + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Ref<Texture2D> icon; - bool expand_icon; - bool clip_text; - TextAlign align; - float _internal_margin[4]; + bool expand_icon = false; + bool clip_text = false; + TextAlign align = ALIGN_CENTER; + TextAlign icon_align = ALIGN_LEFT; + float _internal_margin[4] = {}; + + void _shape(); protected: - void _set_internal_margin(Margin p_margin, float p_value); + void _set_internal_margin(Side p_side, float p_value); void _notification(int p_what); static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + public: virtual Size2 get_minimum_size() const override; void set_text(const String &p_text); String get_text() const; + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + void set_icon(const Ref<Texture2D> &p_icon); Ref<Texture2D> get_icon() const; @@ -79,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/center_container.cpp b/scene/gui/center_container.cpp index 1a72f3ca4d..909516e7ef 100644 --- a/scene/gui/center_container.cpp +++ b/scene/gui/center_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -95,6 +95,4 @@ void CenterContainer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_top_left"), "set_use_top_left", "is_using_top_left"); } -CenterContainer::CenterContainer() { - use_top_left = false; -} +CenterContainer::CenterContainer() {} diff --git a/scene/gui/center_container.h b/scene/gui/center_container.h index 638843c389..0944f200fc 100644 --- a/scene/gui/center_container.h +++ b/scene/gui/center_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,7 +36,7 @@ class CenterContainer : public Container { GDCLASS(CenterContainer, Container); - bool use_top_left; + bool use_top_left = false; protected: void _notification(int p_what); diff --git a/scene/gui/check_box.cpp b/scene/gui/check_box.cpp index df6f38f65d..d93107df2d 100644 --- a/scene/gui/check_box.cpp +++ b/scene/gui/check_box.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -33,10 +33,12 @@ #include "servers/rendering_server.h" Size2 CheckBox::get_icon_size() const { - Ref<Texture2D> checked = Control::get_theme_icon("checked"); - Ref<Texture2D> unchecked = Control::get_theme_icon("unchecked"); - 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> checked_disabled = Control::get_theme_icon(SNAME("checked_disabled")); + Ref<Texture2D> unchecked = Control::get_theme_icon(SNAME("unchecked")); + Ref<Texture2D> unchecked_disabled = Control::get_theme_icon(SNAME("unchecked_disabled")); + Ref<Texture2D> radio_checked = Control::get_theme_icon(SNAME("radio_checked")); + Ref<Texture2D> radio_unchecked = Control::get_theme_icon(SNAME("radio_unchecked")); Size2 tex_size = Size2(0, 0); if (!checked.is_null()) { @@ -59,27 +61,37 @@ 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"); - minsize.height = MAX(minsize.height, tex_size.height + sb->get_margin(MARGIN_TOP) + sb->get_margin(MARGIN_BOTTOM)); + 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; } void CheckBox::_notification(int p_what) { - if (p_what == NOTIFICATION_THEME_CHANGED) { - _set_internal_margin(MARGIN_LEFT, get_icon_size().width); + if ((p_what == NOTIFICATION_THEME_CHANGED) || (p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || (p_what == NOTIFICATION_TRANSLATION_CHANGED))) { + if (is_layout_rtl()) { + _set_internal_margin(SIDE_LEFT, 0.f); + _set_internal_margin(SIDE_RIGHT, get_icon_size().width); + } else { + _set_internal_margin(SIDE_LEFT, get_icon_size().width); + _set_internal_margin(SIDE_RIGHT, 0.f); + } } else if (p_what == NOTIFICATION_DRAW) { RID ci = get_canvas_item(); - Ref<Texture2D> on = Control::get_theme_icon(is_radio() ? "radio_checked" : "checked"); - Ref<Texture2D> off = Control::get_theme_icon(is_radio() ? "radio_unchecked" : "unchecked"); - Ref<StyleBox> sb = get_theme_stylebox("normal"); + 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(SNAME("normal")); Vector2 ofs; - ofs.x = sb->get_margin(MARGIN_LEFT); - ofs.y = int((get_size().height - get_icon_size().height) / 2) + get_theme_constant("check_vadjust"); + if (is_layout_rtl()) { + ofs.x = get_size().x - sb->get_margin(SIDE_RIGHT) - get_icon_size().width; + } else { + ofs.x = sb->get_margin(SIDE_LEFT); + } + ofs.y = int((get_size().height - get_icon_size().height) / 2) + get_theme_constant(SNAME("check_vadjust")); if (is_pressed()) { on->draw(ci, ofs); @@ -96,8 +108,14 @@ bool CheckBox::is_radio() { CheckBox::CheckBox(const String &p_text) : Button(p_text) { set_toggle_mode(true); + set_text_align(ALIGN_LEFT); - _set_internal_margin(MARGIN_LEFT, get_icon_size().width); + + if (is_layout_rtl()) { + _set_internal_margin(SIDE_RIGHT, get_icon_size().width); + } else { + _set_internal_margin(SIDE_LEFT, get_icon_size().width); + } } CheckBox::~CheckBox() { diff --git a/scene/gui/check_box.h b/scene/gui/check_box.h index cc00524698..9fb0aea218 100644 --- a/scene/gui/check_box.h +++ b/scene/gui/check_box.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ diff --git a/scene/gui/check_button.cpp b/scene/gui/check_button.cpp index 790faeb4fd..162a256d23 100644 --- a/scene/gui/check_button.cpp +++ b/scene/gui/check_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -52,29 +52,50 @@ 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"); - minsize.height = MAX(minsize.height, tex_size.height + sb->get_margin(MARGIN_TOP) + sb->get_margin(MARGIN_BOTTOM)); + 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; } void CheckButton::_notification(int p_what) { - if (p_what == NOTIFICATION_THEME_CHANGED) { - _set_internal_margin(MARGIN_RIGHT, get_icon_size().width); + if ((p_what == NOTIFICATION_THEME_CHANGED) || (p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED) || (p_what == NOTIFICATION_TRANSLATION_CHANGED)) { + if (is_layout_rtl()) { + _set_internal_margin(SIDE_LEFT, get_icon_size().width); + _set_internal_margin(SIDE_RIGHT, 0.f); + } else { + _set_internal_margin(SIDE_LEFT, 0.f); + _set_internal_margin(SIDE_RIGHT, get_icon_size().width); + } } else if (p_what == NOTIFICATION_DRAW) { RID ci = get_canvas_item(); + bool rtl = is_layout_rtl(); - Ref<Texture2D> on = Control::get_theme_icon(is_disabled() ? "on_disabled" : "on"); - Ref<Texture2D> off = Control::get_theme_icon(is_disabled() ? "off_disabled" : "off"); + Ref<Texture2D> on; + if (rtl) { + on = Control::get_theme_icon(is_disabled() ? "on_disabled_mirrored" : "on_mirrored"); + } else { + on = Control::get_theme_icon(is_disabled() ? "on_disabled" : "on"); + } + Ref<Texture2D> off; + if (rtl) { + off = Control::get_theme_icon(is_disabled() ? "off_disabled_mirrored" : "off_mirrored"); + } else { + 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(); - ofs.x = get_size().width - (tex_size.width + sb->get_margin(MARGIN_RIGHT)); - ofs.y = (get_size().height - tex_size.height) / 2 + get_theme_constant("check_vadjust"); + if (rtl) { + ofs.x = sb->get_margin(SIDE_LEFT); + } 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(SNAME("check_vadjust")); if (is_pressed()) { on->draw(ci, ofs); @@ -87,8 +108,11 @@ void CheckButton::_notification(int p_what) { CheckButton::CheckButton() { set_toggle_mode(true); set_text_align(ALIGN_LEFT); - - _set_internal_margin(MARGIN_RIGHT, get_icon_size().width); + if (is_layout_rtl()) { + _set_internal_margin(SIDE_LEFT, get_icon_size().width); + } else { + _set_internal_margin(SIDE_RIGHT, get_icon_size().width); + } } CheckButton::~CheckButton() { diff --git a/scene/gui/check_button.h b/scene/gui/check_button.h index 99a12a3270..29c557ce89 100644 --- a/scene/gui/check_button.h +++ b/scene/gui/check_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index f6f52fbf55..5f3ab18cca 100644 --- a/scene/gui/code_edit.cpp +++ b/scene/gui/code_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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, cache.row_height); - set_gutter_width(line_number_gutter, (line_number_digits + 1) * cache.font->get_char_size('0').width); - set_gutter_width(fold_gutter, cache.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(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 (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(); } @@ -234,14 +1290,16 @@ 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 = String::num(p_line + 1).lpad(line_number_digits, line_number_padding); - - int yofs = p_region.position.y + (cache.row_height - cache.font->get_height()) / 2; + String fc = TS->format_number(String::num(p_line + 1).lpad(line_number_digits, line_number_padding)); + Ref<TextLine> tl; + 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; } - cache.font->draw(get_canvas_item(), Point2(p_region.position.x, yofs + cache.font->get_ascent()), fc, number_color); + tl->draw(get_canvas_item(), Point2(p_region.position.x, yofs), number_color); } /* Fold Gutter */ @@ -254,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; } @@ -266,14 +1324,734 @@ 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. */ + int end_line = get_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. */ + if (end_line == p_line) { + for (int i = p_line + 1; i < get_line_count(); i++) { + 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 < get_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 (get_indent_level(i) <= start_indent && get_line(i).strip_edges().size() != 0) { + end_line = i - 1; + 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. */ + if (start_position.x != -1 && start_position.x != get_line(p_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. */ + if (start_position.x != get_line(i).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 == _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 = ""; + _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); @@ -318,20 +2096,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); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_bookmarks"), "set_draw_bookmarks_gutter", "is_drawing_bookmarks_gutter"); + /* 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); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "draw_executing_lines"), "set_draw_executing_lines_gutter", "is_drawing_executing_lines_gutter"); + ClassDB::bind_method(D_METHOD("get_text_for_symbol_lookup"), &CodeEdit::get_text_for_symbol_lookup); - 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"); + 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_fold_gutter"), "set_draw_fold_gutter", "is_drawing_fold_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::PACKED_INT32_ARRAY, "line_length_guidelines"), "set_line_length_guidelines", "get_line_length_guidelines"); + + 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, "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) { @@ -343,73 +2304,670 @@ 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').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 (p_start_key.length() > 0) { + 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].is_null() ? "" : p_delimiters[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; +} - if (get_gutter_name(i) == "fold_gutter") { - fold_gutter = i; +/* 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); + /* Gutters */ int gutter_idx = 0; @@ -418,7 +2976,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++; @@ -426,7 +2984,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++; @@ -434,13 +2992,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 c989e5ed79..4fbb5194e6 100644 --- a/scene/gui/code_edit.h +++ b/scene/gui/code_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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 gui_input(const Ref<InputEvent> &p_gui_input) override; 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 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 f8a67d154b..0dddb2b09a 100644 --- a/scene/gui/color_picker.cpp +++ b/scene/gui/color_picker.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -40,39 +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_margin((Margin)i, get_margin((Margin)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: { @@ -83,8 +91,67 @@ void ColorPicker::_notification(int p_what) { } } +Ref<Shader> ColorPicker::wheel_shader; +Ref<Shader> ColorPicker::circle_shader; + +void ColorPicker::init_shaders() { + 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() { + wheel_shader.unref(); + circle_shader.unref(); +} + void ColorPicker::set_focus_on_line_edit() { - c_text->call_deferred("grab_focus"); + c_text->call_deferred(SNAME("grab_focus")); } void ColorPicker::_update_controls() { @@ -112,6 +179,27 @@ void ColorPicker::_update_controls() { btn_hsv->set_disabled(false); } + if (raw_mode_enabled) { + for (int i = 0; i < 3; i++) { + scroll[i]->remove_theme_icon_override("grabber"); + scroll[i]->remove_theme_icon_override("grabber_highlight"); + scroll[i]->remove_theme_style_override("slider"); + scroll[i]->remove_theme_style_override("grabber_area"); + scroll[i]->remove_theme_style_override("grabber_area_highlight"); + } + } else { + Ref<StyleBoxEmpty> style_box_empty(memnew(StyleBoxEmpty)); + 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); + scroll[i]->add_theme_icon_override("grabber_highlight", bar_arrow); + scroll[i]->add_theme_style_override("slider", style_box_empty); + scroll[i]->add_theme_style_override("grabber_area", style_box_empty); + scroll[i]->add_theme_style_override("grabber_area_highlight", style_box_empty); + } + } + if (edit_alpha) { values[3]->show(); scroll[3]->show(); @@ -121,6 +209,30 @@ void ColorPicker::_update_controls() { scroll[3]->hide(); labels[3]->hide(); } + + switch (picker_type) { + case SHAPE_HSV_RECTANGLE: + wheel_edit->hide(); + w_edit->show(); + uv_edit->show(); + break; + case SHAPE_HSV_WHEEL: + wheel_edit->show(); + w_edit->hide(); + uv_edit->hide(); + + wheel->set_material(wheel_mat); + break; + case SHAPE_VHS_CIRCLE: + wheel_edit->show(); + w_edit->show(); + uv_edit->hide(); + + wheel->set_material(circle_mat); + break; + default: { + } + } } void ColorPicker::_set_pick_color(const Color &p_color, bool p_update_sliders) { @@ -143,6 +255,18 @@ void ColorPicker::set_pick_color(const Color &p_color) { _set_pick_color(p_color, true); //because setters can't have more arguments } +void ColorPicker::set_old_color(const Color &p_color) { + old_color = p_color; +} + +void ColorPicker::set_display_old_color(bool p_enabled) { + display_old_color = p_enabled; +} + +bool ColorPicker::is_displaying_old_color() const { + return display_old_color; +} + void ColorPicker::set_edit_alpha(bool p_show) { edit_alpha = p_show; _update_controls(); @@ -165,10 +289,13 @@ void ColorPicker::_value_changed(double) { } if (hsv_mode_enabled) { - color.set_hsv(scroll[0]->get_value() / 360.0, - scroll[1]->get_value() / 100.0, - scroll[2]->get_value() / 100.0, - scroll[3]->get_value() / 255.0); + h = scroll[0]->get_value() / 360.0; + s = scroll[1]->get_value() / 100.0; + v = scroll[2]->get_value() / 100.0; + color.set_hsv(h, s, v, scroll[3]->get_value() / 255.0); + + last_hsv = color; + } else { for (int i = 0; i < 4; i++) { color.components[i] = scroll[i]->get_value() / (raw_mode_enabled ? 1.0 : 255.0); @@ -176,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; } @@ -195,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) { @@ -239,33 +366,39 @@ void ColorPicker::_update_color(bool p_update_sliders) { sample->update(); uv_edit->update(); w_edit->update(); + for (int i = 0; i < 4; i++) { + scroll[i]->update(); + } + wheel->update(); + wheel_uv->update(); updating = false; } 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 { @@ -281,13 +414,49 @@ Color ColorPicker::get_pick_color() const { return color; } +void ColorPicker::set_picker_shape(PickerShapeType p_picker_type) { + ERR_FAIL_INDEX(p_picker_type, SHAPE_MAX); + picker_type = p_picker_type; + + _update_controls(); + _update_color(); +} + +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()) { @@ -300,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()) { @@ -392,18 +570,53 @@ void ColorPicker::_update_text_value() { c_text->set_visible(visible); } +void ColorPicker::_sample_input(const Ref<InputEvent> &p_event) { + const Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { + const Rect2 rect_old = Rect2(Point2(), Size2(sample->get_size().width * 0.5, sample->get_size().height * 0.95)); + if (rect_old.has_point(mb->get_position())) { + // Revert to the old color when left-clicking the old color sample. + color = old_color; + _update_color(); + emit_signal(SNAME("color_changed"), color); + } + } +} + void ColorPicker::_sample_draw() { - const Rect2 r = Rect2(Point2(), Size2(uv_edit->get_size().width, sample->get_size().height * 0.95)); + // Covers the right half of the sample if the old color is being displayed, + // or the whole sample if it's not being displayed. + Rect2 rect_new; + + if (display_old_color) { + rect_new = Rect2(Point2(sample->get_size().width * 0.5, 0), Size2(sample->get_size().width * 0.5, sample->get_size().height * 0.95)); + + // Draw both old and new colors for easier comparison (only if spawned from a ColorPickerButton). + 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(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(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"), r, true); + sample->draw_texture_rect(get_theme_icon(SNAME("sample_bg"), SNAME("ColorPicker")), rect_new, true); } - sample->draw_rect(r, color); + sample->draw_rect(rect_new, 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 - sample->draw_texture(get_theme_icon("overbright_indicator", "ColorPicker"), Point2()); + // 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(SNAME("overbright_indicator"), SNAME("ColorPicker")), Point2(uv_edit->get_size().width * 0.5, 0)); } } @@ -413,67 +626,256 @@ void ColorPicker::_hsv_draw(int p_which, Control *c) { } if (p_which == 0) { Vector<Point2> points; - points.push_back(Vector2()); - points.push_back(Vector2(c->get_size().x, 0)); - points.push_back(c->get_size()); - points.push_back(Vector2(0, c->get_size().y)); Vector<Color> colors; - colors.push_back(Color(1, 1, 1, 1)); - colors.push_back(Color(1, 1, 1, 1)); - colors.push_back(Color(0, 0, 0, 1)); - colors.push_back(Color(0, 0, 0, 1)); - c->draw_polygon(points, colors); Vector<Color> colors2; Color col = color; + Vector2 center = c->get_size() / 2.0; + + switch (picker_type) { + case SHAPE_HSV_WHEEL: { + points.resize(4); + colors.resize(4); + colors2.resize(4); + real_t ring_radius_x = Math_SQRT12 * c->get_size().width * 0.42; + real_t ring_radius_y = Math_SQRT12 * c->get_size().height * 0.42; + + points.set(0, center - Vector2(ring_radius_x, ring_radius_y)); + points.set(1, center + Vector2(ring_radius_x, -ring_radius_y)); + points.set(2, center + Vector2(ring_radius_x, ring_radius_y)); + points.set(3, center + Vector2(-ring_radius_x, ring_radius_y)); + colors.set(0, Color(1, 1, 1, 1)); + colors.set(1, Color(1, 1, 1, 1)); + colors.set(2, Color(0, 0, 0, 1)); + colors.set(3, Color(0, 0, 0, 1)); + c->draw_polygon(points, colors); + + col.set_hsv(h, 1, 1); + col.a = 0; + colors2.set(0, col); + col.a = 1; + colors2.set(1, col); + col.set_hsv(h, 1, 0); + colors2.set(2, col); + col.a = 0; + colors2.set(3, col); + c->draw_polygon(points, colors2); + break; + } + case SHAPE_HSV_RECTANGLE: { + points.resize(4); + colors.resize(4); + colors2.resize(4); + points.set(0, Vector2()); + points.set(1, Vector2(c->get_size().x, 0)); + points.set(2, c->get_size()); + points.set(3, Vector2(0, c->get_size().y)); + colors.set(0, Color(1, 1, 1, 1)); + colors.set(1, Color(1, 1, 1, 1)); + colors.set(2, Color(0, 0, 0, 1)); + colors.set(3, Color(0, 0, 0, 1)); + c->draw_polygon(points, colors); + col = color; + col.set_hsv(h, 1, 1); + col.a = 0; + colors2.set(0, col); + col.a = 1; + colors2.set(1, col); + col.set_hsv(h, 1, 0); + colors2.set(2, col); + col.a = 0; + colors2.set(3, col); + c->draw_polygon(points, colors2); + break; + } + default: { + } + } + Ref<Texture2D> cursor = get_theme_icon(SNAME("picker_cursor"), SNAME("ColorPicker")); + int x; + int y; + if (picker_type == SHAPE_VHS_CIRCLE) { + x = center.x + (center.x * Math::cos(h * Math_TAU) * s) - (cursor->get_width() / 2); + y = center.y + (center.y * Math::sin(h * Math_TAU) * s) - (cursor->get_height() / 2); + } else { + real_t corner_x = (c == wheel_uv) ? center.x - Math_SQRT12 * c->get_size().width * 0.42 : 0; + real_t corner_y = (c == wheel_uv) ? center.y - Math_SQRT12 * c->get_size().height * 0.42 : 0; + + Size2 real_size(c->get_size().x - corner_x * 2, c->get_size().y - corner_y * 2); + x = CLAMP(real_size.x * s, 0, real_size.x) + corner_x - (cursor->get_width() / 2); + y = CLAMP(real_size.y - real_size.y * v, 0, real_size.y) + corner_y - (cursor->get_height() / 2); + } + c->draw_texture(cursor, Point2(x, y)); + col.set_hsv(h, 1, 1); - col.a = 0; - colors2.push_back(col); - col.a = 1; - colors2.push_back(col); - col.set_hsv(h, 1, 0); - colors2.push_back(col); - col.a = 0; - colors2.push_back(col); - c->draw_polygon(points, colors2); - int x = CLAMP(c->get_size().x * s, 0, c->get_size().x); - int y = CLAMP(c->get_size().y - c->get_size().y * v, 0, c->get_size().y); - col = color; - col.a = 1; - c->draw_line(Point2(x, 0), Point2(x, c->get_size().y), col.inverted()); - c->draw_line(Point2(0, y), Point2(c->get_size().x, y), col.inverted()); - c->draw_line(Point2(x, y), Point2(x, y), Color(1, 1, 1), 2); + if (picker_type == SHAPE_HSV_WHEEL) { + points.resize(4); + double h1 = h - (0.5 / 360); + double h2 = h + (0.5 / 360); + points.set(0, Point2(center.x + (center.x * Math::cos(h1 * Math_TAU)), center.y + (center.y * Math::sin(h1 * Math_TAU)))); + points.set(1, Point2(center.x + (center.x * Math::cos(h1 * Math_TAU) * 0.84), center.y + (center.y * Math::sin(h1 * Math_TAU) * 0.84))); + points.set(2, Point2(center.x + (center.x * Math::cos(h2 * Math_TAU)), center.y + (center.y * Math::sin(h2 * Math_TAU)))); + points.set(3, Point2(center.x + (center.x * Math::cos(h2 * Math_TAU) * 0.84), center.y + (center.y * Math::sin(h2 * Math_TAU) * 0.84))); + c->draw_multiline(points, col.inverted()); + } + } else if (p_which == 1) { - Ref<Texture2D> hue = get_theme_icon("color_hue", "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 = Color(); - col.set_hsv(h, 1, 1); - c->draw_line(Point2(0, y), Point2(c->get_size().x, y), col.inverted()); + if (picker_type == SHAPE_HSV_RECTANGLE) { + 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; + col.set_hsv(h, 1, 1); + c->draw_line(Point2(0, y), Point2(c->get_size().x, y), col.inverted()); + } else if (picker_type == SHAPE_VHS_CIRCLE) { + Vector<Point2> points; + Vector<Color> colors; + Color col; + col.set_hsv(h, s, 1); + points.resize(4); + colors.resize(4); + points.set(0, Vector2()); + points.set(1, Vector2(c->get_size().x, 0)); + points.set(2, c->get_size()); + points.set(3, Vector2(0, c->get_size().y)); + colors.set(0, col); + colors.set(1, col); + colors.set(2, Color(0, 0, 0)); + colors.set(3, Color(0, 0, 0)); + c->draw_polygon(points, colors); + int y = c->get_size().y - c->get_size().y * CLAMP(v, 0, 1); + col.set_hsv(h, 1, v); + c->draw_line(Point2(0, y), Point2(c->get_size().x, y), col.inverted()); + } + } else if (p_which == 2) { + c->draw_rect(Rect2(Point2(), c->get_size()), Color(1, 1, 1)); + if (picker_type == SHAPE_VHS_CIRCLE) { + circle_mat->set_shader_param("v", v); + } + } +} + +void ColorPicker::_slider_draw(int p_which) { + Vector<Vector2> pos; + pos.resize(4); + Vector<Color> col; + col.resize(4); + Size2 size = scroll[p_which]->get_size(); + Color left_color; + Color right_color; +#ifdef TOOLS_ENABLED + const real_t margin = 4 * EDSCALE; +#else + const real_t margin = 4; +#endif + + if (p_which == 3) { + 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; + right_color = color; + right_color.a = 1; + } else { + if (raw_mode_enabled) { + return; + } + if (hsv_mode_enabled) { + if (p_which == 0) { + 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; + } + Color s_col; + Color v_col; + s_col.set_hsv(h, 0, v); + left_color = (p_which == 1) ? s_col : Color(0, 0, 0); + s_col.set_hsv(h, 1, v); + v_col.set_hsv(h, s, 1); + right_color = (p_which == 1) ? s_col : v_col; + } else { + left_color = Color( + p_which == 0 ? 0 : color.r, + p_which == 1 ? 0 : color.g, + p_which == 2 ? 0 : color.b); + right_color = Color( + p_which == 0 ? 1 : color.r, + p_which == 1 ? 1 : color.g, + p_which == 2 ? 1 : color.b); + } } + + col.set(0, left_color); + col.set(1, right_color); + col.set(2, right_color); + col.set(3, left_color); + pos.set(0, Vector2(0, margin)); + pos.set(1, Vector2(size.x, margin)); + pos.set(2, Vector2(size.x, margin * 2)); + pos.set(3, Vector2(0, margin * 2)); + + scroll[p_which]->draw_polygon(pos, col); } -void ColorPicker::_uv_input(const Ref<InputEvent> &p_event) { +void ColorPicker::_uv_input(const Ref<InputEvent> &p_event, Control *c) { Ref<InputEventMouseButton> bev = p_event; if (bev.is_valid()) { - if (bev->is_pressed() && bev->get_button_index() == BUTTON_LEFT) { + if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { + Vector2 center = c->get_size() / 2.0; + if (picker_type == SHAPE_VHS_CIRCLE) { + real_t dist = center.distance_to(bev->get_position()); + + if (dist <= center.x) { + real_t rad = Math::atan2(bev->get_position().y - center.y, bev->get_position().x - center.x); + h = ((rad >= 0) ? rad : (Math_TAU + rad)) / Math_TAU; + s = CLAMP(dist / center.x, 0, 1); + } else { + return; + } + } else { + real_t corner_x = (c == wheel_uv) ? center.x - Math_SQRT12 * c->get_size().width * 0.42 : 0; + real_t corner_y = (c == wheel_uv) ? center.y - Math_SQRT12 * c->get_size().height * 0.42 : 0; + Size2 real_size(c->get_size().x - corner_x * 2, c->get_size().y - corner_y * 2); + + if (bev->get_position().x < corner_x || bev->get_position().x > c->get_size().x - corner_x || + bev->get_position().y < corner_y || bev->get_position().y > c->get_size().y - corner_y) { + { + real_t dist = center.distance_to(bev->get_position()); + + if (dist >= center.x * 0.84 && dist <= center.x) { + real_t rad = Math::atan2(bev->get_position().y - center.y, bev->get_position().x - center.x); + h = ((rad >= 0) ? rad : (Math_TAU + rad)) / Math_TAU; + spinning = true; + } else { + return; + } + } + } + + if (!spinning) { + real_t x = CLAMP(bev->get_position().x, corner_x, c->get_size().x - corner_x); + real_t y = CLAMP(bev->get_position().y, corner_x, c->get_size().y - corner_y); + + s = (x - c->get_position().x - corner_x) / real_size.x; + v = 1.0 - (y - c->get_position().y - corner_y) / real_size.y; + } + } changing_color = true; - float x = CLAMP((float)bev->get_position().x, 0, uv_edit->get_size().width); - float y = CLAMP((float)bev->get_position().y, 0, uv_edit->get_size().height); - s = x / uv_edit->get_size().width; - v = 1.0 - y / uv_edit->get_size().height; color.set_hsv(h, s, v, color.a); last_hsv = color; 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() == BUTTON_LEFT) { - emit_signal("color_changed", color); + } else if (deferred_mode_enabled && !bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { + emit_signal(SNAME("color_changed"), color); changing_color = false; + spinning = false; } else { changing_color = false; + spinning = false; } } @@ -483,16 +885,36 @@ void ColorPicker::_uv_input(const Ref<InputEvent> &p_event) { if (!changing_color) { return; } - float x = CLAMP((float)mev->get_position().x, 0, uv_edit->get_size().width); - float y = CLAMP((float)mev->get_position().y, 0, uv_edit->get_size().height); - s = x / uv_edit->get_size().width; - v = 1.0 - y / uv_edit->get_size().height; + + Vector2 center = c->get_size() / 2.0; + if (picker_type == SHAPE_VHS_CIRCLE) { + real_t dist = center.distance_to(mev->get_position()); + real_t rad = Math::atan2(mev->get_position().y - center.y, mev->get_position().x - center.x); + h = ((rad >= 0) ? rad : (Math_TAU + rad)) / Math_TAU; + s = CLAMP(dist / center.x, 0, 1); + } else { + if (spinning) { + real_t rad = Math::atan2(mev->get_position().y - center.y, mev->get_position().x - center.x); + h = ((rad >= 0) ? rad : (Math_TAU + rad)) / Math_TAU; + } else { + real_t corner_x = (c == wheel_uv) ? center.x - Math_SQRT12 * c->get_size().width * 0.42 : 0; + real_t corner_y = (c == wheel_uv) ? center.y - Math_SQRT12 * c->get_size().height * 0.42 : 0; + Size2 real_size(c->get_size().x - corner_x * 2, c->get_size().y - corner_y * 2); + + real_t x = CLAMP(mev->get_position().x, corner_x, c->get_size().x - corner_x); + real_t y = CLAMP(mev->get_position().y, corner_x, c->get_size().y - corner_y); + + s = (x - corner_x) / real_size.x; + v = 1.0 - (y - corner_y) / real_size.y; + } + } + color.set_hsv(h, s, v, color.a); last_hsv = color; set_pick_color(color); _update_color(); if (!deferred_mode_enabled) { - emit_signal("color_changed", color); + emit_signal(SNAME("color_changed"), color); } } } @@ -501,10 +923,14 @@ void ColorPicker::_w_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> bev = p_event; if (bev.is_valid()) { - if (bev->is_pressed() && bev->get_button_index() == BUTTON_LEFT) { + if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { changing_color = true; float y = CLAMP((float)bev->get_position().y, 0, w_edit->get_size().height); - h = y / w_edit->get_size().height; + if (picker_type == SHAPE_VHS_CIRCLE) { + v = 1.0 - (y / w_edit->get_size().height); + } else { + h = y / w_edit->get_size().height; + } } else { changing_color = false; } @@ -513,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); - } else if (!bev->is_pressed() && bev->get_button_index() == BUTTON_LEFT) { - 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(SNAME("color_changed"), color); } } @@ -526,60 +952,44 @@ void ColorPicker::_w_input(const Ref<InputEvent> &p_event) { return; } float y = CLAMP((float)mev->get_position().y, 0, w_edit->get_size().height); - h = y / w_edit->get_size().height; + if (picker_type == SHAPE_VHS_CIRCLE) { + v = 1.0 - (y / w_edit->get_size().height); + } else { + h = y / w_edit->get_size().height; + } color.set_hsv(h, s, v, color.a); last_hsv = color; 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() == 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]); + if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_LEFT) { + set_pick_color(p_color); _update_color(); - emit_signal("color_changed", color); - } else if (bev->is_pressed() && bev->get_button_index() == 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; + emit_signal(SNAME("color_changed"), p_color); + } else if (bev->is_pressed() && bev->get_button_index() == MOUSE_BUTTON_RIGHT && presets_enabled) { + 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))); } } void ColorPicker::_screen_input(const Ref<InputEvent> &p_event) { + if (!is_inside_tree()) { + return; + } + Ref<InputEventMouseButton> bev = p_event; - if (bev.is_valid() && bev->get_button_index() == BUTTON_LEFT && !bev->is_pressed()) { - emit_signal("color_changed", color); + if (bev.is_valid() && bev->get_button_index() == MOUSE_BUTTON_LEFT && !bev->is_pressed()) { + emit_signal(SNAME("color_changed"), color); screen->hide(); } @@ -590,8 +1000,8 @@ void ColorPicker::_screen_input(const Ref<InputEvent> &p_event) { return; } - Ref<Image> img = r->get_texture()->get_data(); - if (img.is_valid() && !img->empty()) { + Ref<Image> img = r->get_texture()->get_image(); + if (img.is_valid() && !img->is_empty()) { Vector2 ofs = mev->get_global_position() - r->get_visible_rect().get_position(); Color c = img->get_pixel(ofs.x, r->get_visible_rect().size.height - ofs.y); @@ -602,20 +1012,24 @@ 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() { + if (!is_inside_tree()) { + return; + } + Viewport *r = get_tree()->get_root(); if (!screen) { screen = memnew(Control); r->add_child(screen); screen->set_as_top_level(true); - screen->set_anchors_and_margins_preset(Control::PRESET_WIDE); + screen->set_anchors_and_offsets_preset(Control::PRESET_WIDE); 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", "hide", 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 @@ -651,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); } } @@ -677,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 { @@ -702,67 +1115,53 @@ void ColorPicker::_bind_methods() { ClassDB::bind_method(D_METHOD("add_preset", "color"), &ColorPicker::add_preset); ClassDB::bind_method(D_METHOD("erase_preset", "color"), &ColorPicker::erase_preset); ClassDB::bind_method(D_METHOD("get_presets"), &ColorPicker::get_presets); + ClassDB::bind_method(D_METHOD("set_picker_shape", "picker"), &ColorPicker::set_picker_shape); + ClassDB::bind_method(D_METHOD("get_picker_shape"), &ColorPicker::get_picker_shape); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_pick_color", "get_pick_color"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "edit_alpha"), "set_edit_alpha", "is_editing_alpha"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hsv_mode"), "set_hsv_mode", "is_hsv_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "raw_mode"), "set_raw_mode", "is_raw_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "deferred_mode"), "set_deferred_mode", "is_deferred_mode"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "picker_shape", PROPERTY_HINT_ENUM, "HSV Rectangle,HSV Rectangle Wheel,VHS Circle"), "set_picker_shape", "get_picker_shape"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "presets_enabled"), "set_presets_enabled", "are_presets_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "presets_visible"), "set_presets_visible", "are_presets_visible"); ADD_SIGNAL(MethodInfo("color_changed", PropertyInfo(Variant::COLOR, "color"))); ADD_SIGNAL(MethodInfo("preset_added", PropertyInfo(Variant::COLOR, "color"))); ADD_SIGNAL(MethodInfo("preset_removed", PropertyInfo(Variant::COLOR, "color"))); + + BIND_ENUM_CONSTANT(SHAPE_HSV_RECTANGLE); + BIND_ENUM_CONSTANT(SHAPE_HSV_WHEEL); + BIND_ENUM_CONSTANT(SHAPE_VHS_CIRCLE); } ColorPicker::ColorPicker() : BoxContainer(true) { - updating = true; - edit_alpha = true; - text_is_constructor = false; - hsv_mode_enabled = false; - raw_mode_enabled = false; - deferred_mode_enabled = false; - changing_color = false; - presets_enabled = true; - presets_visible = true; - screen = nullptr; - HBoxContainer *hb_edit = memnew(HBoxContainer); add_child(hb_edit); hb_edit->set_v_size_flags(SIZE_EXPAND_FILL); - uv_edit = memnew(Control); hb_edit->add_child(uv_edit); - uv_edit->connect("gui_input", callable_mp(this, &ColorPicker::_uv_input)); + uv_edit->connect("gui_input", callable_mp(this, &ColorPicker::_uv_input), make_binds(uv_edit)); 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)); - w_edit = memnew(Control); - hb_edit->add_child(w_edit); - w_edit->set_custom_minimum_size(Size2(get_theme_constant("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)); - w_edit->connect("draw", callable_mp(this, &ColorPicker::_hsv_draw), make_binds(1, w_edit)); - HBoxContainer *hb_smpl = memnew(HBoxContainer); add_child(hb_smpl); - sample = memnew(TextureRect); hb_smpl->add_child(sample); sample->set_h_size_flags(SIZE_EXPAND_FILL); + sample->connect("gui_input", callable_mp(this, &ColorPicker::_sample_input)); sample->connect("draw", callable_mp(this, &ColorPicker::_sample_draw)); - btn_pick = memnew(Button); btn_pick->set_flat(true); hb_smpl->add_child(btn_pick); btn_pick->set_toggle_mode(true); - btn_pick->set_tooltip(TTR("Pick a color from the editor window.")); + btn_pick->set_tooltip(RTR("Pick a color from the editor window.")); btn_pick->connect("pressed", callable_mp(this, &ColorPicker::_screen_pick_pressed)); VBoxContainer *vbl = memnew(VBoxContainer); @@ -778,7 +1177,7 @@ ColorPicker::ColorPicker() : 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]); @@ -798,25 +1197,24 @@ ColorPicker::ColorPicker() : scroll[i]->set_h_size_flags(SIZE_EXPAND_FILL); scroll[i]->connect("value_changed", callable_mp(this, &ColorPicker::_value_changed)); + scroll[i]->connect("draw", callable_mp(this, &ColorPicker::_slider_draw), make_binds(i)); vbr->add_child(hbc); } + labels[3]->set_text("A"); HBoxContainer *hhb = memnew(HBoxContainer); vbr->add_child(hhb); - btn_hsv = memnew(CheckButton); hhb->add_child(btn_hsv); - btn_hsv->set_text(TTR("HSV")); + btn_hsv->set_text(RTR("HSV")); btn_hsv->connect("toggled", callable_mp(this, &ColorPicker::set_hsv_mode)); - btn_raw = memnew(CheckButton); hhb->add_child(btn_raw); - btn_raw->set_text(TTR("Raw")); + btn_raw->set_text(RTR("Raw")); btn_raw->connect("toggled", callable_mp(this, &ColorPicker::set_raw_mode)); - text_type = memnew(Button); hhb->add_child(text_type); text_type->set_text("#"); text_type->set_tooltip(TTR("Switch between hexadecimal and code values.")); @@ -830,49 +1228,80 @@ ColorPicker::ColorPicker() : text_type->set_mouse_filter(MOUSE_FILTER_IGNORE); } - c_text = memnew(LineEdit); 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(SNAME("sv_width")), get_theme_constant(SNAME("sv_height")))); + hb_edit->add_child(wheel_edit); + + wheel_mat.instantiate(); + wheel_mat->set_shader(wheel_shader); + circle_mat.instantiate(); + circle_mat->set_shader(circle_shader); + + MarginContainer *wheel_margin(memnew(MarginContainer)); +#ifdef TOOLS_ENABLED + wheel_margin->add_theme_constant_override("margin_bottom", 8 * EDSCALE); +#else + wheel_margin->add_theme_constant_override("margin_bottom", 8); +#endif + wheel_edit->add_child(wheel_margin); + + wheel_margin->add_child(wheel); + wheel->set_mouse_filter(MOUSE_FILTER_PASS); + wheel->connect("draw", callable_mp(this, &ColorPicker::_hsv_draw), make_binds(2, wheel)); + + wheel_margin->add_child(wheel_uv); + wheel_uv->connect("gui_input", callable_mp(this, &ColorPicker::_uv_input), make_binds(wheel_uv)); + 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(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)); + w_edit->connect("draw", callable_mp(this, &ColorPicker::_hsv_draw), make_binds(1, w_edit)); + + picker_type = SHAPE_HSV_RECTANGLE; _update_controls(); updating = false; set_pick_color(Color(1, 1, 1)); - preset_separator = memnew(HSeparator); add_child(preset_separator); - preset_container = memnew(HBoxContainer); preset_container->set_h_size_flags(SIZE_EXPAND_FILL); + preset_container->set_columns(preset_column_count); add_child(preset_container); - preset = memnew(TextureRect); - 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_container2 = memnew(HBoxContainer); - preset_container2->set_h_size_flags(SIZE_EXPAND_FILL); - add_child(preset_container2); - bt_add_preset = memnew(Button); - preset_container2->add_child(bt_add_preset); - bt_add_preset->set_tooltip(TTR("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); } ///////////////// +void ColorPickerButton::_about_to_popup() { + set_pressed(true); + if (picker) { + picker->set_old_color(color); + } +} + 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); } @@ -880,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 @@ -910,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: { @@ -973,15 +1403,16 @@ void ColorPickerButton::_update_picker() { popup = memnew(PopupPanel); popup->set_wrap_controls(true); picker = memnew(ColorPicker); - picker->set_anchors_and_margins_preset(PRESET_WIDE); + picker->set_anchors_and_offsets_preset(PRESET_WIDE); popup->add_child(picker); add_child(popup); picker->connect("color_changed", callable_mp(this, &ColorPickerButton::_color_changed)); - popup->connect("about_to_popup", callable_mp((BaseButton *)this, &BaseButton::set_pressed), varray(true)); + 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); - emit_signal("picker_created"); + picker->set_display_old_color(true); + emit_signal(SNAME("picker_created")); } } @@ -992,6 +1423,7 @@ void ColorPickerButton::_bind_methods() { ClassDB::bind_method(D_METHOD("get_popup"), &ColorPickerButton::get_popup); ClassDB::bind_method(D_METHOD("set_edit_alpha", "show"), &ColorPickerButton::set_edit_alpha); ClassDB::bind_method(D_METHOD("is_editing_alpha"), &ColorPickerButton::is_editing_alpha); + ClassDB::bind_method(D_METHOD("_about_to_popup"), &ColorPickerButton::_about_to_popup); ADD_SIGNAL(MethodInfo("color_changed", PropertyInfo(Variant::COLOR, "color"))); ADD_SIGNAL(MethodInfo("popup_closed")); @@ -1001,12 +1433,66 @@ void ColorPickerButton::_bind_methods() { } ColorPickerButton::ColorPickerButton() { - // Initialization is now done deferred, - // this improves performance in the inspector as the color picker - // can be expensive to initialize. - picker = nullptr; - popup = nullptr; - edit_alpha = true; - 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 128664b49d..67ca007eb5 100644 --- a/scene/gui/color_picker.h +++ b/scene/gui/color_picker.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -31,9 +31,11 @@ #ifndef COLOR_PICKER_H #define COLOR_PICKER_H +#include "scene/gui/aspect_ratio_container.h" #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" @@ -42,57 +44,101 @@ #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); +public: + enum PickerShapeType { + SHAPE_HSV_RECTANGLE, + SHAPE_HSV_WHEEL, + SHAPE_VHS_CIRCLE, + + SHAPE_MAX + }; + private: - Control *screen; - Control *uv_edit; - Control *w_edit; - TextureRect *sample; - TextureRect *preset; - HBoxContainer *preset_container; - HBoxContainer *preset_container2; - HSeparator *preset_separator; - Button *bt_add_preset; - List<Color> presets; - Button *btn_pick; - CheckButton *btn_hsv; - CheckButton *btn_raw; + static Ref<Shader> wheel_shader; + static Ref<Shader> circle_shader; + static List<Color> preset_cache; + + Control *screen = nullptr; + Control *uv_edit = memnew(Control); + Control *w_edit = memnew(Control); + AspectRatioContainer *wheel_edit = memnew(AspectRatioContainer); + Ref<ShaderMaterial> wheel_mat; + Ref<ShaderMaterial> circle_mat; + Control *wheel = memnew(Control); + Control *wheel_uv = memnew(Control); + TextureRect *sample = memnew(TextureRect); + GridContainer *preset_container = memnew(GridContainer); + HSeparator *preset_separator = memnew(HSeparator); + Button *btn_add_preset = memnew(Button); + Button *btn_pick = memnew(Button); + CheckButton *btn_hsv = memnew(CheckButton); + CheckButton *btn_raw = memnew(CheckButton); HSlider *scroll[4]; SpinBox *values[4]; Label *labels[4]; - Button *text_type; - LineEdit *c_text; - bool edit_alpha; + Button *text_type = memnew(Button); + LineEdit *c_text = memnew(LineEdit); + + bool edit_alpha = true; Size2i ms; - bool text_is_constructor; - int presets_per_row; + bool text_is_constructor = false; + PickerShapeType picker_type = SHAPE_HSV_WHEEL; + + const int preset_column_count = 9; + int prev_preset_size = 0; + List<Color> presets; Color color; - bool raw_mode_enabled; - bool hsv_mode_enabled; - bool deferred_mode_enabled; - bool updating; - bool changing_color; - bool presets_enabled; - bool presets_visible; - float h, s, v; + Color old_color; + + bool display_old_color = false; + bool raw_mode_enabled = false; + bool hsv_mode_enabled = false; + bool deferred_mode_enabled = false; + bool updating = true; + bool changing_color = false; + 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); void _sample_draw(); void _hsv_draw(int p_which, Control *c); + void _slider_draw(int p_which); - void _uv_input(const Ref<InputEvent> &p_event); + 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(); @@ -100,21 +146,35 @@ 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(); public: + static void init_shaders(); + static void finish_shaders(); + void set_edit_alpha(bool p_show); bool is_editing_alpha() const; void _set_pick_color(const Color &p_color, bool p_update_sliders); void set_pick_color(const Color &p_color); Color get_pick_color() const; + void set_old_color(const Color &p_color); + + void set_display_old_color(bool p_enabled); + bool is_displaying_old_color() const; + + void set_picker_shape(PickerShapeType p_picker_type); + PickerShapeType get_picker_shape() const; 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; @@ -139,11 +199,16 @@ public: class ColorPickerButton : public Button { GDCLASS(ColorPickerButton, Button); - PopupPanel *popup; - ColorPicker *picker; + // Initialization is now done deferred, + // this improves performance in the inspector as the color picker + // can be expensive to initialize. + + PopupPanel *popup = nullptr; + ColorPicker *picker = nullptr; Color color; - bool edit_alpha; + bool edit_alpha = true; + void _about_to_popup(); void _color_changed(const Color &p_color); void _modal_closed(); @@ -168,4 +233,5 @@ public: ColorPickerButton(); }; +VARIANT_ENUM_CAST(ColorPicker::PickerShapeType); #endif // COLOR_PICKER_H diff --git a/scene/gui/color_rect.cpp b/scene/gui/color_rect.cpp index 0c38b93c60..e35d37d520 100644 --- a/scene/gui/color_rect.cpp +++ b/scene/gui/color_rect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ diff --git a/scene/gui/color_rect.h b/scene/gui/color_rect.h index 61d57f7cca..5c650f9f01 100644 --- a/scene/gui/color_rect.h +++ b/scene/gui/color_rect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ diff --git a/scene/gui/container.cpp b/scene/gui/container.cpp index 5643110b89..c97434f69b 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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(); @@ -121,12 +121,7 @@ void Container::fit_child_in_rect(Control *p_child, const Rect2 &p_rect) { } } - for (int i = 0; i < 4; i++) { - p_child->set_anchor(Margin(i), ANCHOR_BEGIN); - } - - p_child->set_position(r.position); - p_child->set_size(r.size); + p_child->set_rect(r); p_child->set_rotation(0); p_child->set_scale(Vector2(1, 1)); } @@ -164,16 +159,14 @@ void Container::_notification(int p_what) { } } -String Container::get_configuration_warning() const { - String warning = Control::get_configuration_warning(); +TypedArray<String> Container::get_configuration_warnings() const { + TypedArray<String> warnings = Control::get_configuration_warnings(); if (get_class() == "Container" && get_script().is_null()) { - if (!warning.empty()) { - warning += "\n\n"; - } - warning += TTR("Container by itself serves no purpose unless a script configures its children placement behavior.\nIf you don't intend to add a script, use a plain Control node instead."); + warnings.push_back(TTR("Container by itself serves no purpose unless a script configures its children placement behavior.\nIf you don't intend to add a script, use a plain Control node instead.")); } - return warning; + + return warnings; } void Container::_bind_methods() { @@ -185,7 +178,6 @@ void Container::_bind_methods() { } Container::Container() { - pending_sort = false; // All containers should let mouse events pass by default. set_mouse_filter(MOUSE_FILTER_PASS); } diff --git a/scene/gui/container.h b/scene/gui/container.h index b789bcf3b0..bce3085f0c 100644 --- a/scene/gui/container.h +++ b/scene/gui/container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,7 +36,7 @@ class Container : public Control { GDCLASS(Container, Control); - bool pending_sort; + bool pending_sort = false; void _sort_children(); void _child_minsize_changed(); @@ -56,7 +56,7 @@ public: void fit_child_in_rect(Control *p_child, const Rect2 &p_rect); - virtual String get_configuration_warning() const override; + TypedArray<String> get_configuration_warnings() const override; Container(); }; diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 3414b04978..4ac6a58d36 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,12 +36,15 @@ #include "core/os/keyboard.h" #include "core/os/os.h" #include "core/string/print_string.h" +#include "core/string/translation.h" + #include "scene/gui/label.h" #include "scene/gui/panel.h" #include "scene/main/canvas_layer.h" #include "scene/main/window.h" #include "scene/scene_string_names.h" #include "servers/rendering_server.h" +#include "servers/text_server.h" #ifdef TOOLS_ENABLED #include "editor/editor_settings.h" @@ -55,42 +58,46 @@ Dictionary Control::_edit_get_state() const { s["scale"] = get_scale(); s["pivot"] = get_pivot_offset(); Array anchors; - anchors.push_back(get_anchor(MARGIN_LEFT)); - anchors.push_back(get_anchor(MARGIN_TOP)); - anchors.push_back(get_anchor(MARGIN_RIGHT)); - anchors.push_back(get_anchor(MARGIN_BOTTOM)); + anchors.push_back(get_anchor(SIDE_LEFT)); + anchors.push_back(get_anchor(SIDE_TOP)); + anchors.push_back(get_anchor(SIDE_RIGHT)); + anchors.push_back(get_anchor(SIDE_BOTTOM)); s["anchors"] = anchors; - Array margins; - margins.push_back(get_margin(MARGIN_LEFT)); - margins.push_back(get_margin(MARGIN_TOP)); - margins.push_back(get_margin(MARGIN_RIGHT)); - margins.push_back(get_margin(MARGIN_BOTTOM)); - s["margins"] = margins; + Array offsets; + offsets.push_back(get_offset(SIDE_LEFT)); + offsets.push_back(get_offset(SIDE_TOP)); + offsets.push_back(get_offset(SIDE_RIGHT)); + offsets.push_back(get_offset(SIDE_BOTTOM)); + s["offsets"] = offsets; return s; } void Control::_edit_set_state(const Dictionary &p_state) { + ERR_FAIL_COND((p_state.size() <= 0) || + !p_state.has("rotation") || !p_state.has("scale") || + !p_state.has("pivot") || !p_state.has("anchors") || !p_state.has("offsets")); Dictionary state = p_state; set_rotation(state["rotation"]); set_scale(state["scale"]); set_pivot_offset(state["pivot"]); Array anchors = state["anchors"]; - data.anchor[MARGIN_LEFT] = anchors[0]; - data.anchor[MARGIN_TOP] = anchors[1]; - data.anchor[MARGIN_RIGHT] = anchors[2]; - data.anchor[MARGIN_BOTTOM] = anchors[3]; - Array margins = state["margins"]; - data.margin[MARGIN_LEFT] = margins[0]; - data.margin[MARGIN_TOP] = margins[1]; - data.margin[MARGIN_RIGHT] = margins[2]; - data.margin[MARGIN_BOTTOM] = margins[3]; + data.anchor[SIDE_LEFT] = anchors[0]; + data.anchor[SIDE_TOP] = anchors[1]; + data.anchor[SIDE_RIGHT] = anchors[2]; + data.anchor[SIDE_BOTTOM] = anchors[3]; + Array offsets = state["offsets"]; + data.offset[SIDE_LEFT] = offsets[0]; + data.offset[SIDE_TOP] = offsets[1]; + data.offset[SIDE_RIGHT] = offsets[2]; + data.offset[SIDE_BOTTOM] = offsets[3]; _size_changed(); } void Control::_edit_set_position(const Point2 &p_position) { #ifdef TOOLS_ENABLED - set_position(p_position, CanvasItemEditor::get_singleton()->is_anchors_mode_enabled()); + ERR_FAIL_COND_MSG(!Engine::get_singleton()->is_editor_hint(), "This function can only be used from editor plugins."); + set_position(p_position, CanvasItemEditor::get_singleton()->is_anchors_mode_enabled() && Object::cast_to<Control>(data.parent)); #else // Unlikely to happen. TODO: enclose all _edit_ functions into TOOLS_ENABLED set_position(p_position); @@ -111,6 +118,7 @@ Size2 Control::_edit_get_scale() const { void Control::_edit_set_rect(const Rect2 &p_edit_rect) { #ifdef TOOLS_ENABLED + ERR_FAIL_COND_MSG(!Engine::get_singleton()->is_editor_hint(), "This function can only be used from editor plugins."); set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snapped(Vector2(1, 1)), CanvasItemEditor::get_singleton()->is_anchors_mode_enabled()); set_size(p_edit_rect.size.snapped(Vector2(1, 1)), CanvasItemEditor::get_singleton()->is_anchors_mode_enabled()); #else @@ -128,11 +136,11 @@ bool Control::_edit_use_rect() const { return true; } -void Control::_edit_set_rotation(float p_rotation) { +void Control::_edit_set_rotation(real_t p_rotation) { set_rotation(p_rotation); } -float Control::_edit_get_rotation() const { +real_t Control::_edit_get_rotation() const { return get_rotation(); } @@ -160,6 +168,12 @@ Size2 Control::_edit_get_minimum_size() const { } #endif +void Control::accept_event() { + if (is_inside_tree()) { + get_viewport()->_gui_accept_event(); + } +} + void Control::set_custom_minimum_size(const Size2 &p_custom) { if (p_custom == data.custom_minimum_size) { return; @@ -208,44 +222,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_shaders/")) { - String dname = name.get_slicec('/', 1); - if (data.shader_override.has(dname)) { - data.shader_override[dname]->disconnect("changed", callable_mp(this, &Control::_override_changed)); - } - data.shader_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_colors/")) { + } 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("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); @@ -254,22 +266,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_shaders/")) { - String dname = name.get_slicec('/', 1); - add_theme_shader_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_colors/")) { + } 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("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 { @@ -285,48 +297,38 @@ void Control::_update_minimum_size() { } Size2 minsize = get_combined_minimum_size(); - if (minsize.x > data.size_cache.x || - minsize.y > data.size_cache.y) { - _size_changed(); - } - data.updating_last_minimum_size = false; if (minsize != data.last_minimum_size) { data.last_minimum_size = minsize; + _size_changed(); emit_signal(SceneStringNames::get_singleton()->minimum_size_changed); } } 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_shaders/")) { - String name = sname.get_slicec('/', 1); - - r_ret = data.shader_override.has(name) ? Variant(data.shader_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_colors/")) { + } 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("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 { return false; @@ -337,95 +339,180 @@ 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()) { - uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.icon_override.has(E->get())) { - hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; + theme->get_color_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)) { + usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_icons/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", hint)); + p_list->push_back(PropertyInfo(Variant::COLOR, "theme_override_colors/" + E, PROPERTY_HINT_NONE, "", usage)); } } { List<StringName> names; - theme->get_shader_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { - uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.shader_override.has(E->get())) { - hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; + theme->get_constant_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)) { + usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::OBJECT, "custom_shaders/" + E->get(), PROPERTY_HINT_RESOURCE_TYPE, "Shader,VisualShader", hint)); + p_list->push_back(PropertyInfo(Variant::INT, "theme_override_constants/" + E, PROPERTY_HINT_RANGE, "-16384,16384", usage)); } } { List<StringName> names; - theme->get_stylebox_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { - uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.style_override.has(E->get())) { - hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; + theme->get_font_list(get_class_name(), &names); + for (const StringName &E : names) { + uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; + if (data.font_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", hint)); + p_list->push_back(PropertyInfo(Variant::OBJECT, "theme_override_fonts/" + E, PROPERTY_HINT_RESOURCE_TYPE, "Font", usage)); } } { List<StringName> names; - theme->get_font_list(get_class_name(), &names); - for (List<StringName>::Element *E = names.front(); E; E = E->next()) { - uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.font_override.has(E->get())) { - hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; + theme->get_font_size_list(get_class_name(), &names); + for (const StringName &E : names) { + uint32_t usage = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; + if (data.font_size_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", hint)); + 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()) { - uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.color_override.has(E->get())) { - hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; + theme->get_icon_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)) { + usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::COLOR, "custom_colors/" + E->get(), PROPERTY_HINT_NONE, "", hint)); + 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()) { - uint32_t hint = PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_CHECKABLE; - if (data.constant_override.has(E->get())) { - hint |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; + theme->get_stylebox_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)) { + usage |= PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_CHECKED; } - p_list->push_back(PropertyInfo(Variant::INT, "custom_constants/" + E->get(), PROPERTY_HINT_RANGE, "-16384,16384", hint)); + 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; + } +} + Control *Control::get_parent_control() const { return data.parent; } -void Control::_resize(const Size2 &p_size) { - _size_changed(); +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); +} + +Control::LayoutDirection Control::get_layout_direction() const { + return data.layout_dir; +} + +bool Control::is_layout_rtl() const { + 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); + } + } + } 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); + } + } + 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() { + data.size_warning = false; } //moved theme configuration here, so controls can set up even if still not inside active scene @@ -471,15 +558,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; @@ -544,10 +638,12 @@ 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: { - // some parents need to know the order of the childrens to draw (like TabContainer) + // some parents need to know the order of the children to draw (like TabContainer) // update if necessary if (data.parent) { data.parent->update(); @@ -582,7 +678,6 @@ void Control::_notification(int p_notification) { case NOTIFICATION_FOCUS_EXIT: { emit_signal(SceneStringNames::get_singleton()->focus_exited); update(); - } break; case NOTIFICATION_THEME_CHANGED: { minimum_size_changed(); @@ -602,30 +697,19 @@ 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); } @@ -642,18 +726,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(); @@ -664,21 +743,15 @@ 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 Variant(); + return false; } void Control::drop_data(const Point2 &p_point, const Variant &p_data) { @@ -686,20 +759,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) { @@ -715,45 +780,50 @@ 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> -bool Control::_find_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, T &r_ret, T (Theme::*get_func)(const StringName &, const StringName &) const, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_type) { - // try with custom themes +T Control::get_theme_item_in_types(Control *p_theme_owner, Window *p_theme_owner_window, Theme::DataType p_data_type, const StringName &p_name, List<StringName> p_theme_types) { + ERR_FAIL_COND_V_MSG(p_theme_types.size() == 0, T(), "At least one theme type must be specified."); + + // First, look through each control or window node in the branch, until no valid parent can be found. + // For each control iterate through its inheritance chain and see if p_name exists in any of them. Control *theme_owner = p_theme_owner; Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - StringName class_name = p_type; - - while (class_name != StringName()) { - if (theme_owner && (theme_owner->data.theme.operator->()->*has_func)(p_name, class_name)) { - r_ret = (theme_owner->data.theme.operator->()->*get_func)(p_name, class_name); - return true; + 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.operator->()->*has_func)(p_name, class_name)) { - r_ret = (theme_owner_window->theme.operator->()->*get_func)(p_name, class_name); - return true; + 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); } - - class_name = ClassDB::get_parent_class_nocheck(class_name); } Node *parent = theme_owner ? theme_owner->get_parent() : theme_owner_window->get_parent(); - Control *parent_c = Object::cast_to<Control>(parent); - if (parent_c) { theme_owner = parent_c->data.theme_owner; theme_owner_window = parent_c->data.theme_owner_window; @@ -768,33 +838,47 @@ bool Control::_find_theme_item(Control *p_theme_owner, Window *p_theme_owner_win } } } - return false; + + // Secondly, check the project-defined Theme resource. + if (Theme::get_project_default().is_valid()) { + 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 (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. + return Theme::get_default()->get_theme_item(p_data_type, p_name, p_theme_types[0]); } -bool Control::_has_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_type) { - // try with custom themes +bool Control::has_theme_item_in_types(Control *p_theme_owner, Window *p_theme_owner_window, Theme::DataType p_data_type, const StringName &p_name, List<StringName> p_theme_types) { + ERR_FAIL_COND_V_MSG(p_theme_types.size() == 0, false, "At least one theme type must be specified."); + + // First, look through each control or window node in the branch, until no valid parent can be found. + // For each control iterate through its inheritance chain and see if p_name exists in any of them. Control *theme_owner = p_theme_owner; Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - StringName class_name = p_type; - - while (class_name != StringName()) { - if (theme_owner && (theme_owner->data.theme.operator->()->*has_func)(p_name, class_name)) { + 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.operator->()->*has_func)(p_name, class_name)) { + if (theme_owner_window && theme_owner_window->theme->has_theme_item(p_data_type, p_name, E)) { return true; } - - class_name = ClassDB::get_parent_class_nocheck(class_name); } Node *parent = theme_owner ? theme_owner->get_parent() : theme_owner_window->get_parent(); - Control *parent_c = Object::cast_to<Control>(parent); - if (parent_c) { theme_owner = parent_c->data.theme_owner; theme_owner_window = parent_c->data.theme_owner_window; @@ -809,179 +893,113 @@ bool Control::_has_theme_item(Control *p_theme_owner, Window *p_theme_owner_wind } } } - return false; -} -Ref<Texture2D> Control::get_theme_icon(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { - const Ref<Texture2D> *tex = data.icon_override.getptr(p_name); - if (tex) { - return *tex; + // Secondly, check the project-defined Theme resource. + if (Theme::get_project_default().is_valid()) { + for (const StringName &E : p_theme_types) { + if (Theme::get_project_default()->has_theme_item(p_data_type, p_name, E)) { + return true; + } } } - StringName type = p_type ? p_type : get_class_name(); - - return get_icons(data.theme_owner, data.theme_owner_window, p_name, type); -} - -Ref<Texture2D> Control::get_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - Ref<Texture2D> icon; - - if (_find_theme_item(p_theme_owner, p_theme_owner_window, icon, &Theme::get_icon, &Theme::has_icon, p_name, p_type)) { - return icon; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_icon(p_name, p_type)) { - return Theme::get_project_default()->get_icon(p_name, p_type); + // Lastly, fall back on the items defined in the default Theme, if they exist. + for (const StringName &E : p_theme_types) { + if (Theme::get_default()->has_theme_item(p_data_type, p_name, E)) { + return true; } } - - return Theme::get_default()->get_icon(p_name, p_type); + return false; } -Ref<Shader> Control::get_theme_shader(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { - const Ref<Shader> *sdr = data.shader_override.getptr(p_name); - if (sdr) { - return *sdr; +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_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); } + } else { + Theme::get_default()->get_type_dependencies(p_theme_type, StringName(), p_list); } - - StringName type = p_type ? p_type : get_class_name(); - - return get_shaders(data.theme_owner, data.theme_owner_window, p_name, type); } -Ref<Shader> Control::get_shaders(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - Ref<Shader> shader; - - if (_find_theme_item(p_theme_owner, p_theme_owner_window, shader, &Theme::get_shader, &Theme::has_shader, p_name, p_type)) { - return shader; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_shader(p_name, p_type)) { - return Theme::get_project_default()->get_shader(p_name, p_type); +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_type_variation) { + const Ref<Texture2D> *tex = data.icon_override.getptr(p_name); + if (tex) { + return *tex; } } - return Theme::get_default()->get_shader(p_name, p_type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return get_theme_item_in_types<Ref<Texture2D>>(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_ICON, p_name, theme_types); } -Ref<StyleBox> Control::get_theme_stylebox(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { const Ref<StyleBox> *style = data.style_override.getptr(p_name); if (style) { return *style; } } - StringName type = p_type ? p_type : get_class_name(); - - return get_styleboxs(data.theme_owner, data.theme_owner_window, p_name, type); -} - -Ref<StyleBox> Control::get_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - Ref<StyleBox> stylebox; - - if (_find_theme_item(p_theme_owner, p_theme_owner_window, stylebox, &Theme::get_stylebox, &Theme::has_stylebox, p_name, p_type)) { - return stylebox; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_stylebox(p_name, p_type)) { - return Theme::get_project_default()->get_stylebox(p_name, p_type); - } - } - - return Theme::get_default()->get_stylebox(p_name, p_type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return get_theme_item_in_types<Ref<StyleBox>>(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_STYLEBOX, p_name, theme_types); } -Ref<Font> Control::get_theme_font(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { const Ref<Font> *font = data.font_override.getptr(p_name); if (font) { return *font; } } - StringName type = p_type ? p_type : get_class_name(); - - return get_fonts(data.theme_owner, data.theme_owner_window, p_name, type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return get_theme_item_in_types<Ref<Font>>(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_FONT, p_name, theme_types); } -Ref<Font> Control::get_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - Ref<Font> font; - - if (_find_theme_item(p_theme_owner, p_theme_owner_window, font, &Theme::get_font, &Theme::has_font, p_name, p_type)) { - return font; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_font(p_name, p_type)) { - return Theme::get_project_default()->get_font(p_name, p_type); +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_type_variation) { + const int *font_size = data.font_size_override.getptr(p_name); + if (font_size) { + return *font_size; } } - return Theme::get_default()->get_font(p_name, p_type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return get_theme_item_in_types<int>(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_FONT_SIZE, p_name, theme_types); } -Color Control::get_theme_color(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { const Color *color = data.color_override.getptr(p_name); if (color) { return *color; } } - StringName type = p_type ? p_type : get_class_name(); - - return get_colors(data.theme_owner, data.theme_owner_window, p_name, type); -} - -Color Control::get_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - Color color; - - if (_find_theme_item(p_theme_owner, p_theme_owner_window, color, &Theme::get_color, &Theme::has_color, p_name, p_type)) { - return color; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_color(p_name, p_type)) { - return Theme::get_project_default()->get_color(p_name, p_type); - } - } - return Theme::get_default()->get_color(p_name, p_type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return get_theme_item_in_types<Color>(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_COLOR, p_name, theme_types); } -int Control::get_theme_constant(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { const int *constant = data.constant_override.getptr(p_name); if (constant) { return *constant; } } - StringName type = p_type ? p_type : get_class_name(); - - return get_constants(data.theme_owner, data.theme_owner_window, p_name, type); -} - -int Control::get_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - int constant; - - if (_find_theme_item(p_theme_owner, p_theme_owner_window, constant, &Theme::get_constant, &Theme::has_constant, p_name, p_type)) { - return constant; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_constant(p_name, p_type)) { - return Theme::get_project_default()->get_constant(p_name, p_type); - } - } - return Theme::get_default()->get_constant(p_name, p_type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return get_theme_item_in_types<int>(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_CONSTANT, p_name, theme_types); } bool Control::has_theme_icon_override(const StringName &p_name) const { @@ -989,11 +1007,6 @@ bool Control::has_theme_icon_override(const StringName &p_name) const { return tex != nullptr; } -bool Control::has_theme_shader_override(const StringName &p_name) const { - const Ref<Shader> *sdr = data.shader_override.getptr(p_name); - return sdr != nullptr; -} - bool Control::has_theme_stylebox_override(const StringName &p_name) const { const Ref<StyleBox> *style = data.style_override.getptr(p_name); return style != nullptr; @@ -1004,6 +1017,11 @@ bool Control::has_theme_font_override(const StringName &p_name) const { return font != nullptr; } +bool Control::has_theme_font_size_override(const StringName &p_name) const { + const int *font_size = data.font_size_override.getptr(p_name); + return font_size != nullptr; +} + bool Control::has_theme_color_override(const StringName &p_name) const { const Color *color = data.color_override.getptr(p_name); return color != nullptr; @@ -1014,154 +1032,76 @@ bool Control::has_theme_constant_override(const StringName &p_name) const { return constant != nullptr; } -bool Control::has_theme_icon(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { if (has_theme_icon_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); - - return has_icons(data.theme_owner, data.theme_owner_window, p_name, type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return has_theme_item_in_types(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_ICON, p_name, theme_types); } -bool Control::has_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_icon, p_name, p_type)) { - return true; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_color(p_name, p_type)) { - return true; - } - } - return Theme::get_default()->has_icon(p_name, p_type); -} - -bool Control::has_theme_shader(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { - if (has_theme_shader_override(p_name)) { - return true; - } - } - - StringName type = p_type ? p_type : get_class_name(); - - return has_shaders(data.theme_owner, data.theme_owner_window, p_name, type); -} - -bool Control::has_shaders(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_shader, p_name, p_type)) { - return true; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_shader(p_name, p_type)) { - return true; - } - } - return Theme::get_default()->has_shader(p_name, p_type); -} - -bool Control::has_theme_stylebox(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { if (has_theme_stylebox_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); - - return has_styleboxs(data.theme_owner, data.theme_owner_window, p_name, type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return has_theme_item_in_types(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_STYLEBOX, p_name, theme_types); } -bool Control::has_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_stylebox, p_name, p_type)) { - return true; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_stylebox(p_name, p_type)) { - return true; - } - } - return Theme::get_default()->has_stylebox(p_name, p_type); -} - -bool Control::has_theme_font(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { if (has_theme_font_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); - - return has_fonts(data.theme_owner, data.theme_owner_window, p_name, type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return has_theme_item_in_types(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_FONT, p_name, theme_types); } -bool Control::has_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_font, p_name, p_type)) { - return true; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_font(p_name, p_type)) { +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_type_variation) { + if (has_theme_font_size_override(p_name)) { return true; } } - return Theme::get_default()->has_font(p_name, p_type); + + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return has_theme_item_in_types(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_FONT_SIZE, p_name, theme_types); } -bool Control::has_theme_color(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { if (has_theme_color_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); - - return has_colors(data.theme_owner, data.theme_owner_window, p_name, type); -} - -bool Control::has_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_color, p_name, p_type)) { - return true; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_color(p_name, p_type)) { - return true; - } - } - return Theme::get_default()->has_color(p_name, p_type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return has_theme_item_in_types(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_COLOR, p_name, theme_types); } -bool Control::has_theme_constant(const StringName &p_name, const StringName &p_type) const { - if (p_type == StringName() || p_type == get_class_name()) { +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_type_variation) { if (has_theme_constant_override(p_name)) { return true; } } - StringName type = p_type ? p_type : get_class_name(); - - return has_constants(data.theme_owner, data.theme_owner_window, p_name, p_type); -} - -bool Control::has_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type) { - if (_has_theme_item(p_theme_owner, p_theme_owner_window, &Theme::has_constant, p_name, p_type)) { - return true; - } - - if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_constant(p_name, p_type)) { - return true; - } - } - return Theme::get_default()->has_constant(p_name, p_type); + List<StringName> theme_types; + _get_theme_type_dependencies(p_theme_type, &theme_types); + return has_theme_item_in_types(data.theme_owner, data.theme_owner_window, Theme::DATA_TYPE_CONSTANT, p_name, theme_types); } Rect2 Control::get_parent_anchorable_rect() const { @@ -1175,7 +1115,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(); @@ -1196,15 +1136,15 @@ Size2 Control::get_parent_area_size() const { void Control::_size_changed() { Rect2 parent_rect = get_parent_anchorable_rect(); - float margin_pos[4]; + real_t edge_pos[4]; for (int i = 0; i < 4; i++) { - float area = parent_rect.size[i & 1]; - margin_pos[i] = data.margin[i] + (data.anchor[i] * area); + real_t area = parent_rect.size[i & 1]; + edge_pos[i] = data.offset[i] + (data.anchor[i] * area); } - Point2 new_pos_cache = Point2(margin_pos[0], margin_pos[1]); - Size2 new_size_cache = Point2(margin_pos[2], margin_pos[3]) - new_pos_cache; + Point2 new_pos_cache = Point2(edge_pos[0], edge_pos[1]); + Size2 new_size_cache = Point2(edge_pos[2], edge_pos[3]) - new_pos_cache; Size2 minimum_size = get_combined_minimum_size(); @@ -1218,6 +1158,10 @@ void Control::_size_changed() { new_size_cache.width = minimum_size.width; } + if (is_layout_rtl()) { + new_pos_cache.x = parent_rect.size.x - new_pos_cache.x - new_size_cache.x; + } + if (minimum_size.height > new_size_cache.height) { if (data.v_grow == GROW_DIRECTION_BEGIN) { new_pos_cache.y += new_size_cache.height - minimum_size.height; @@ -1240,7 +1184,6 @@ void Control::_size_changed() { } if (pos_changed || size_changed) { item_rect_changed(size_changed); - _change_notify_margins(); _notify_transform(); } @@ -1250,29 +1193,29 @@ void Control::_size_changed() { } } -void Control::set_anchor(Margin p_margin, float p_anchor, bool p_keep_margin, bool p_push_opposite_anchor) { - ERR_FAIL_INDEX((int)p_margin, 4); +void Control::set_anchor(Side p_side, real_t p_anchor, bool p_keep_offset, bool p_push_opposite_anchor) { + ERR_FAIL_INDEX((int)p_side, 4); Rect2 parent_rect = get_parent_anchorable_rect(); - float parent_range = (p_margin == MARGIN_LEFT || p_margin == MARGIN_RIGHT) ? parent_rect.size.x : parent_rect.size.y; - float previous_margin_pos = data.margin[p_margin] + data.anchor[p_margin] * parent_range; - float previous_opposite_margin_pos = data.margin[(p_margin + 2) % 4] + data.anchor[(p_margin + 2) % 4] * parent_range; + real_t parent_range = (p_side == SIDE_LEFT || p_side == SIDE_RIGHT) ? parent_rect.size.x : parent_rect.size.y; + real_t previous_pos = data.offset[p_side] + data.anchor[p_side] * parent_range; + real_t previous_opposite_pos = data.offset[(p_side + 2) % 4] + data.anchor[(p_side + 2) % 4] * parent_range; - data.anchor[p_margin] = p_anchor; + data.anchor[p_side] = p_anchor; - if (((p_margin == MARGIN_LEFT || p_margin == MARGIN_TOP) && data.anchor[p_margin] > data.anchor[(p_margin + 2) % 4]) || - ((p_margin == MARGIN_RIGHT || p_margin == MARGIN_BOTTOM) && data.anchor[p_margin] < data.anchor[(p_margin + 2) % 4])) { + if (((p_side == SIDE_LEFT || p_side == SIDE_TOP) && data.anchor[p_side] > data.anchor[(p_side + 2) % 4]) || + ((p_side == SIDE_RIGHT || p_side == SIDE_BOTTOM) && data.anchor[p_side] < data.anchor[(p_side + 2) % 4])) { if (p_push_opposite_anchor) { - data.anchor[(p_margin + 2) % 4] = data.anchor[p_margin]; + data.anchor[(p_side + 2) % 4] = data.anchor[p_side]; } else { - data.anchor[p_margin] = data.anchor[(p_margin + 2) % 4]; + data.anchor[p_side] = data.anchor[(p_side + 2) % 4]; } } - if (!p_keep_margin) { - data.margin[p_margin] = previous_margin_pos - data.anchor[p_margin] * parent_range; + if (!p_keep_offset) { + data.offset[p_side] = previous_pos - data.anchor[p_side] * parent_range; if (p_push_opposite_anchor) { - data.margin[(p_margin + 2) % 4] = previous_opposite_margin_pos - data.anchor[(p_margin + 2) % 4] * parent_range; + data.offset[(p_side + 2) % 4] = previous_opposite_pos - data.anchor[(p_side + 2) % 4] * parent_range; } } if (is_inside_tree()) { @@ -1280,22 +1223,18 @@ void Control::set_anchor(Margin p_margin, float p_anchor, bool p_keep_margin, bo } update(); - _change_notify("anchor_left"); - _change_notify("anchor_right"); - _change_notify("anchor_top"); - _change_notify("anchor_bottom"); } -void Control::_set_anchor(Margin p_margin, float p_anchor) { - set_anchor(p_margin, p_anchor); +void Control::_set_anchor(Side p_side, real_t p_anchor) { + set_anchor(p_side, p_anchor); } -void Control::set_anchor_and_margin(Margin p_margin, float p_anchor, float p_pos, bool p_push_opposite_anchor) { - set_anchor(p_margin, p_anchor, false, p_push_opposite_anchor); - set_margin(p_margin, p_pos); +void Control::set_anchor_and_offset(Side p_side, real_t p_anchor, real_t p_pos, bool p_push_opposite_anchor) { + set_anchor(p_side, p_anchor, false, p_push_opposite_anchor); + set_offset(p_side, p_pos); } -void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { +void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_offsets) { ERR_FAIL_INDEX((int)p_preset, 16); //Left @@ -1308,21 +1247,21 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_LEFT_WIDE: case PRESET_HCENTER_WIDE: case PRESET_WIDE: - set_anchor(MARGIN_LEFT, ANCHOR_BEGIN, p_keep_margins); + set_anchor(SIDE_LEFT, ANCHOR_BEGIN, p_keep_offsets); break; case PRESET_CENTER_TOP: case PRESET_CENTER_BOTTOM: case PRESET_CENTER: case PRESET_VCENTER_WIDE: - set_anchor(MARGIN_LEFT, 0.5, p_keep_margins); + set_anchor(SIDE_LEFT, 0.5, p_keep_offsets); break; case PRESET_TOP_RIGHT: case PRESET_BOTTOM_RIGHT: case PRESET_CENTER_RIGHT: case PRESET_RIGHT_WIDE: - set_anchor(MARGIN_LEFT, ANCHOR_END, p_keep_margins); + set_anchor(SIDE_LEFT, ANCHOR_END, p_keep_offsets); break; } @@ -1336,21 +1275,21 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_TOP_WIDE: case PRESET_VCENTER_WIDE: case PRESET_WIDE: - set_anchor(MARGIN_TOP, ANCHOR_BEGIN, p_keep_margins); + set_anchor(SIDE_TOP, ANCHOR_BEGIN, p_keep_offsets); break; case PRESET_CENTER_LEFT: case PRESET_CENTER_RIGHT: case PRESET_CENTER: case PRESET_HCENTER_WIDE: - set_anchor(MARGIN_TOP, 0.5, p_keep_margins); + set_anchor(SIDE_TOP, 0.5, p_keep_offsets); break; case PRESET_BOTTOM_LEFT: case PRESET_BOTTOM_RIGHT: case PRESET_CENTER_BOTTOM: case PRESET_BOTTOM_WIDE: - set_anchor(MARGIN_TOP, ANCHOR_END, p_keep_margins); + set_anchor(SIDE_TOP, ANCHOR_END, p_keep_offsets); break; } @@ -1360,14 +1299,14 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_BOTTOM_LEFT: case PRESET_CENTER_LEFT: case PRESET_LEFT_WIDE: - set_anchor(MARGIN_RIGHT, ANCHOR_BEGIN, p_keep_margins); + set_anchor(SIDE_RIGHT, ANCHOR_BEGIN, p_keep_offsets); break; case PRESET_CENTER_TOP: case PRESET_CENTER_BOTTOM: case PRESET_CENTER: case PRESET_VCENTER_WIDE: - set_anchor(MARGIN_RIGHT, 0.5, p_keep_margins); + set_anchor(SIDE_RIGHT, 0.5, p_keep_offsets); break; case PRESET_TOP_RIGHT: @@ -1378,7 +1317,7 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_BOTTOM_WIDE: case PRESET_HCENTER_WIDE: case PRESET_WIDE: - set_anchor(MARGIN_RIGHT, ANCHOR_END, p_keep_margins); + set_anchor(SIDE_RIGHT, ANCHOR_END, p_keep_offsets); break; } @@ -1388,14 +1327,14 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_TOP_RIGHT: case PRESET_CENTER_TOP: case PRESET_TOP_WIDE: - set_anchor(MARGIN_BOTTOM, ANCHOR_BEGIN, p_keep_margins); + set_anchor(SIDE_BOTTOM, ANCHOR_BEGIN, p_keep_offsets); break; case PRESET_CENTER_LEFT: case PRESET_CENTER_RIGHT: case PRESET_CENTER: case PRESET_HCENTER_WIDE: - set_anchor(MARGIN_BOTTOM, 0.5, p_keep_margins); + set_anchor(SIDE_BOTTOM, 0.5, p_keep_offsets); break; case PRESET_BOTTOM_LEFT: @@ -1406,12 +1345,12 @@ void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_margins) { case PRESET_BOTTOM_WIDE: case PRESET_VCENTER_WIDE: case PRESET_WIDE: - set_anchor(MARGIN_BOTTOM, ANCHOR_END, p_keep_margins); + set_anchor(SIDE_BOTTOM, ANCHOR_END, p_keep_offsets); break; } } -void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode, int p_margin) { +void Control::set_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode, int p_margin) { ERR_FAIL_INDEX((int)p_preset, 16); ERR_FAIL_INDEX((int)p_resize_mode, 4); @@ -1427,6 +1366,10 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz Rect2 parent_rect = get_parent_anchorable_rect(); + real_t x = parent_rect.size.x; + if (is_layout_rtl()) { + x = parent_rect.size.x - x - new_size.x; + } //Left switch (p_preset) { case PRESET_TOP_LEFT: @@ -1437,21 +1380,21 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_LEFT_WIDE: case PRESET_HCENTER_WIDE: case PRESET_WIDE: - data.margin[0] = parent_rect.size.x * (0.0 - data.anchor[0]) + p_margin + parent_rect.position.x; + data.offset[0] = x * (0.0 - data.anchor[0]) + p_margin + parent_rect.position.x; break; case PRESET_CENTER_TOP: case PRESET_CENTER_BOTTOM: case PRESET_CENTER: case PRESET_VCENTER_WIDE: - data.margin[0] = parent_rect.size.x * (0.5 - data.anchor[0]) - new_size.x / 2 + parent_rect.position.x; + data.offset[0] = x * (0.5 - data.anchor[0]) - new_size.x / 2 + parent_rect.position.x; break; case PRESET_TOP_RIGHT: case PRESET_BOTTOM_RIGHT: case PRESET_CENTER_RIGHT: case PRESET_RIGHT_WIDE: - data.margin[0] = parent_rect.size.x * (1.0 - data.anchor[0]) - new_size.x - p_margin + parent_rect.position.x; + data.offset[0] = x * (1.0 - data.anchor[0]) - new_size.x - p_margin + parent_rect.position.x; break; } @@ -1465,21 +1408,21 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_TOP_WIDE: case PRESET_VCENTER_WIDE: case PRESET_WIDE: - data.margin[1] = parent_rect.size.y * (0.0 - data.anchor[1]) + p_margin + parent_rect.position.y; + data.offset[1] = parent_rect.size.y * (0.0 - data.anchor[1]) + p_margin + parent_rect.position.y; break; case PRESET_CENTER_LEFT: case PRESET_CENTER_RIGHT: case PRESET_CENTER: case PRESET_HCENTER_WIDE: - data.margin[1] = parent_rect.size.y * (0.5 - data.anchor[1]) - new_size.y / 2 + parent_rect.position.y; + data.offset[1] = parent_rect.size.y * (0.5 - data.anchor[1]) - new_size.y / 2 + parent_rect.position.y; break; case PRESET_BOTTOM_LEFT: case PRESET_BOTTOM_RIGHT: case PRESET_CENTER_BOTTOM: case PRESET_BOTTOM_WIDE: - data.margin[1] = parent_rect.size.y * (1.0 - data.anchor[1]) - new_size.y - p_margin + parent_rect.position.y; + data.offset[1] = parent_rect.size.y * (1.0 - data.anchor[1]) - new_size.y - p_margin + parent_rect.position.y; break; } @@ -1489,14 +1432,14 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_BOTTOM_LEFT: case PRESET_CENTER_LEFT: case PRESET_LEFT_WIDE: - data.margin[2] = parent_rect.size.x * (0.0 - data.anchor[2]) + new_size.x + p_margin + parent_rect.position.x; + data.offset[2] = x * (0.0 - data.anchor[2]) + new_size.x + p_margin + parent_rect.position.x; break; case PRESET_CENTER_TOP: case PRESET_CENTER_BOTTOM: case PRESET_CENTER: case PRESET_VCENTER_WIDE: - data.margin[2] = parent_rect.size.x * (0.5 - data.anchor[2]) + new_size.x / 2 + parent_rect.position.x; + data.offset[2] = x * (0.5 - data.anchor[2]) + new_size.x / 2 + parent_rect.position.x; break; case PRESET_TOP_RIGHT: @@ -1507,7 +1450,7 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_BOTTOM_WIDE: case PRESET_HCENTER_WIDE: case PRESET_WIDE: - data.margin[2] = parent_rect.size.x * (1.0 - data.anchor[2]) - p_margin + parent_rect.position.x; + data.offset[2] = x * (1.0 - data.anchor[2]) - p_margin + parent_rect.position.x; break; } @@ -1517,14 +1460,14 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_TOP_RIGHT: case PRESET_CENTER_TOP: case PRESET_TOP_WIDE: - data.margin[3] = parent_rect.size.y * (0.0 - data.anchor[3]) + new_size.y + p_margin + parent_rect.position.y; + data.offset[3] = parent_rect.size.y * (0.0 - data.anchor[3]) + new_size.y + p_margin + parent_rect.position.y; break; case PRESET_CENTER_LEFT: case PRESET_CENTER_RIGHT: case PRESET_CENTER: case PRESET_HCENTER_WIDE: - data.margin[3] = parent_rect.size.y * (0.5 - data.anchor[3]) + new_size.y / 2 + parent_rect.position.y; + data.offset[3] = parent_rect.size.y * (0.5 - data.anchor[3]) + new_size.y / 2 + parent_rect.position.y; break; case PRESET_BOTTOM_LEFT: @@ -1535,65 +1478,55 @@ void Control::set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resiz case PRESET_BOTTOM_WIDE: case PRESET_VCENTER_WIDE: case PRESET_WIDE: - data.margin[3] = parent_rect.size.y * (1.0 - data.anchor[3]) - p_margin + parent_rect.position.y; + data.offset[3] = parent_rect.size.y * (1.0 - data.anchor[3]) - p_margin + parent_rect.position.y; break; } _size_changed(); } -void Control::set_anchors_and_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode, int p_margin) { +void Control::set_anchors_and_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode, int p_margin) { set_anchors_preset(p_preset); - set_margins_preset(p_preset, p_resize_mode, p_margin); + set_offsets_preset(p_preset, p_resize_mode, p_margin); } -float Control::get_anchor(Margin p_margin) const { - ERR_FAIL_INDEX_V(int(p_margin), 4, 0.0); - - return data.anchor[p_margin]; -} +real_t Control::get_anchor(Side p_side) const { + ERR_FAIL_INDEX_V(int(p_side), 4, 0.0); -void Control::_change_notify_margins() { - // this avoids sending the whole object data again on a change - _change_notify("margin_left"); - _change_notify("margin_top"); - _change_notify("margin_right"); - _change_notify("margin_bottom"); - _change_notify("rect_position"); - _change_notify("rect_size"); + return data.anchor[p_side]; } -void Control::set_margin(Margin p_margin, float p_value) { - ERR_FAIL_INDEX((int)p_margin, 4); +void Control::set_offset(Side p_side, real_t p_value) { + ERR_FAIL_INDEX((int)p_side, 4); - data.margin[p_margin] = p_value; + data.offset[p_side] = p_value; _size_changed(); } void Control::set_begin(const Size2 &p_point) { - data.margin[0] = p_point.x; - data.margin[1] = p_point.y; + data.offset[0] = p_point.x; + data.offset[1] = p_point.y; _size_changed(); } void Control::set_end(const Size2 &p_point) { - data.margin[2] = p_point.x; - data.margin[3] = p_point.y; + data.offset[2] = p_point.x; + data.offset[3] = p_point.y; _size_changed(); } -float Control::get_margin(Margin p_margin) const { - ERR_FAIL_INDEX_V((int)p_margin, 4, 0); +real_t Control::get_offset(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, 0); - return data.margin[p_margin]; + return data.offset[p_side]; } Size2 Control::get_begin() const { - return Size2(data.margin[0], data.margin[1]); + return Size2(data.offset[0], data.offset[1]); } Size2 Control::get_end() const { - return Size2(data.margin[2], data.margin[3]); + return Size2(data.offset[2], data.offset[3]); } Point2 Control::get_global_position() const { @@ -1602,7 +1535,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(); @@ -1615,57 +1548,78 @@ void Control::_set_global_position(const Point2 &p_point) { set_global_position(p_point); } -void Control::set_global_position(const Point2 &p_point, bool p_keep_margins) { +void Control::set_global_position(const Point2 &p_point, bool p_keep_offsets) { Transform2D inv; if (data.parent_canvas_item) { inv = data.parent_canvas_item->get_global_transform().affine_inverse(); } - set_position(inv.xform(p_point), p_keep_margins); + set_position(inv.xform(p_point), p_keep_offsets); } -void Control::_compute_anchors(Rect2 p_rect, const float p_margins[4], float (&r_anchors)[4]) { +void Control::_compute_anchors(Rect2 p_rect, const real_t p_offsets[4], real_t (&r_anchors)[4]) { Size2 parent_rect_size = get_parent_anchorable_rect().size; ERR_FAIL_COND(parent_rect_size.x == 0.0); ERR_FAIL_COND(parent_rect_size.y == 0.0); - r_anchors[0] = (p_rect.position.x - p_margins[0]) / parent_rect_size.x; - r_anchors[1] = (p_rect.position.y - p_margins[1]) / parent_rect_size.y; - r_anchors[2] = (p_rect.position.x + p_rect.size.x - p_margins[2]) / parent_rect_size.x; - r_anchors[3] = (p_rect.position.y + p_rect.size.y - p_margins[3]) / parent_rect_size.y; + real_t x = p_rect.position.x; + if (is_layout_rtl()) { + x = parent_rect_size.x - x - p_rect.size.x; + } + r_anchors[0] = (x - p_offsets[0]) / parent_rect_size.x; + r_anchors[1] = (p_rect.position.y - p_offsets[1]) / parent_rect_size.y; + r_anchors[2] = (x + p_rect.size.x - p_offsets[2]) / parent_rect_size.x; + r_anchors[3] = (p_rect.position.y + p_rect.size.y - p_offsets[3]) / parent_rect_size.y; } -void Control::_compute_margins(Rect2 p_rect, const float p_anchors[4], float (&r_margins)[4]) { +void Control::_compute_offsets(Rect2 p_rect, const real_t p_anchors[4], real_t (&r_offsets)[4]) { Size2 parent_rect_size = get_parent_anchorable_rect().size; - r_margins[0] = p_rect.position.x - (p_anchors[0] * parent_rect_size.x); - r_margins[1] = p_rect.position.y - (p_anchors[1] * parent_rect_size.y); - r_margins[2] = p_rect.position.x + p_rect.size.x - (p_anchors[2] * parent_rect_size.x); - r_margins[3] = p_rect.position.y + p_rect.size.y - (p_anchors[3] * parent_rect_size.y); + + real_t x = p_rect.position.x; + if (is_layout_rtl()) { + x = parent_rect_size.x - x - p_rect.size.x; + } + r_offsets[0] = x - (p_anchors[0] * parent_rect_size.x); + r_offsets[1] = p_rect.position.y - (p_anchors[1] * parent_rect_size.y); + r_offsets[2] = x + p_rect.size.x - (p_anchors[2] * parent_rect_size.x); + r_offsets[3] = p_rect.position.y + p_rect.size.y - (p_anchors[3] * parent_rect_size.y); } void Control::_set_position(const Size2 &p_point) { set_position(p_point); } -void Control::set_position(const Size2 &p_point, bool p_keep_margins) { - if (p_keep_margins) { - _compute_anchors(Rect2(p_point, data.size_cache), data.margin, data.anchor); - _change_notify("anchor_left"); - _change_notify("anchor_right"); - _change_notify("anchor_top"); - _change_notify("anchor_bottom"); +void Control::set_position(const Size2 &p_point, bool p_keep_offsets) { + if (p_keep_offsets) { + _compute_anchors(Rect2(p_point, data.size_cache), data.offset, data.anchor); } else { - _compute_margins(Rect2(p_point, data.size_cache), data.anchor, data.margin); + _compute_offsets(Rect2(p_point, data.size_cache), data.anchor, data.offset); } _size_changed(); } +void Control::set_rect(const Rect2 &p_rect) { + for (int i = 0; i < 4; i++) { + data.anchor[i] = ANCHOR_BEGIN; + } + + _compute_offsets(p_rect, data.anchor, data.offset); + if (is_inside_tree()) { + _size_changed(); + } +} + void Control::_set_size(const Size2 &p_size) { +#ifdef DEBUG_ENABLED + 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); } -void Control::set_size(const Size2 &p_size, bool p_keep_margins) { +void Control::set_size(const Size2 &p_size, bool p_keep_offsets) { Size2 new_size = p_size; Size2 min = get_combined_minimum_size(); if (new_size.x < min.x) { @@ -1675,14 +1629,10 @@ void Control::set_size(const Size2 &p_size, bool p_keep_margins) { new_size.y = min.y; } - if (p_keep_margins) { - _compute_anchors(Rect2(data.pos_cache, new_size), data.margin, data.anchor); - _change_notify("anchor_left"); - _change_notify("anchor_right"); - _change_notify("anchor_top"); - _change_notify("anchor_bottom"); + if (p_keep_offsets) { + _compute_anchors(Rect2(data.pos_cache, new_size), data.offset, data.anchor); } else { - _compute_margins(Rect2(data.pos_cache, new_size), data.anchor, data.margin); + _compute_offsets(Rect2(data.pos_cache, new_size), data.anchor, data.offset); } _size_changed(); } @@ -1727,82 +1677,108 @@ Rect2 Control::get_anchorable_rect() const { return Rect2(Point2(), get_size()); } -void Control::add_theme_icon_override(const StringName &p_name, const Ref<Texture2D> &p_icon) { - if (data.icon_override.has(p_name)) { - data.icon_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); - } +void Control::begin_bulk_theme_override() { + data.bulk_theme_override = true; +} - // clear if "null" is passed instead of a icon - if (p_icon.is_null()) { - data.icon_override.erase(p_name); - } else { - data.icon_override[p_name] = p_icon; - if (data.icon_override[p_name].is_valid()) { - data.icon_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - } - } - notification(NOTIFICATION_THEME_CHANGED); +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_shader_override(const StringName &p_name, const Ref<Shader> &p_shader) { - if (data.shader_override.has(p_name)) { - data.shader_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); - } +void Control::add_theme_icon_override(const StringName &p_name, const Ref<Texture2D> &p_icon) { + ERR_FAIL_COND(!p_icon.is_valid()); - // clear if "null" is passed instead of a shader - if (p_shader.is_null()) { - data.shader_override.erase(p_name); - } else { - data.shader_override[p_name] = p_shader; - if (data.shader_override[p_name].is_valid()) { - data.shader_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - } + if (data.icon_override.has(p_name)) { + data.icon_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } - notification(NOTIFICATION_THEME_CHANGED); + + 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); + _notify_theme_changed(); } void Control::add_theme_style_override(const StringName &p_name, const Ref<StyleBox> &p_style) { + ERR_FAIL_COND(!p_style.is_valid()); + if (data.style_override.has(p_name)) { data.style_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } - // clear if "null" is passed instead of a style - if (p_style.is_null()) { - data.style_override.erase(p_name); - } else { - data.style_override[p_name] = p_style; - if (data.style_override[p_name].is_valid()) { - data.style_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - } - } - notification(NOTIFICATION_THEME_CHANGED); + 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); + _notify_theme_changed(); } void Control::add_theme_font_override(const StringName &p_name, const Ref<Font> &p_font) { + ERR_FAIL_COND(!p_font.is_valid()); + if (data.font_override.has(p_name)) { data.font_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); } - // clear if "null" is passed instead of a font - if (p_font.is_null()) { - data.font_override.erase(p_name); - } else { - data.font_override[p_name] = p_font; - if (data.font_override[p_name].is_valid()) { - data.font_override[p_name]->connect("changed", callable_mp(this, &Control::_override_changed), Vector<Variant>(), CONNECT_REFERENCE_COUNTED); - } - } - notification(NOTIFICATION_THEME_CHANGED); + 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); + _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; + _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) { + if (data.icon_override.has(p_name)) { + data.icon_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); + } + + data.icon_override.erase(p_name); + _notify_theme_changed(); +} + +void Control::remove_theme_style_override(const StringName &p_name) { + if (data.style_override.has(p_name)) { + data.style_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); + } + + data.style_override.erase(p_name); + _notify_theme_changed(); +} + +void Control::remove_theme_font_override(const StringName &p_name) { + if (data.font_override.has(p_name)) { + data.font_override[p_name]->disconnect("changed", callable_mp(this, &Control::_override_changed)); + } + + data.font_override.erase(p_name); + _notify_theme_changed(); +} + +void Control::remove_theme_font_size_override(const StringName &p_name) { + data.font_size_override.erase(p_name); + _notify_theme_changed(); +} + +void Control::remove_theme_color_override(const StringName &p_name) { + data.color_override.erase(p_name); + _notify_theme_changed(); +} + +void Control::remove_theme_constant_override(const StringName &p_name) { + data.constant_override.erase(p_name); + _notify_theme_changed(); } void Control::set_focus_mode(FocusMode p_focus_mode) { @@ -2075,6 +2051,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; @@ -2109,19 +2091,22 @@ void Control::set_theme(const Ref<Theme> &p_theme) { } } -void Control::accept_event() { - if (is_inside_tree()) { - get_viewport()->_gui_accept_event(); - } -} - Ref<Theme> Control::get_theme() const { return data.theme; } +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_type_variation() const { + return data.theme_type_variation; +} + void Control::set_tooltip(const String &p_tooltip) { data.tooltip = p_tooltip; - update_configuration_warning(); + update_configuration_warnings(); } String Control::get_tooltip(const Point2 &p_pos) const { @@ -2129,8 +2114,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; } @@ -2159,14 +2145,14 @@ String Control::_get_tooltip() const { return data.tooltip; } -void Control::set_focus_neighbour(Margin p_margin, const NodePath &p_neighbour) { - ERR_FAIL_INDEX((int)p_margin, 4); - data.focus_neighbour[p_margin] = p_neighbour; +void Control::set_focus_neighbor(Side p_side, const NodePath &p_neighbor) { + ERR_FAIL_INDEX((int)p_side, 4); + data.focus_neighbor[p_side] = p_neighbor; } -NodePath Control::get_focus_neighbour(Margin p_margin) const { - ERR_FAIL_INDEX_V((int)p_margin, 4, NodePath()); - return data.focus_neighbour[p_margin]; +NodePath Control::get_focus_neighbor(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, NodePath()); + return data.focus_neighbor[p_side]; } void Control::set_focus_next(const NodePath &p_next) { @@ -2185,17 +2171,17 @@ NodePath Control::get_focus_previous() const { return data.focus_prev; } -#define MAX_NEIGHBOUR_SEARCH_COUNT 512 +#define MAX_NEIGHBOR_SEARCH_COUNT 512 -Control *Control::_get_focus_neighbour(Margin p_margin, int p_count) { - ERR_FAIL_INDEX_V((int)p_margin, 4, nullptr); +Control *Control::_get_focus_neighbor(Side p_side, int p_count) { + ERR_FAIL_INDEX_V((int)p_side, 4, nullptr); - if (p_count >= MAX_NEIGHBOUR_SEARCH_COUNT) { + if (p_count >= MAX_NEIGHBOR_SEARCH_COUNT) { return nullptr; } - if (!data.focus_neighbour[p_margin].is_empty()) { + if (!data.focus_neighbor[p_side].is_empty()) { Control *c = nullptr; - Node *n = get_node(data.focus_neighbour[p_margin]); + Node *n = get_node(data.focus_neighbor[p_side]); if (n) { c = Object::cast_to<Control>(n); ERR_FAIL_COND_V_MSG(!c, nullptr, "Neighbor focus node is not a control: " + n->get_name() + "."); @@ -2213,11 +2199,11 @@ Control *Control::_get_focus_neighbour(Margin p_margin, int p_count) { return c; } - c = c->_get_focus_neighbour(p_margin, p_count + 1); + c = c->_get_focus_neighbor(p_side, p_count + 1); return c; } - float dist = 1e7; + real_t dist = 1e7; Control *result = nullptr; Point2 points[4]; @@ -2236,12 +2222,12 @@ Control *Control::_get_focus_neighbour(Margin p_margin, int p_count) { Vector2(0, 1) }; - Vector2 vdir = dir[p_margin]; + Vector2 vdir = dir[p_side]; - float maxd = -1e7; + real_t maxd = -1e7; for (int i = 0; i < 4; i++) { - float d = vdir.dot(points[i]); + real_t d = vdir.dot(points[i]); if (d > maxd) { maxd = d; } @@ -2263,12 +2249,12 @@ Control *Control::_get_focus_neighbour(Margin p_margin, int p_count) { return nullptr; } - _window_find_focus_neighbour(vdir, base, points, maxd, dist, &result); + _window_find_focus_neighbor(vdir, base, points, maxd, dist, &result); return result; } -void Control::_window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, float p_min, float &r_closest_dist, Control **r_closest) { +void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, real_t p_min, real_t &r_closest_dist, Control **r_closest) { if (Object::cast_to<Viewport>(p_at)) { return; //bye } @@ -2285,10 +2271,10 @@ void Control::_window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, con points[2] = xform.xform(c->get_size()); points[3] = xform.xform(Point2(0, c->get_size().y)); - float min = 1e7; + real_t min = 1e7; for (int i = 0; i < 4; i++) { - float d = p_dir.dot(points[i]); + real_t d = p_dir.dot(points[i]); if (d < min) { min = d; } @@ -2304,8 +2290,8 @@ void Control::_window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, con Vector2 fb = points[(j + 1) % 4]; Vector2 pa, pb; - float d = Geometry2D::get_closest_points_between_segments(la, lb, fa, fb, pa, pb); - //float d = Geometry2D::get_closest_distance_between_segments(Vector3(la.x,la.y,0),Vector3(lb.x,lb.y,0),Vector3(fa.x,fa.y,0),Vector3(fb.x,fb.y,0)); + real_t d = Geometry2D::get_closest_points_between_segments(la, lb, fa, fb, pa, pb); + //real_t d = Geometry2D::get_closest_distance_between_segments(Vector3(la.x,la.y,0),Vector3(lb.x,lb.y,0),Vector3(fa.x,fa.y,0),Vector3(fb.x,fb.y,0)); if (d < r_closest_dist) { r_closest_dist = d; *r_closest = c; @@ -2321,7 +2307,7 @@ void Control::_window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, con if (childc && childc->data.RI) { continue; //subwindow, ignore } - _window_find_focus_neighbour(p_dir, p_at->get_child(i), p_points, p_min, r_closest_dist, r_closest); + _window_find_focus_neighbor(p_dir, p_at->get_child(i), p_points, p_min, r_closest_dist, r_closest); } } @@ -2345,7 +2331,7 @@ void Control::set_v_size_flags(int p_flags) { emit_signal(SceneStringNames::get_singleton()->size_flags_changed); } -void Control::set_stretch_ratio(float p_ratio) { +void Control::set_stretch_ratio(real_t p_ratio) { if (data.expand == p_ratio) { return; } @@ -2354,7 +2340,7 @@ void Control::set_stretch_ratio(float p_ratio) { emit_signal(SceneStringNames::get_singleton()->size_flags_changed); } -float Control::get_stretch_ratio() const { +real_t Control::get_stretch_ratio() const { return data.expand; } @@ -2406,7 +2392,7 @@ int Control::get_v_size_flags() const { void Control::set_mouse_filter(MouseFilter p_filter) { ERR_FAIL_INDEX(p_filter, 3); data.mouse_filter = p_filter; - update_configuration_warning(); + update_configuration_warnings(); } Control::MouseFilter Control::get_mouse_filter() const { @@ -2437,25 +2423,102 @@ bool Control::is_text_field() const { return false; } -void Control::set_rotation(float p_radians) { +Vector<Vector2i> Control::structured_text_parser(StructuredTextParser p_theme_type, const Array &p_args, const String p_text) const { + Vector<Vector2i> ret; + switch (p_theme_type) { + case STRUCTURED_TEXT_URI: { + int prev = 0; + for (int i = 0; i < p_text.length(); i++) { + if ((p_text[i] == '\\') || (p_text[i] == '/') || (p_text[i] == '.') || (p_text[i] == ':') || (p_text[i] == '&') || (p_text[i] == '=') || (p_text[i] == '@') || (p_text[i] == '?') || (p_text[i] == '#')) { + if (prev != i) { + ret.push_back(Vector2i(prev, i)); + } + ret.push_back(Vector2i(i, i + 1)); + prev = i + 1; + } + } + if (prev != p_text.length()) { + ret.push_back(Vector2i(prev, p_text.length())); + } + } break; + case STRUCTURED_TEXT_FILE: { + int prev = 0; + for (int i = 0; i < p_text.length(); i++) { + if ((p_text[i] == '\\') || (p_text[i] == '/') || (p_text[i] == ':')) { + if (prev != i) { + ret.push_back(Vector2i(prev, i)); + } + ret.push_back(Vector2i(i, i + 1)); + prev = i + 1; + } + } + if (prev != p_text.length()) { + ret.push_back(Vector2i(prev, p_text.length())); + } + } break; + case STRUCTURED_TEXT_EMAIL: { + bool local = true; + int prev = 0; + for (int i = 0; i < p_text.length(); i++) { + if ((p_text[i] == '@') && local) { // Add full "local" as single context. + local = false; + ret.push_back(Vector2i(prev, i)); + ret.push_back(Vector2i(i, i + 1)); + prev = i + 1; + } else if (!local & (p_text[i] == '.')) { // Add each dot separated "domain" part as context. + if (prev != i) { + ret.push_back(Vector2i(prev, i)); + } + ret.push_back(Vector2i(i, i + 1)); + prev = i + 1; + } + } + if (prev != p_text.length()) { + ret.push_back(Vector2i(prev, p_text.length())); + } + } break; + case STRUCTURED_TEXT_LIST: { + if (p_args.size() == 1 && p_args[0].get_type() == Variant::STRING) { + Vector<String> tags = p_text.split(String(p_args[0])); + int prev = 0; + for (int i = 0; i < tags.size(); i++) { + if (prev != i) { + ret.push_back(Vector2i(prev, prev + tags[i].length())); + } + ret.push_back(Vector2i(prev + tags[i].length(), prev + tags[i].length() + 1)); + prev = prev + tags[i].length() + 1; + } + } + } break; + case STRUCTURED_TEXT_CUSTOM: { + 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])); + } + } + } + } break; + case STRUCTURED_TEXT_NONE: + case STRUCTURED_TEXT_DEFAULT: + default: { + ret.push_back(Vector2i(0, p_text.length())); + } + } + return ret; +} + +void Control::set_rotation(real_t p_radians) { data.rotation = p_radians; update(); _notify_transform(); - _change_notify("rect_rotation"); } -float Control::get_rotation() const { +real_t Control::get_rotation() const { return data.rotation; } -void Control::set_rotation_degrees(float p_degrees) { - set_rotation(Math::deg2rad(p_degrees)); -} - -float 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); @@ -2466,7 +2529,6 @@ void Control::set_pivot_offset(const Vector2 &p_pivot) { data.pivot_offset = p_pivot; update(); _notify_transform(); - _change_notify("rect_pivot_offset"); } Vector2 Control::get_pivot_offset() const { @@ -2529,7 +2591,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 @@ -2539,34 +2601,33 @@ void Control::get_argument_options(const StringName &p_function, int p_idx, List if (p_idx == 0) { List<StringName> sn; String pf = p_function; - if (pf == "add_color_override" || pf == "has_color" || pf == "has_color_override" || pf == "get_color") { + if (pf == "add_theme_color_override" || pf == "has_theme_color" || pf == "has_theme_color_override" || pf == "get_theme_color") { Theme::get_default()->get_color_list(get_class(), &sn); - } else if (pf == "add_style_override" || pf == "has_style" || pf == "has_style_override" || pf == "get_style") { + } else if (pf == "add_theme_style_override" || pf == "has_theme_style" || pf == "has_theme_style_override" || pf == "get_theme_style") { Theme::get_default()->get_stylebox_list(get_class(), &sn); - } else if (pf == "add_font_override" || pf == "has_font" || pf == "has_font_override" || pf == "get_font") { + } else if (pf == "add_theme_font_override" || pf == "has_theme_font" || pf == "has_theme_font_override" || pf == "get_theme_font") { Theme::get_default()->get_font_list(get_class(), &sn); - } else if (pf == "add_constant_override" || pf == "has_constant" || pf == "has_constant_override" || pf == "get_constant") { + } else if (pf == "add_theme_font_size_override" || pf == "has_theme_font_size" || pf == "has_theme_font_size_override" || pf == "get_theme_font_size") { + Theme::get_default()->get_font_size_list(get_class(), &sn); + } else if (pf == "add_theme_constant_override" || pf == "has_theme_constant" || pf == "has_theme_constant_override" || pf == "get_theme_constant") { Theme::get_default()->get_constant_list(get_class(), &sn); } 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)); } } } -String Control::get_configuration_warning() const { - String warning = CanvasItem::get_configuration_warning(); +TypedArray<String> Control::get_configuration_warnings() const { + TypedArray<String> warnings = Node::get_configuration_warnings(); if (data.mouse_filter == MOUSE_FILTER_IGNORE && data.tooltip != "") { - if (!warning.empty()) { - warning += "\n\n"; - } - warning += TTR("The Hint Tooltip won't be displayed as the control's Mouse Filter is set to \"Ignore\". To solve this, set the Mouse Filter to \"Stop\" or \"Pass\"."); + warnings.push_back(TTR("The Hint Tooltip won't be displayed as the control's Mouse Filter is set to \"Ignore\". To solve this, set the Mouse Filter to \"Stop\" or \"Pass\".")); } - return warning; + return warnings; } void Control::set_clip_contents(bool p_clip) { @@ -2607,34 +2668,32 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("accept_event"), &Control::accept_event); ClassDB::bind_method(D_METHOD("get_minimum_size"), &Control::get_minimum_size); ClassDB::bind_method(D_METHOD("get_combined_minimum_size"), &Control::get_combined_minimum_size); - ClassDB::bind_method(D_METHOD("set_anchors_preset", "preset", "keep_margins"), &Control::set_anchors_preset, DEFVAL(false)); - ClassDB::bind_method(D_METHOD("set_margins_preset", "preset", "resize_mode", "margin"), &Control::set_margins_preset, DEFVAL(PRESET_MODE_MINSIZE), DEFVAL(0)); - ClassDB::bind_method(D_METHOD("set_anchors_and_margins_preset", "preset", "resize_mode", "margin"), &Control::set_anchors_and_margins_preset, DEFVAL(PRESET_MODE_MINSIZE), DEFVAL(0)); - ClassDB::bind_method(D_METHOD("_set_anchor", "margin", "anchor"), &Control::_set_anchor); - ClassDB::bind_method(D_METHOD("set_anchor", "margin", "anchor", "keep_margin", "push_opposite_anchor"), &Control::set_anchor, DEFVAL(false), DEFVAL(true)); - ClassDB::bind_method(D_METHOD("get_anchor", "margin"), &Control::get_anchor); - ClassDB::bind_method(D_METHOD("set_margin", "margin", "offset"), &Control::set_margin); - ClassDB::bind_method(D_METHOD("set_anchor_and_margin", "margin", "anchor", "offset", "push_opposite_anchor"), &Control::set_anchor_and_margin, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("set_anchors_preset", "preset", "keep_offsets"), &Control::set_anchors_preset, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("set_offsets_preset", "preset", "resize_mode", "margin"), &Control::set_offsets_preset, DEFVAL(PRESET_MODE_MINSIZE), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("set_anchors_and_offsets_preset", "preset", "resize_mode", "margin"), &Control::set_anchors_and_offsets_preset, DEFVAL(PRESET_MODE_MINSIZE), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("_set_anchor", "side", "anchor"), &Control::_set_anchor); + ClassDB::bind_method(D_METHOD("set_anchor", "side", "anchor", "keep_offset", "push_opposite_anchor"), &Control::set_anchor, DEFVAL(false), DEFVAL(true)); + ClassDB::bind_method(D_METHOD("get_anchor", "side"), &Control::get_anchor); + ClassDB::bind_method(D_METHOD("set_offset", "side", "offset"), &Control::set_offset); + ClassDB::bind_method(D_METHOD("set_anchor_and_offset", "side", "anchor", "offset", "push_opposite_anchor"), &Control::set_anchor_and_offset, DEFVAL(false)); ClassDB::bind_method(D_METHOD("set_begin", "position"), &Control::set_begin); ClassDB::bind_method(D_METHOD("set_end", "position"), &Control::set_end); - ClassDB::bind_method(D_METHOD("set_position", "position", "keep_margins"), &Control::set_position, DEFVAL(false)); - ClassDB::bind_method(D_METHOD("_set_position", "margin"), &Control::_set_position); - ClassDB::bind_method(D_METHOD("set_size", "size", "keep_margins"), &Control::set_size, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("set_position", "position", "keep_offsets"), &Control::set_position, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("_set_position", "position"), &Control::_set_position); + ClassDB::bind_method(D_METHOD("set_size", "size", "keep_offsets"), &Control::set_size, DEFVAL(false)); ClassDB::bind_method(D_METHOD("_set_size", "size"), &Control::_set_size); ClassDB::bind_method(D_METHOD("set_custom_minimum_size", "size"), &Control::set_custom_minimum_size); - ClassDB::bind_method(D_METHOD("set_global_position", "position", "keep_margins"), &Control::set_global_position, DEFVAL(false)); + 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_margin", "margin"), &Control::get_margin); + ClassDB::bind_method(D_METHOD("get_offset", "offset"), &Control::get_offset); ClassDB::bind_method(D_METHOD("get_begin"), &Control::get_begin); ClassDB::bind_method(D_METHOD("get_end"), &Control::get_end); 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); @@ -2647,6 +2706,8 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("has_focus"), &Control::has_focus); ClassDB::bind_method(D_METHOD("grab_focus"), &Control::grab_focus); ClassDB::bind_method(D_METHOD("release_focus"), &Control::release_focus); + ClassDB::bind_method(D_METHOD("find_prev_valid_focus"), &Control::find_prev_valid_focus); + ClassDB::bind_method(D_METHOD("find_next_valid_focus"), &Control::find_next_valid_focus); ClassDB::bind_method(D_METHOD("get_focus_owner"), &Control::get_focus_owner); ClassDB::bind_method(D_METHOD("set_h_size_flags", "flags"), &Control::set_h_size_flags); @@ -2661,31 +2722,46 @@ 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_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_shader_override", "name", "shader"), &Control::add_theme_shader_override); ClassDB::bind_method(D_METHOD("add_theme_stylebox_override", "name", "stylebox"), &Control::add_theme_style_override); ClassDB::bind_method(D_METHOD("add_theme_font_override", "name", "font"), &Control::add_theme_font_override); + ClassDB::bind_method(D_METHOD("add_theme_font_size_override", "name", "font_size"), &Control::add_theme_font_size_override); ClassDB::bind_method(D_METHOD("add_theme_color_override", "name", "color"), &Control::add_theme_color_override); ClassDB::bind_method(D_METHOD("add_theme_constant_override", "name", "constant"), &Control::add_theme_constant_override); - ClassDB::bind_method(D_METHOD("get_theme_icon", "name", "type"), &Control::get_theme_icon, DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_theme_stylebox", "name", "type"), &Control::get_theme_stylebox, DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_theme_font", "name", "type"), &Control::get_theme_font, DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_theme_color", "name", "type"), &Control::get_theme_color, DEFVAL("")); - ClassDB::bind_method(D_METHOD("get_theme_constant", "name", "type"), &Control::get_theme_constant, DEFVAL("")); + ClassDB::bind_method(D_METHOD("remove_theme_icon_override", "name"), &Control::remove_theme_icon_override); + ClassDB::bind_method(D_METHOD("remove_theme_stylebox_override", "name"), &Control::remove_theme_style_override); + ClassDB::bind_method(D_METHOD("remove_theme_font_override", "name"), &Control::remove_theme_font_override); + ClassDB::bind_method(D_METHOD("remove_theme_font_size_override", "name"), &Control::remove_theme_font_size_override); + ClassDB::bind_method(D_METHOD("remove_theme_color_override", "name"), &Control::remove_theme_color_override); + ClassDB::bind_method(D_METHOD("remove_theme_constant_override", "name"), &Control::remove_theme_constant_override); + + ClassDB::bind_method(D_METHOD("get_theme_icon", "name", "theme_type"), &Control::get_theme_icon, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_stylebox", "name", "theme_type"), &Control::get_theme_stylebox, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_font", "name", "theme_type"), &Control::get_theme_font, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_font_size", "name", "theme_type"), &Control::get_theme_font_size, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_color", "name", "theme_type"), &Control::get_theme_color, DEFVAL("")); + ClassDB::bind_method(D_METHOD("get_theme_constant", "name", "theme_type"), &Control::get_theme_constant, DEFVAL("")); ClassDB::bind_method(D_METHOD("has_theme_icon_override", "name"), &Control::has_theme_icon_override); - ClassDB::bind_method(D_METHOD("has_theme_shader_override", "name"), &Control::has_theme_shader_override); ClassDB::bind_method(D_METHOD("has_theme_stylebox_override", "name"), &Control::has_theme_stylebox_override); ClassDB::bind_method(D_METHOD("has_theme_font_override", "name"), &Control::has_theme_font_override); + ClassDB::bind_method(D_METHOD("has_theme_font_size_override", "name"), &Control::has_theme_font_size_override); ClassDB::bind_method(D_METHOD("has_theme_color_override", "name"), &Control::has_theme_color_override); ClassDB::bind_method(D_METHOD("has_theme_constant_override", "name"), &Control::has_theme_constant_override); - ClassDB::bind_method(D_METHOD("has_theme_icon", "name", "type"), &Control::has_theme_icon, DEFVAL("")); - ClassDB::bind_method(D_METHOD("has_theme_stylebox", "name", "type"), &Control::has_theme_stylebox, DEFVAL("")); - ClassDB::bind_method(D_METHOD("has_theme_font", "name", "type"), &Control::has_theme_font, DEFVAL("")); - ClassDB::bind_method(D_METHOD("has_theme_color", "name", "type"), &Control::has_theme_color, DEFVAL("")); - ClassDB::bind_method(D_METHOD("has_theme_constant", "name", "type"), &Control::has_theme_constant, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_icon", "name", "theme_type"), &Control::has_theme_icon, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_stylebox", "name", "theme_type"), &Control::has_theme_stylebox, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_font", "name", "theme_type"), &Control::has_theme_font, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_font_size", "name", "theme_type"), &Control::has_theme_font_size, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_color", "name", "theme_type"), &Control::has_theme_color, DEFVAL("")); + ClassDB::bind_method(D_METHOD("has_theme_constant", "name", "theme_type"), &Control::has_theme_constant, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_parent_control"), &Control::get_parent_control); @@ -2703,8 +2779,8 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_default_cursor_shape"), &Control::get_default_cursor_shape); ClassDB::bind_method(D_METHOD("get_cursor_shape", "position"), &Control::get_cursor_shape, DEFVAL(Point2())); - ClassDB::bind_method(D_METHOD("set_focus_neighbour", "margin", "neighbour"), &Control::set_focus_neighbour); - ClassDB::bind_method(D_METHOD("get_focus_neighbour", "margin"), &Control::get_focus_neighbour); + ClassDB::bind_method(D_METHOD("set_focus_neighbor", "side", "neighbor"), &Control::set_focus_neighbor); + ClassDB::bind_method(D_METHOD("get_focus_neighbor", "side"), &Control::get_focus_neighbor); ClassDB::bind_method(D_METHOD("set_focus_next", "next"), &Control::set_focus_next); ClassDB::bind_method(D_METHOD("get_focus_next"), &Control::get_focus_next); @@ -2729,42 +2805,41 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("minimum_size_changed"), &Control::minimum_size_changed); - 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); + ClassDB::bind_method(D_METHOD("set_layout_direction", "direction"), &Control::set_layout_direction); + 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(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", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_top", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_right", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_bottom", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", MARGIN_BOTTOM); + ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_left", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_top", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_right", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::FLOAT, "anchor_bottom", PROPERTY_HINT_RANGE, "0,1,0.001,or_lesser,or_greater"), "_set_anchor", "get_anchor", SIDE_BOTTOM); - ADD_GROUP("Margin", "margin_"); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "margin_left", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "margin_top", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "margin_right", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "margin_bottom", PROPERTY_HINT_RANGE, "-4096,4096"), "set_margin", "get_margin", MARGIN_BOTTOM); + ADD_GROUP("Offset", "offset_"); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_left", PROPERTY_HINT_RANGE, "-4096,4096"), "set_offset", "get_offset", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_top", PROPERTY_HINT_RANGE, "-4096,4096"), "set_offset", "get_offset", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_right", PROPERTY_HINT_RANGE, "-4096,4096"), "set_offset", "get_offset", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_bottom", PROPERTY_HINT_RANGE, "-4096,4096"), "set_offset", "get_offset", SIDE_BOTTOM); ADD_GROUP("Grow Direction", "grow_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "grow_horizontal", PROPERTY_HINT_ENUM, "Begin,End,Both"), "set_h_grow_direction", "get_h_grow_direction"); ADD_PROPERTY(PropertyInfo(Variant::INT, "grow_vertical", PROPERTY_HINT_ENUM, "Begin,End,Both"), "set_v_grow_direction", "get_v_grow_direction"); + 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_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"); @@ -2773,25 +2848,26 @@ void Control::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "hint_tooltip", PROPERTY_HINT_MULTILINE_TEXT), "set_tooltip", "_get_tooltip"); ADD_GROUP("Focus", "focus_"); - ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbour_left", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbour", "get_focus_neighbour", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbour_top", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbour", "get_focus_neighbour", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbour_right", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbour", "get_focus_neighbour", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbour_bottom", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbour", "get_focus_neighbour", MARGIN_BOTTOM); + ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_left", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_top", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_right", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::NODE_PATH, "focus_neighbor_bottom", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_neighbor", "get_focus_neighbor", SIDE_BOTTOM); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "focus_next", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_next", "get_focus_next"); ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "focus_previous", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_previous", "get_focus_previous"); ADD_PROPERTY(PropertyInfo(Variant::INT, "focus_mode", PROPERTY_HINT_ENUM, "None,Click,All"), "set_focus_mode", "get_focus_mode"); ADD_GROUP("Mouse", "mouse_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_filter", PROPERTY_HINT_ENUM, "Stop,Pass,Ignore"), "set_mouse_filter", "get_mouse_filter"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_default_cursor_shape", PROPERTY_HINT_ENUM, "Arrow,Ibeam,Pointing hand,Cross,Wait,Busy,Drag,Can drop,Forbidden,Vertical resize,Horizontal resize,Secondary diagonal resize,Main diagonal resize,Move,Vertical split,Horizontal split,Help"), "set_default_cursor_shape", "get_default_cursor_shape"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_default_cursor_shape", PROPERTY_HINT_ENUM, "Arrow,I-Beam,Pointing Hand,Cross,Wait,Busy,Drag,Can Drop,Forbidden,Vertical Resize,Horizontal Resize,Secondary Diagonal Resize,Main Diagonal Resize,Move,Vertical Split,Horizontal Split,Help"), "set_default_cursor_shape", "get_default_cursor_shape"); ADD_GROUP("Size Flags", "size_flags_"); 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", ""); + + ADD_GROUP("Theme", "theme_"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "theme", PROPERTY_HINT_RESOURCE_TYPE, "Theme"), "set_theme", "get_theme"); - 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); @@ -2805,6 +2881,7 @@ void Control::_bind_methods() { BIND_CONSTANT(NOTIFICATION_THEME_CHANGED); BIND_CONSTANT(NOTIFICATION_SCROLL_BEGIN); BIND_CONSTANT(NOTIFICATION_SCROLL_END); + BIND_CONSTANT(NOTIFICATION_LAYOUT_DIRECTION_CHANGED); BIND_ENUM_CONSTANT(CURSOR_ARROW); BIND_ENUM_CONSTANT(CURSOR_IBEAM); @@ -2863,6 +2940,24 @@ void Control::_bind_methods() { BIND_ENUM_CONSTANT(ANCHOR_BEGIN); BIND_ENUM_CONSTANT(ANCHOR_END); + BIND_ENUM_CONSTANT(LAYOUT_DIRECTION_INHERITED); + BIND_ENUM_CONSTANT(LAYOUT_DIRECTION_LOCALE); + BIND_ENUM_CONSTANT(LAYOUT_DIRECTION_LTR); + BIND_ENUM_CONSTANT(LAYOUT_DIRECTION_RTL); + + BIND_ENUM_CONSTANT(TEXT_DIRECTION_INHERITED); + BIND_ENUM_CONSTANT(TEXT_DIRECTION_AUTO); + BIND_ENUM_CONSTANT(TEXT_DIRECTION_LTR); + BIND_ENUM_CONSTANT(TEXT_DIRECTION_RTL); + + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_DEFAULT); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_URI); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_FILE); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_EMAIL); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_LIST); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_NONE); + BIND_ENUM_CONSTANT(STRUCTURED_TEXT_CUSTOM); + ADD_SIGNAL(MethodInfo("resized")); ADD_SIGNAL(MethodInfo("gui_input", PropertyInfo(Variant::OBJECT, "event", PROPERTY_HINT_RESOURCE_TYPE, "InputEvent"))); ADD_SIGNAL(MethodInfo("mouse_entered")); @@ -2873,39 +2968,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"))); -} - -Control::Control() { - data.parent = nullptr; - - data.mouse_filter = MOUSE_FILTER_STOP; - - data.RI = nullptr; - data.theme_owner = nullptr; - data.theme_owner_window = nullptr; - data.default_cursor = CURSOR_ARROW; - data.h_size_flags = SIZE_FILL; - data.v_size_flags = SIZE_FILL; - data.expand = 1; - data.rotation = 0; - data.parent_canvas_item = nullptr; - data.scale = Vector2(1, 1); + GDVIRTUAL_BIND(_has_point, "position"); + GDVIRTUAL_BIND(_structured_text_parser, "args", "text"); + GDVIRTUAL_BIND(_get_minimum_size); - data.block_minimum_size_adjust = false; - data.disable_visibility_clip = false; - data.h_grow = GROW_DIRECTION_END; - data.v_grow = GROW_DIRECTION_END; - data.minimum_size_valid = false; - data.updating_last_minimum_size = false; - - data.clip_contents = false; - for (int i = 0; i < 4; i++) { - data.anchor[i] = ANCHOR_BEGIN; - data.margin[i] = 0; - } - data.focus_mode = FOCUS_NONE; -} + 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"); -Control::~Control() { + GDVIRTUAL_BIND(_gui_input, "event"); } diff --git a/scene/gui/control.h b/scene/gui/control.h index f2f558cf4f..0faa617f8d 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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" @@ -49,7 +50,6 @@ class Control : public CanvasItem { public: enum Anchor { - ANCHOR_BEGIN = 0, ANCHOR_END = 1 }; @@ -67,7 +67,6 @@ public: }; enum SizeFlags { - SIZE_FILL = 1, SIZE_EXPAND = 2, SIZE_EXPAND_FILL = SIZE_EXPAND | SIZE_FILL, @@ -129,6 +128,30 @@ public: PRESET_MODE_KEEP_SIZE }; + enum LayoutDirection { + LAYOUT_DIRECTION_INHERITED, + LAYOUT_DIRECTION_LOCALE, + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL + }; + + enum TextDirection { + TEXT_DIRECTION_AUTO = TextServer::DIRECTION_AUTO, + TEXT_DIRECTION_LTR = TextServer::DIRECTION_LTR, + TEXT_DIRECTION_RTL = TextServer::DIRECTION_RTL, + TEXT_DIRECTION_INHERITED, + }; + + enum StructuredTextParser { + STRUCTURED_TEXT_DEFAULT, + STRUCTURED_TEXT_URI, + STRUCTURED_TEXT_FILE, + STRUCTURED_TEXT_EMAIL, + STRUCTURED_TEXT_LIST, + STRUCTURED_TEXT_NONE, + STRUCTURED_TEXT_CUSTOM + }; + private: struct CComparator { bool operator()(const Control *p_a, const Control *p_b) const { @@ -144,55 +167,64 @@ private: Point2 pos_cache; Size2 size_cache; Size2 minimum_size_cache; - bool minimum_size_valid; + bool minimum_size_valid = false; Size2 last_minimum_size; - bool updating_last_minimum_size; + bool updating_last_minimum_size = false; - float margin[4]; - float anchor[4]; - FocusMode focus_mode; - GrowDirection h_grow; - GrowDirection v_grow; + real_t offset[4] = { 0.0, 0.0, 0.0, 0.0 }; + real_t anchor[4] = { ANCHOR_BEGIN, ANCHOR_BEGIN, ANCHOR_BEGIN, ANCHOR_BEGIN }; + FocusMode focus_mode = FOCUS_NONE; + GrowDirection h_grow = GROW_DIRECTION_END; + GrowDirection v_grow = GROW_DIRECTION_END; - float rotation; - Vector2 scale; - Vector2 pivot_offset; + LayoutDirection layout_dir = LAYOUT_DIRECTION_INHERITED; + bool is_rtl_dirty = true; + bool is_rtl = false; + + bool auto_translate = true; - bool pending_resize; + real_t rotation = 0.0; + Vector2 scale = Vector2(1, 1); + Vector2 pivot_offset; + bool size_warning = true; - int h_size_flags; - int v_size_flags; - float expand; + int h_size_flags = SIZE_FILL; + int v_size_flags = SIZE_FILL; + real_t expand = 1.0; Point2 custom_minimum_size; - MouseFilter mouse_filter; + MouseFilter mouse_filter = MOUSE_FILTER_STOP; - bool clip_contents; + bool clip_contents = false; - bool block_minimum_size_adjust; - bool disable_visibility_clip; + bool block_minimum_size_adjust = false; + bool disable_visibility_clip = false; - Control *parent; + Control *parent = nullptr; ObjectID drag_owner; Ref<Theme> theme; - Control *theme_owner; - Window *theme_owner_window; + Control *theme_owner = nullptr; + Window *theme_owner_window = nullptr; + Window *parent_window = nullptr; + StringName theme_type_variation; + String tooltip; - CursorShape default_cursor; + CursorShape default_cursor = CURSOR_ARROW; - List<Control *>::Element *RI; + List<Control *>::Element *RI = nullptr; - CanvasItem *parent_canvas_item; + CanvasItem *parent_canvas_item = nullptr; - NodePath focus_neighbour[4]; + NodePath focus_neighbor[4]; NodePath focus_next; NodePath focus_prev; + bool bulk_theme_override = false; HashMap<StringName, Ref<Texture2D>> icon_override; - HashMap<StringName, Ref<Shader>> shader_override; HashMap<StringName, Ref<StyleBox>> style_override; HashMap<StringName, Ref<Font>> font_override; + HashMap<StringName, int> font_size_override; HashMap<StringName, Color> color_override; HashMap<StringName, int> constant_override; @@ -201,24 +233,24 @@ private: // used internally Control *_find_control_at_pos(CanvasItem *p_node, const Point2 &p_pos, const Transform2D &p_xform, Transform2D &r_inv_xform); - void _window_find_focus_neighbour(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, float p_min, float &r_closest_dist, Control **r_closest); - Control *_get_focus_neighbour(Margin p_margin, int p_count = 0); + void _window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, const Point2 *p_points, real_t p_min, real_t &r_closest_dist, Control **r_closest); + Control *_get_focus_neighbor(Side p_side, int p_count = 0); - void _set_anchor(Margin p_margin, float p_anchor); + void _set_anchor(Side p_side, real_t p_anchor); void _set_position(const Point2 &p_point); void _set_global_position(const Point2 &p_point); void _set_size(const Size2 &p_size); void _theme_changed(); + void _notify_theme_changed(); - void _change_notify_margins(); void _update_minimum_size(); + void _clear_size_warning(); void _update_scroll(); - void _resize(const Size2 &p_size); - void _compute_margins(Rect2 p_rect, const float p_anchors[4], float (&r_margins)[4]); - void _compute_anchors(Rect2 p_rect, const float p_margins[4], float (&r_anchors)[4]); + void _compute_offsets(Rect2 p_rect, const real_t p_anchors[4], real_t (&r_offsets)[4]); + void _compute_anchors(Rect2 p_rect, const real_t p_offsets[4], real_t (&r_anchors)[4]); void _size_changed(); String _get_tooltip() const; @@ -231,28 +263,16 @@ 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); template <class T> - _FORCE_INLINE_ static bool _find_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, T &, T (Theme::*get_func)(const StringName &, const StringName &) const, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_type); - - _FORCE_INLINE_ static bool _has_theme_item(Control *p_theme_owner, Window *p_theme_owner_window, bool (Theme::*has_func)(const StringName &, const StringName &) const, const StringName &p_name, const StringName &p_type); - - static Ref<Texture2D> get_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static Ref<Shader> get_shaders(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static Ref<StyleBox> get_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static Ref<Font> get_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static Color get_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static int get_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - - static bool has_icons(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_shaders(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_styleboxs(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_fonts(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_colors(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); - static bool has_constants(Control *p_theme_owner, Window *p_theme_owner_window, const StringName &p_name, const StringName &p_type = StringName()); + static T get_theme_item_in_types(Control *p_theme_owner, Window *p_theme_owner_window, Theme::DataType p_data_type, const StringName &p_name, List<StringName> p_theme_types); + static bool has_theme_item_in_types(Control *p_theme_owner, Window *p_theme_owner_window, Theme::DataType p_data_type, const StringName &p_name, List<StringName> p_theme_types); + _FORCE_INLINE_ void _get_theme_type_dependencies(const StringName &p_theme_type, List<StringName> *p_list) const; protected: virtual void add_child_notify(Node *p_child) override; @@ -260,19 +280,31 @@ protected: //virtual void _window_gui_input(InputEvent p_event); + virtual Vector<Vector2i> structured_text_parser(StructuredTextParser p_theme_type, const Array &p_args, const String p_text) const; + bool _set(const StringName &p_name, const Variant &p_value); bool _get(const StringName &p_name, Variant &r_ret) const; 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, NOTIFICATION_VISIBILITY_CHANGED=38*/ NOTIFICATION_RESIZED = 40, @@ -283,6 +315,7 @@ public: NOTIFICATION_THEME_CHANGED = 45, NOTIFICATION_SCROLL_BEGIN = 47, NOTIFICATION_SCROLL_END = 48, + NOTIFICATION_LAYOUT_DIRECTION_CHANGED = 49, }; @@ -301,8 +334,8 @@ public: virtual Rect2 _edit_get_rect() const override; virtual bool _edit_use_rect() const override; - virtual void _edit_set_rotation(float p_rotation) override; - virtual float _edit_get_rotation() const override; + virtual void _edit_set_rotation(real_t p_rotation) override; + virtual real_t _edit_get_rotation() const override; virtual bool _edit_use_rotation() const override; virtual void _edit_set_pivot(const Point2 &p_pivot) override; @@ -312,12 +345,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; @@ -329,20 +363,29 @@ 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_margins = true); - void set_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode = PRESET_MODE_MINSIZE, int p_margin = 0); - void set_anchors_and_margins_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode = PRESET_MODE_MINSIZE, int p_margin = 0); + void set_anchors_preset(LayoutPreset p_preset, bool p_keep_offsets = true); + void set_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode = PRESET_MODE_MINSIZE, int p_margin = 0); + void set_anchors_and_offsets_preset(LayoutPreset p_preset, LayoutPresetMode p_resize_mode = PRESET_MODE_MINSIZE, int p_margin = 0); - void set_anchor(Margin p_margin, float p_anchor, bool p_keep_margin = true, bool p_push_opposite_anchor = true); - float get_anchor(Margin p_margin) const; + void set_anchor(Side p_side, real_t p_anchor, bool p_keep_offset = true, bool p_push_opposite_anchor = true); + real_t get_anchor(Side p_side) const; - void set_margin(Margin p_margin, float p_value); - float get_margin(Margin p_margin) const; + void set_offset(Side p_side, real_t p_value); + real_t get_offset(Side p_side) const; - void set_anchor_and_margin(Margin p_margin, float p_anchor, float p_pos, bool p_push_opposite_anchor = true); + void set_anchor_and_offset(Side p_side, real_t p_anchor, real_t p_pos, bool p_push_opposite_anchor = true); void set_begin(const Point2 &p_point); // helper void set_end(const Point2 &p_point); // helper @@ -350,13 +393,13 @@ public: Point2 get_begin() const; Point2 get_end() const; - void set_position(const Point2 &p_point, bool p_keep_margins = false); - void set_global_position(const Point2 &p_point, bool p_keep_margins = false); + void set_position(const Point2 &p_point, bool p_keep_offsets = false); + void set_global_position(const Point2 &p_point, bool p_keep_offsets = false); Point2 get_position() const; Point2 get_global_position() const; Point2 get_screen_position() const; - void set_size(const Size2 &p_size, bool p_keep_margins = false); + void set_size(const Size2 &p_size, bool p_keep_offsets = false); Size2 get_size() const; Rect2 get_rect() const; @@ -365,10 +408,10 @@ public: Rect2 get_window_rect() const; ///< use with care, as it blocks waiting for the visual server Rect2 get_anchorable_rect() const override; - void set_rotation(float p_radians); - void set_rotation_degrees(float p_degrees); - float get_rotation() const; - float get_rotation_degrees() const; + 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); + real_t get_rotation() const; void set_h_grow_direction(GrowDirection p_direction); GrowDirection get_h_grow_direction() const; @@ -385,14 +428,17 @@ public: void set_theme(const Ref<Theme> &p_theme); Ref<Theme> get_theme() 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; void set_v_size_flags(int p_flags); int get_v_size_flags() const; - void set_stretch_ratio(float p_ratio); - float get_stretch_ratio() const; + void set_stretch_ratio(real_t p_ratio); + real_t get_stretch_ratio() const; void minimum_size_changed(); @@ -407,8 +453,8 @@ public: Control *find_next_valid_focus() const; Control *find_prev_valid_focus() const; - void set_focus_neighbour(Margin p_margin, const NodePath &p_neighbour); - NodePath get_focus_neighbour(Margin p_margin) const; + void set_focus_neighbor(Side p_side, const NodePath &p_neighbor); + NodePath get_focus_neighbor(Side p_side) const; void set_focus_next(const NodePath &p_next); NodePath get_focus_next() const; @@ -422,33 +468,43 @@ 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_shader_override(const StringName &p_name, const Ref<Shader> &p_shader); 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); + void add_theme_font_size_override(const StringName &p_name, int p_font_size); void add_theme_color_override(const StringName &p_name, const Color &p_color); void add_theme_constant_override(const StringName &p_name, int p_constant); - Ref<Texture2D> get_theme_icon(const StringName &p_name, const StringName &p_type = StringName()) const; - Ref<Shader> get_theme_shader(const StringName &p_name, const StringName &p_type = StringName()) const; - Ref<StyleBox> get_theme_stylebox(const StringName &p_name, const StringName &p_type = StringName()) const; - Ref<Font> get_theme_font(const StringName &p_name, const StringName &p_type = StringName()) const; - Color get_theme_color(const StringName &p_name, const StringName &p_type = StringName()) const; - int get_theme_constant(const StringName &p_name, const StringName &p_type = StringName()) const; + void remove_theme_icon_override(const StringName &p_name); + void remove_theme_style_override(const StringName &p_name); + void remove_theme_font_override(const StringName &p_name); + void remove_theme_font_size_override(const StringName &p_name); + void remove_theme_color_override(const StringName &p_name); + void remove_theme_constant_override(const StringName &p_name); + + Ref<Texture2D> get_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + Ref<StyleBox> get_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + Ref<Font> get_theme_font(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + int get_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + Color get_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + int get_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const; bool has_theme_icon_override(const StringName &p_name) const; - bool has_theme_shader_override(const StringName &p_name) const; bool has_theme_stylebox_override(const StringName &p_name) const; bool has_theme_font_override(const StringName &p_name) const; + bool has_theme_font_size_override(const StringName &p_name) const; bool has_theme_color_override(const StringName &p_name) const; bool has_theme_constant_override(const StringName &p_name) const; - bool has_theme_icon(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_shader(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_stylebox(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_font(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_color(const StringName &p_name, const StringName &p_type = StringName()) const; - bool has_theme_constant(const StringName &p_name, const StringName &p_type = StringName()) const; + bool has_theme_icon(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + bool has_theme_stylebox(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + bool has_theme_font(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + bool has_theme_font_size(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + bool has_theme_color(const StringName &p_name, const StringName &p_theme_type = StringName()) const; + bool has_theme_constant(const StringName &p_name, const StringName &p_theme_type = StringName()) const; /* TOOLTIP */ @@ -487,10 +543,9 @@ public: bool is_visibility_clip_disabled() const; virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const override; - virtual String get_configuration_warning() const override; + TypedArray<String> get_configuration_warnings() const override; - Control(); - ~Control(); + Control() {} }; VARIANT_ENUM_CAST(Control::FocusMode); @@ -501,5 +556,8 @@ VARIANT_ENUM_CAST(Control::LayoutPresetMode); VARIANT_ENUM_CAST(Control::MouseFilter); VARIANT_ENUM_CAST(Control::GrowDirection); VARIANT_ENUM_CAST(Control::Anchor); +VARIANT_ENUM_CAST(Control::LayoutDirection); +VARIANT_ENUM_CAST(Control::TextDirection); +VARIANT_ENUM_CAST(Control::StructuredTextParser); #endif diff --git a/scene/gui/dialogs.cpp b/scene/gui/dialogs.cpp index 4f59f4a36a..da858e8e83 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -60,7 +60,7 @@ void AcceptDialog::_notification(int p_what) { switch (p_what) { case NOTIFICATION_VISIBILITY_CHANGED: { if (is_visible()) { - get_ok()->grab_focus(); + get_ok_button()->grab_focus(); _update_child_rects(); parent_visible = get_parent_visible_window(); if (parent_visible) { @@ -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,27 +145,27 @@ 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)); } } void AcceptDialog::_update_child_rects() { Size2 label_size = label->get_minimum_size(); - if (label->get_text().empty()) { + 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); } @@ -253,23 +250,46 @@ Button *AcceptDialog::add_button(const String &p_text, bool p_right, const Strin return button; } -Button *AcceptDialog::add_cancel(const String &p_cancel) { +Button *AcceptDialog::add_cancel_button(const String &p_cancel) { String c = p_cancel; if (p_cancel == "") { - c = RTR("Cancel"); + c = TTRC("Cancel"); } Button *b = swap_cancel_ok ? add_button(c, true) : add_button(c); b->connect("pressed", callable_mp(this, &AcceptDialog::_cancel_pressed)); 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"), &AcceptDialog::get_ok); + ClassDB::bind_method(D_METHOD("get_ok_button"), &AcceptDialog::get_ok_button); ClassDB::bind_method(D_METHOD("get_label"), &AcceptDialog::get_label); ClassDB::bind_method(D_METHOD("set_hide_on_ok", "enabled"), &AcceptDialog::set_hide_on_ok); 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", "name"), &AcceptDialog::add_cancel); + 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); @@ -292,8 +312,6 @@ void AcceptDialog::set_swap_cancel_ok(bool p_swap) { } AcceptDialog::AcceptDialog() { - parent_visible = nullptr; - set_wrap_controls(true); set_visible(false); set_transient(true); @@ -305,12 +323,12 @@ AcceptDialog::AcceptDialog() { 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(MARGIN_RIGHT, Control::ANCHOR_END); - label->set_anchor(MARGIN_BOTTOM, Control::ANCHOR_END); + 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); @@ -319,14 +337,13 @@ AcceptDialog::AcceptDialog() { hbc->add_spacer(); ok = memnew(Button); - ok->set_text(RTR("OK")); + ok->set_text(TTRC("OK")); hbc->add_child(ok); hbc->add_spacer(); ok->connect("pressed", callable_mp(this, &AcceptDialog::_ok_pressed)); - hide_on_ok = true; - set_title(RTR("Alert!")); + set_title(TTRC("Alert!")); connect("window_input", callable_mp(this, &AcceptDialog::_input_from_window)); } @@ -337,17 +354,17 @@ AcceptDialog::~AcceptDialog() { // ConfirmationDialog void ConfirmationDialog::_bind_methods() { - ClassDB::bind_method(D_METHOD("get_cancel"), &ConfirmationDialog::get_cancel); + ClassDB::bind_method(D_METHOD("get_cancel_button"), &ConfirmationDialog::get_cancel_button); } -Button *ConfirmationDialog::get_cancel() { +Button *ConfirmationDialog::get_cancel_button() { return cancel; } ConfirmationDialog::ConfirmationDialog() { - set_title(RTR("Please Confirm...")); + set_title(TTRC("Please Confirm...")); #ifdef TOOLS_ENABLED set_min_size(Size2(200, 70) * EDSCALE); #endif - cancel = add_cancel(); + cancel = add_cancel_button(); } diff --git a/scene/gui/dialogs.h b/scene/gui/dialogs.h index de08685ce2..8e803a2a7c 100644 --- a/scene/gui/dialogs.h +++ b/scene/gui/dialogs.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -44,12 +44,12 @@ class LineEdit; class AcceptDialog : public Window { GDCLASS(AcceptDialog, Window); - Window *parent_visible; + Window *parent_visible = nullptr; Panel *bg; HBoxContainer *hbc; Label *label; Button *ok; - bool hide_on_ok; + bool hide_on_ok = true; void _custom_action(const String &p_action); void _update_child_rects(); @@ -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() { return ok; } + Button *get_ok_button() { return ok; } Button *add_button(const String &p_text, bool p_right = false, const String &p_action = ""); - Button *add_cancel(const String &p_cancel = ""); + 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; @@ -104,7 +105,7 @@ protected: static void _bind_methods(); public: - Button *get_cancel(); + Button *get_cancel_button(); ConfirmationDialog(); }; diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 7ce4e90f28..2512b24623 100644 --- a/scene/gui/file_dialog.cpp +++ b/scene/gui/file_dialog.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -49,21 +49,29 @@ VBoxContainer *FileDialog::get_vbox() { } void FileDialog::_theme_changed() { - Color font_color = vbox->get_theme_color("font_color", "Button"); - Color font_color_hover = vbox->get_theme_color("font_color_hover", "Button"); - Color font_color_pressed = vbox->get_theme_color("font_color_pressed", "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_color_normal", font_color); - dir_up->add_theme_color_override("icon_color_hover", font_color_hover); - dir_up->add_theme_color_override("icon_color_pressed", font_color_pressed); + dir_up->add_theme_color_override("icon_normal_color", font_color); + dir_up->add_theme_color_override("icon_hover_color", font_hover_color); + dir_up->add_theme_color_override("icon_pressed_color", font_pressed_color); - refresh->add_theme_color_override("icon_color_normal", font_color); - refresh->add_theme_color_override("icon_color_hover", font_color_hover); - refresh->add_theme_color_override("icon_color_pressed", font_color_pressed); + dir_prev->add_theme_color_override("icon_color_normal", font_color); + dir_prev->add_theme_color_override("icon_color_hover", font_hover_color); + dir_prev->add_theme_color_override("icon_color_pressed", font_pressed_color); - show_hidden->add_theme_color_override("icon_color_normal", font_color); - show_hidden->add_theme_color_override("icon_color_hover", font_color_hover); - show_hidden->add_theme_color_override("icon_color_pressed", font_color_pressed); + dir_next->add_theme_color_override("icon_color_normal", font_color); + dir_next->add_theme_color_override("icon_color_hover", font_hover_color); + dir_next->add_theme_color_override("icon_color_pressed", font_pressed_color); + + refresh->add_theme_color_override("icon_normal_color", font_color); + refresh->add_theme_color_override("icon_hover_color", font_hover_color); + refresh->add_theme_color_override("icon_pressed_color", font_pressed_color); + + show_hidden->add_theme_color_override("icon_normal_color", font_color); + show_hidden->add_theme_color_override("icon_hover_color", font_hover_color); + show_hidden->add_theme_color_override("icon_pressed_color", font_pressed_color); } void FileDialog::_notification(int p_what) { @@ -73,14 +81,23 @@ void FileDialog::_notification(int p_what) { } } if (p_what == NOTIFICATION_ENTER_TREE) { - dir_up->set_icon(vbox->get_theme_icon("parent_folder", "FileDialog")); - refresh->set_icon(vbox->get_theme_icon("reload", "FileDialog")); - show_hidden->set_icon(vbox->get_theme_icon("toggle_hidden", "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(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(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(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; if (k.is_valid() && has_focus()) { if (k->is_pressed()) { @@ -88,7 +105,7 @@ void FileDialog::_unhandled_input(const Ref<InputEvent> &p_event) { switch (k->get_keycode()) { case KEY_H: { - if (k->get_command()) { + if (k->is_command_pressed()) { set_show_hidden_files(!show_hidden_files); } else { handled = false; @@ -99,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; @@ -136,23 +153,24 @@ void FileDialog::update_dir() { } // Deselect any item, to make "Select Current Folder" button text by default. - deselect_items(); + 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(); update_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(); } @@ -172,11 +190,26 @@ void FileDialog::_post_popup() { // For open dir mode, deselect all items on file dialog open. if (mode == FILE_MODE_OPEN_DIR) { - deselect_items(); + deselect_all(); file_box->set_visible(false); } else { file_box->set_visible(true); } + + local_history.clear(); + local_history_pos = -1; + _push_history(); +} + +void FileDialog::_push_history() { + local_history.resize(local_history_pos + 1); + String new_path = dir_access->get_current_dir(); + if (local_history.size() == 0 || new_path != local_history[local_history_pos]) { + local_history.push_back(new_path); + local_history_pos++; + dir_prev->set_disabled(local_history_pos == 0); + dir_next->set_disabled(true); + } } void FileDialog::_action_pressed() { @@ -191,7 +224,7 @@ void FileDialog::_action_pressed() { } if (files.size()) { - emit_signal("files_selected", files); + emit_signal(SNAME("files_selected"), files); hide(); } @@ -201,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(); @@ -215,7 +248,7 @@ void FileDialog::_action_pressed() { } } - emit_signal("dir_selected", path); + emit_signal(SNAME("dir_selected"), path); hide(); } @@ -272,10 +305,10 @@ void FileDialog::_action_pressed() { } if (dir_access->file_exists(f)) { - confirm_save->set_text(RTR("File exists, overwrite?")); + 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(); } } @@ -316,23 +349,52 @@ void FileDialog::_go_up() { dir_access->change_dir(".."); update_file_list(); update_dir(); + _push_history(); } -void FileDialog::deselect_items() { +void FileDialog::_go_back() { + if (local_history_pos <= 0) { + return; + } + + local_history_pos--; + dir_access->change_dir(local_history[local_history_pos]); + update_file_list(); + update_dir(); + + dir_prev->set_disabled(local_history_pos == 0); + dir_next->set_disabled(local_history_pos == local_history.size() - 1); +} + +void FileDialog::_go_forward() { + if (local_history_pos == local_history.size() - 1) { + return; + } + + local_history_pos++; + dir_access->change_dir(local_history[local_history_pos]); + update_file_list(); + update_dir(); + + dir_prev->set_disabled(local_history_pos == 0); + dir_next->set_disabled(local_history_pos == local_history.size() - 1); +} + +void FileDialog::deselect_all() { // Clear currently selected items in file manager. tree->deselect_all(); // And change get_ok title. if (!tree->is_anything_selected()) { - get_ok()->set_disabled(_is_open_should_be_disabled()); + get_ok_button()->set_disabled(_is_open_should_be_disabled()); switch (mode) { case FILE_MODE_OPEN_FILE: case FILE_MODE_OPEN_FILES: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(TTRC("Open")); break; case FILE_MODE_OPEN_DIR: - get_ok()->set_text(RTR("Select Current Folder")); + get_ok_button()->set_text(TTRC("Select Current Folder")); break; case FILE_MODE_OPEN_ANY: case FILE_MODE_SAVE_FILE: @@ -356,10 +418,10 @@ void FileDialog::_tree_selected() { if (!d["dir"]) { file->set_text(d["name"]); } else if (mode == FILE_MODE_OPEN_DIR) { - get_ok()->set_text(RTR("Select This Folder")); + get_ok_button()->set_text(TTRC("Select This Folder")); } - get_ok()->set_disabled(_is_open_should_be_disabled()); + get_ok_button()->set_disabled(_is_open_should_be_disabled()); } void FileDialog::_tree_item_activated() { @@ -375,8 +437,9 @@ 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(); } @@ -405,16 +468,23 @@ 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; bool is_hidden; String item; + if (dir_access->is_readable(dir_access->get_current_dir().utf8().get_data())) { + message->hide(); + } else { + message->set_text(TTRC("You don't have permission to access contents of this folder.")); + message->show(); + } + while ((item = dir_access->get_next()) != "") { if (item == "." || item == "..") { continue; @@ -434,7 +504,7 @@ void FileDialog::update_file_list() { dirs.sort_custom<NaturalNoCaseComparator>(); files.sort_custom<NaturalNoCaseComparator>(); - while (!dirs.empty()) { + while (!dirs.is_empty()) { String &dir_name = dirs.front()->get(); TreeItem *ti = tree->create_item(root); ti->set_text(0, dir_name); @@ -478,13 +548,13 @@ void FileDialog::update_file_list() { String base_dir = dir_access->get_current_dir(); - while (!files.empty()) { - bool match = patterns.empty(); + while (!files.is_empty()) { + 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; } @@ -503,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; @@ -519,8 +589,8 @@ void FileDialog::update_file_list() { files.pop_front(); } - if (tree->get_root() && tree->get_root()->get_children() && tree->get_selected() == nullptr) { - tree->get_root()->get_children()->select(0); + if (tree->get_root() && tree->get_root()->get_first_child() && tree->get_selected() == nullptr) { + tree->get_root()->get_first_child()->select(0); } } @@ -549,7 +619,7 @@ void FileDialog::update_filters() { all_filters += ", ..."; } - filter->add_item(RTR("All Recognized") + " (" + all_filters + ")"); + filter->add_item(String(TTRC("All Recognized")) + " (" + all_filters + ")"); } for (int i = 0; i < filters.size(); i++) { String flt = filters[i].get_slice(";", 0).strip_edges(); @@ -561,7 +631,7 @@ void FileDialog::update_filters() { } } - filter->add_item(RTR("All Files (*)")); + filter->add_item(TTRC("All Files (*)")); } void FileDialog::clear_filters() { @@ -602,6 +672,7 @@ void FileDialog::set_current_dir(const String &p_dir) { dir_access->change_dir(p_dir); update_dir(); invalidate(); + _push_history(); } void FileDialog::set_current_file(const String &p_file) { @@ -646,37 +717,37 @@ void FileDialog::set_file_mode(FileMode p_mode) { mode = p_mode; switch (mode) { case FILE_MODE_OPEN_FILE: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(TTRC("Open")); if (mode_overrides_title) { - set_title(RTR("Open a File")); + set_title(TTRC("Open a File")); } makedir->hide(); break; case FILE_MODE_OPEN_FILES: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(TTRC("Open")); if (mode_overrides_title) { - set_title(RTR("Open File(s)")); + set_title(TTRC("Open File(s)")); } makedir->hide(); break; case FILE_MODE_OPEN_DIR: - get_ok()->set_text(RTR("Select Current Folder")); + get_ok_button()->set_text(TTRC("Select Current Folder")); if (mode_overrides_title) { - set_title(RTR("Open a Directory")); + set_title(TTRC("Open a Directory")); } makedir->show(); break; case FILE_MODE_OPEN_ANY: - get_ok()->set_text(RTR("Open")); + get_ok_button()->set_text(TTRC("Open")); if (mode_overrides_title) { - set_title(RTR("Open a File or Directory")); + set_title(TTRC("Open a File or Directory")); } makedir->show(); break; case FILE_MODE_SAVE_FILE: - get_ok()->set_text(RTR("Save")); + get_ok_button()->set_text(TTRC("Save")); if (mode_overrides_title) { - set_title(RTR("Save a File")); + set_title(TTRC("Save a File")); } makedir->show(); break; @@ -731,12 +802,13 @@ FileDialog::Access FileDialog::get_access() const { } void FileDialog::_make_dir_confirm() { - Error err = dir_access->make_dir(makedirname->get_text()); + Error err = dir_access->make_dir(makedirname->get_text().strip_edges()); if (err == OK) { - dir_access->change_dir(makedirname->get_text()); + dir_access->change_dir(makedirname->get_text().strip_edges()); invalidate(); update_filters(); update_dir(); + _push_history(); } else { mkdirerr->popup_centered(Size2(250, 50)); } @@ -754,6 +826,7 @@ void FileDialog::_select_drive(int p_idx) { file->set_text(""); invalidate(); update_dir(); + _push_history(); } void FileDialog::_update_drives() { @@ -781,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); @@ -808,7 +879,7 @@ void FileDialog::_bind_methods() { ClassDB::bind_method(D_METHOD("_update_file_name"), &FileDialog::update_file_name); ClassDB::bind_method(D_METHOD("_update_dir"), &FileDialog::update_dir); ClassDB::bind_method(D_METHOD("_update_file_list"), &FileDialog::update_file_list); - ClassDB::bind_method(D_METHOD("deselect_items"), &FileDialog::deselect_items); + ClassDB::bind_method(D_METHOD("deselect_all"), &FileDialog::deselect_all); ClassDB::bind_method(D_METHOD("invalidate"), &FileDialog::invalidate); @@ -852,24 +923,32 @@ void FileDialog::set_default_show_hidden_files(bool p_show) { FileDialog::FileDialog() { show_hidden_files = default_show_hidden_files; - mode_overrides_title = true; - vbox = memnew(VBoxContainer); add_child(vbox); vbox->connect("theme_changed", callable_mp(this, &FileDialog::_theme_changed)); mode = FILE_MODE_SAVE_FILE; - set_title(RTR("Save a File")); + set_title(TTRC("Save a File")); HBoxContainer *hbc = memnew(HBoxContainer); + dir_prev = memnew(Button); + dir_prev->set_flat(true); + dir_prev->set_tooltip(TTRC("Go to previous folder.")); + dir_next = memnew(Button); + dir_next->set_flat(true); + dir_next->set_tooltip(TTRC("Go to next folder.")); dir_up = memnew(Button); dir_up->set_flat(true); - dir_up->set_tooltip(RTR("Go to parent folder.")); + dir_up->set_tooltip(TTRC("Go to parent folder.")); + hbc->add_child(dir_prev); + hbc->add_child(dir_next); hbc->add_child(dir_up); + dir_prev->connect("pressed", callable_mp(this, &FileDialog::_go_back)); + dir_next->connect("pressed", callable_mp(this, &FileDialog::_go_forward)); dir_up->connect("pressed", callable_mp(this, &FileDialog::_go_up)); - hbc->add_child(memnew(Label(RTR("Path:")))); + hbc->add_child(memnew(Label(TTRC("Path:")))); drives_container = memnew(HBoxContainer); hbc->add_child(drives_container); @@ -879,12 +958,13 @@ FileDialog::FileDialog() { hbc->add_child(drives); dir = memnew(LineEdit); + dir->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); hbc->add_child(dir); dir->set_h_size_flags(Control::SIZE_EXPAND_FILL); refresh = memnew(Button); refresh->set_flat(true); - refresh->set_tooltip(RTR("Refresh files.")); + refresh->set_tooltip(TTRC("Refresh files.")); refresh->connect("pressed", callable_mp(this, &FileDialog::update_file_list)); hbc->add_child(refresh); @@ -892,7 +972,7 @@ FileDialog::FileDialog() { show_hidden->set_flat(true); show_hidden->set_toggle_mode(true); show_hidden->set_pressed(is_showing_hidden_files()); - show_hidden->set_tooltip(RTR("Toggle the visibility of hidden files.")); + show_hidden->set_tooltip(TTRC("Toggle the visibility of hidden files.")); show_hidden->connect("toggled", callable_mp(this, &FileDialog::set_show_hidden_files)); hbc->add_child(show_hidden); @@ -900,18 +980,26 @@ FileDialog::FileDialog() { hbc->add_child(shortcuts_container); makedir = memnew(Button); - makedir->set_text(RTR("Create Folder")); + makedir->set_text(TTRC("Create Folder")); makedir->connect("pressed", callable_mp(this, &FileDialog::_make_dir)); hbc->add_child(makedir); vbox->add_child(hbc); tree = memnew(Tree); tree->set_hide_root(true); - vbox->add_margin_child(RTR("Directories & Files:"), tree, true); + vbox->add_margin_child(TTRC("Directories & Files:"), tree, true); + + message = memnew(Label); + message->hide(); + message->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + message->set_align(Label::ALIGN_CENTER); + message->set_valign(Label::VALIGN_CENTER); + tree->add_child(message); file_box = memnew(HBoxContainer); - file_box->add_child(memnew(Label(RTR("File:")))); + file_box->add_child(memnew(Label(TTRC("File:")))); file = memnew(LineEdit); + file->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); file->set_stretch_ratio(4); file->set_h_size_flags(Control::SIZE_EXPAND_FILL); file_box->add_child(file); @@ -923,16 +1011,15 @@ FileDialog::FileDialog() { vbox->add_child(file_box); dir_access = DirAccess::create(DirAccess::ACCESS_RESOURCES); - access = ACCESS_RESOURCES; _update_drives(); connect("confirmed", callable_mp(this, &FileDialog::_action_pressed)); tree->connect("multi_selected", callable_mp(this, &FileDialog::_tree_multi_selected), varray(), CONNECT_DEFERRED); 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_items)); - dir->connect("text_entered", callable_mp(this, &FileDialog::_dir_entered)); - file->connect("text_entered", callable_mp(this, &FileDialog::_file_entered)); + tree->connect("nothing_selected", callable_mp(this, &FileDialog::deselect_all)); + 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); @@ -942,21 +1029,22 @@ FileDialog::FileDialog() { confirm_save->connect("confirmed", callable_mp(this, &FileDialog::_save_confirm_pressed)); makedialog = memnew(ConfirmationDialog); - makedialog->set_title(RTR("Create Folder")); + makedialog->set_title(TTRC("Create Folder")); VBoxContainer *makevb = memnew(VBoxContainer); makedialog->add_child(makevb); makedirname = memnew(LineEdit); - makevb->add_margin_child(RTR("Name:"), makedirname); + makedirname->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); + makevb->add_margin_child(TTRC("Name:"), makedirname); add_child(makedialog); makedialog->register_text_enter(makedirname); makedialog->connect("confirmed", callable_mp(this, &FileDialog::_make_dir_confirm)); mkdirerr = memnew(AcceptDialog); - mkdirerr->set_text(RTR("Could not create folder.")); + mkdirerr->set_text(TTRC("Could not create folder.")); add_child(mkdirerr); exterr = memnew(AcceptDialog); - exterr->set_text(RTR("Must use a valid extension.")); + exterr->set_text(TTRC("Must use a valid extension.")); add_child(exterr); update_filters(); @@ -964,7 +1052,6 @@ FileDialog::FileDialog() { set_hide_on_ok(false); - invalidated = true; if (register_func) { register_func(this); } diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index 8003650668..b5190bdab1 100644 --- a/scene/gui/file_dialog.h +++ b/scene/gui/file_dialog.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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" @@ -69,7 +69,7 @@ private: LineEdit *makedirname; Button *makedir; - Access access; + Access access = ACCESS_RESOURCES; //Button *action; VBoxContainer *vbox; FileMode mode; @@ -86,6 +86,10 @@ private: DirAccess *dir_access; ConfirmationDialog *confirm_save; + Label *message; + + Button *dir_prev; + Button *dir_next; Button *dir_up; Button *refresh; @@ -93,12 +97,16 @@ private: Vector<String> filters; - bool mode_overrides_title; + Vector<String> local_history; + int local_history_pos = 0; + void _push_history(); + + bool mode_overrides_title = true; static bool default_show_hidden_files; - bool show_hidden_files; + bool show_hidden_files = false; - bool invalidated; + bool invalidated = true; void update_dir(); void update_file_name(); @@ -110,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(); @@ -119,10 +127,12 @@ private: void _make_dir(); void _make_dir_confirm(); void _go_up(); + void _go_back(); + void _go_forward(); 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(); @@ -170,7 +180,7 @@ public: void invalidate(); - void deselect_items(); + void deselect_all(); FileDialog(); ~FileDialog(); diff --git a/scene/gui/gradient_edit.cpp b/scene/gui/gradient_edit.cpp index 53d7ead548..bbab3008e2 100644 --- a/scene/gui/gradient_edit.cpp +++ b/scene/gui/gradient_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -42,8 +42,6 @@ #endif GradientEdit::GradientEdit() { - grabbed = -1; - grabbing = false; set_focus_mode(FOCUS_ALL); popup = memnew(PopupPanel); @@ -51,9 +49,6 @@ GradientEdit::GradientEdit() { popup->add_child(picker); add_child(popup); - - checker = Ref<ImageTexture>(memnew(ImageTexture)); - Ref<Image> img = memnew(Image(checker_bg_png)); } int GradientEdit::_get_point_from_pos(int x) { @@ -93,7 +88,9 @@ 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; if (k.is_valid() && k->is_pressed() && k->get_keycode() == KEY_DELETE && grabbed != -1) { @@ -101,13 +98,13 @@ 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(); } Ref<InputEventMouseButton> mb = p_event; //Show color picker on double click. - if (mb.is_valid() && mb->get_button_index() == 1 && mb->is_doubleclick() && mb->is_pressed()) { + if (mb.is_valid() && mb->get_button_index() == 1 && mb->is_double_click() && mb->is_pressed()) { grabbed = _get_point_from_pos(mb->get_position().x); _show_color_picker(); accept_event(); @@ -121,13 +118,13 @@ 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(); } } //Hold alt key to duplicate selected color - if (mb.is_valid() && mb->get_button_index() == 1 && mb->is_pressed() && mb->get_alt()) { + if (mb.is_valid() && mb->get_button_index() == 1 && mb->is_pressed() && mb->is_alt_pressed()) { int x = mb->get_position().x; grabbed = _get_point_from_pos(x); @@ -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(); } @@ -236,9 +233,9 @@ void GradientEdit::_gui_input(const Ref<InputEvent> &p_event) { // Snap to "round" coordinates if holding Ctrl. // Be more precise if holding Shift as well - if (mm->get_control()) { - newofs = Math::stepify(newofs, mm->get_shift() ? 0.025 : 0.1); - } else if (mm->get_shift()) { + if (mm->is_ctrl_pressed()) { + newofs = Math::snapped(newofs, mm->is_shift_pressed() ? 0.025 : 0.1); + } else if (mm->is_shift_pressed()) { // Snap to nearest point if holding just Shift const float snap_threshold = 0.03; float smallest_ofs = snap_threshold; @@ -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 6e950703bb..a173631963 100644 --- a/scene/gui/gradient_edit.h +++ b/scene/gui/gradient_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -42,10 +42,8 @@ class GradientEdit : public Control { PopupPanel *popup; ColorPicker *picker; - Ref<ImageTexture> checker; - - bool grabbing; - int grabbed; + bool grabbing = false; + int grabbed = -1; Vector<Gradient::Point> points; void _draw_checker(int x, int y, int w, int h); @@ -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 ad02aaade5..0281bc7efb 100644 --- a/scene/gui/graph_edit.cpp +++ b/scene/gui/graph_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -31,6 +31,7 @@ #include "graph_edit.h" #include "core/input/input.h" +#include "core/math/math_funcs.h" #include "core/os/keyboard.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" @@ -39,10 +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) +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); @@ -52,6 +51,148 @@ GraphEditFilter::GraphEditFilter(GraphEdit *p_edit) { ge = p_edit; } +GraphEditMinimap::GraphEditMinimap(GraphEdit *p_edit) { + ge = p_edit; + + graph_proportions = Vector2(1, 1); + graph_padding = Vector2(0, 0); + camera_position = Vector2(100, 50); + camera_size = Vector2(200, 200); + minimap_padding = Vector2(MINIMAP_PADDING, MINIMAP_PADDING); + minimap_offset = minimap_padding + _convert_from_graph_position(graph_padding); + + is_pressing = false; + is_resizing = false; +} + +void GraphEditMinimap::update_minimap() { + Vector2 graph_offset = _get_graph_offset(); + Vector2 graph_size = _get_graph_size(); + + camera_position = ge->get_scroll_ofs() - graph_offset; + camera_size = ge->get_size(); + + Vector2 render_size = _get_render_size(); + float target_ratio = render_size.x / render_size.y; + float graph_ratio = graph_size.x / graph_size.y; + + graph_proportions = graph_size; + graph_padding = Vector2(0, 0); + if (graph_ratio > target_ratio) { + graph_proportions.x = graph_size.x; + graph_proportions.y = graph_size.x / target_ratio; + graph_padding.y = Math::abs(graph_size.y - graph_proportions.y) / 2; + } else { + graph_proportions.x = graph_size.y * target_ratio; + graph_proportions.y = graph_size.y; + graph_padding.x = Math::abs(graph_size.x - graph_proportions.x) / 2; + } + + // This centers minimap inside the minimap rectangle. + minimap_offset = minimap_padding + _convert_from_graph_position(graph_padding); +} + +Rect2 GraphEditMinimap::get_camera_rect() { + Vector2 camera_center = _convert_from_graph_position(camera_position + camera_size / 2) + minimap_offset; + Vector2 camera_viewport = _convert_from_graph_position(camera_size); + Vector2 camera_position = (camera_center - camera_viewport / 2); + return Rect2(camera_position, camera_viewport); +} + +Vector2 GraphEditMinimap::_get_render_size() { + if (!is_inside_tree()) { + return Vector2(0, 0); + } + + return get_size() - 2 * minimap_padding; +} + +Vector2 GraphEditMinimap::_get_graph_offset() { + return Vector2(ge->h_scroll->get_min(), ge->v_scroll->get_min()); +} + +Vector2 GraphEditMinimap::_get_graph_size() { + Vector2 graph_size = Vector2(ge->h_scroll->get_max(), ge->v_scroll->get_max()) - Vector2(ge->h_scroll->get_min(), ge->v_scroll->get_min()); + + if (graph_size.x == 0) { + graph_size.x = 1; + } + if (graph_size.y == 0) { + graph_size.y = 1; + } + + return graph_size; +} + +Vector2 GraphEditMinimap::_convert_from_graph_position(const Vector2 &p_position) { + Vector2 map_position = Vector2(0, 0); + Vector2 render_size = _get_render_size(); + + map_position.x = p_position.x * render_size.x / graph_proportions.x; + map_position.y = p_position.y * render_size.y / graph_proportions.y; + + return map_position; +} + +Vector2 GraphEditMinimap::_convert_to_graph_position(const Vector2 &p_position) { + Vector2 graph_position = Vector2(0, 0); + Vector2 render_size = _get_render_size(); + + graph_position.x = p_position.x * graph_proportions.x / render_size.x; + graph_position.y = p_position.y * graph_proportions.y / render_size.y; + + return graph_position; +} + +void GraphEditMinimap::gui_input(const Ref<InputEvent> &p_ev) { + ERR_FAIL_COND(p_ev.is_null()); + + if (!ge->is_minimap_enabled()) { + return; + } + + Ref<InputEventMouseButton> mb = p_ev; + Ref<InputEventMouseMotion> mm = p_ev; + + if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { + if (mb->is_pressed()) { + is_pressing = true; + + 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; + } else { + Vector2 click_position = _convert_to_graph_position(mb->get_position() - minimap_padding) - graph_padding; + _adjust_graph_scroll(click_position); + } + } else { + is_pressing = false; + is_resizing = false; + } + accept_event(); + } else if (mm.is_valid() && is_pressing) { + if (is_resizing) { + // Prevent setting minimap wider than GraphEdit + Vector2 new_minimap_size; + new_minimap_size.x = MIN(get_size().x - mm->get_relative().x, ge->get_size().x - 2.0 * minimap_padding.x); + new_minimap_size.y = MIN(get_size().y - mm->get_relative().y, ge->get_size().y - 2.0 * minimap_padding.y); + ge->set_minimap_size(new_minimap_size); + + update(); + } else { + Vector2 click_position = _convert_to_graph_position(mm->get_position() - minimap_padding) - graph_padding; + _adjust_graph_scroll(click_position); + } + accept_event(); + } +} + +void GraphEditMinimap::_adjust_graph_scroll(const Vector2 &p_offset) { + Vector2 graph_offset = _get_graph_offset(); + ge->set_scroll_ofs(p_offset + graph_offset - camera_size / 2); +} + Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port) { if (is_node_connected(p_from, p_from_port, p_to, p_to_port)) { return OK; @@ -64,6 +205,7 @@ Error GraphEdit::connect_node(const StringName &p_from, int p_from_port, const S c.activity = 0; connections.push_back(c); top_layer->update(); + minimap->update(); update(); connections_layer->update(); @@ -71,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; } } @@ -81,10 +223,11 @@ 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(); + minimap->update(); update(); connections_layer->update(); return; @@ -92,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; } @@ -114,14 +253,15 @@ 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(); + minimap->update(); 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()); } } @@ -134,7 +274,7 @@ void GraphEdit::_update_scroll_offset() { continue; } - Point2 pos = gn->get_offset() * zoom; + Point2 pos = gn->get_position_offset() * zoom; pos -= Point2(h_scroll->get_value(), v_scroll->get_value()); gn->set_position(pos); if (gn->get_scale() != Vector2(zoom, zoom)) { @@ -164,7 +304,7 @@ void GraphEdit::_update_scroll() { } Rect2 r; - r.position = gn->get_offset() * zoom; + r.position = gn->get_position_offset() * zoom; r.size = gn->get_size() * zoom; screen = screen.merge(r); } @@ -195,13 +335,13 @@ void GraphEdit::_update_scroll() { Size2 vmin = v_scroll->get_combined_minimum_size(); // Avoid scrollbar overlapping. - h_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, v_scroll->is_visible() ? -vmin.width : 0); - v_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, h_scroll->is_visible() ? -hmin.height : 0); + h_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, v_scroll->is_visible() ? -vmin.width : 0); + v_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, h_scroll->is_visible() ? -hmin.height : 0); 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; } @@ -227,13 +367,23 @@ void GraphEdit::_graph_node_raised(Node *p_gn) { 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) { GraphNode *gn = Object::cast_to<GraphNode>(p_gn); ERR_FAIL_COND(!gn); top_layer->update(); + minimap->update(); + update(); + connections_layer->update(); +} + +void GraphEdit::_graph_node_slot_updated(int p_index, Node *p_gn) { + GraphNode *gn = Object::cast_to<GraphNode>(p_gn); + ERR_FAIL_COND(!gn); + top_layer->update(); + minimap->update(); update(); connections_layer->update(); } @@ -241,13 +391,16 @@ void GraphEdit::_graph_node_moved(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) { gn->set_scale(Vector2(zoom, zoom)); - gn->connect("offset_changed", callable_mp(this, &GraphEdit::_graph_node_moved), varray(gn)); + gn->connect("position_offset_changed", callable_mp(this, &GraphEdit::_graph_node_moved), varray(gn)); + gn->connect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated), varray(gn)); gn->connect("raise_request", callable_mp(this, &GraphEdit::_graph_node_raised), varray(gn)); gn->connect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::update)); + gn->connect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::update)); _graph_node_moved(gn); gn->set_mouse_filter(MOUSE_FILTER_PASS); } @@ -255,43 +408,62 @@ void GraphEdit::add_child_notify(Node *p_child) { void GraphEdit::remove_child_notify(Node *p_child) { Control::remove_child_notify(p_child); - if (is_inside_tree()) { - top_layer->call_deferred("raise"); //top layer always on top! + + if (p_child == top_layer) { + top_layer = nullptr; + minimap = nullptr; + } else if (p_child == connections_layer) { + connections_layer = nullptr; } + + if (top_layer != nullptr && is_inside_tree()) { + top_layer->call_deferred(SNAME("raise")); // Top layer always on top! + } + GraphNode *gn = Object::cast_to<GraphNode>(p_child); if (gn) { - gn->disconnect("offset_changed", callable_mp(this, &GraphEdit::_graph_node_moved)); + gn->disconnect("position_offset_changed", callable_mp(this, &GraphEdit::_graph_node_moved)); + gn->disconnect("slot_updated", callable_mp(this, &GraphEdit::_graph_node_slot_updated)); gn->disconnect("raise_request", callable_mp(this, &GraphEdit::_graph_node_raised)); - gn->disconnect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::update)); + + // In case of the whole GraphEdit being destroyed these references can already be freed. + if (connections_layer != nullptr && connections_layer->is_inside_tree()) { + gn->disconnect("item_rect_changed", callable_mp((CanvasItem *)connections_layer, &CanvasItem::update)); + } + if (minimap != nullptr && minimap->is_inside_tree()) { + gn->disconnect("item_rect_changed", callable_mp((CanvasItem *)minimap, &GraphEditMinimap::update)); + } } } 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")); + 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(); Size2 vmin = v_scroll->get_combined_minimum_size(); - h_scroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_BEGIN, 0); - h_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - h_scroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_END, -hmin.height); - h_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + h_scroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0); + h_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + h_scroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -hmin.height); + h_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); - v_scroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_END, -vmin.width); - v_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - v_scroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 0); - v_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + v_scroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -vmin.width); + v_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + v_scroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); + 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 @@ -304,8 +476,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; @@ -338,11 +510,13 @@ void GraphEdit::_notification(int p_what) { if (p_what == NOTIFICATION_RESIZED) { _update_scroll(); top_layer->update(); + minimap->update(); } } 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)); @@ -352,14 +526,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, p_point)) { + 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, p_point)) { + if (is_in_hot_zone(pos / zoom, p_point / zoom, port_size, true)) { return true; } } @@ -370,10 +544,12 @@ 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() == BUTTON_LEFT && mb->is_pressed()) { + 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(); + 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)); if (!gn) { @@ -382,23 +558,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, 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; @@ -424,23 +600,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, 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; @@ -472,11 +648,14 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting_to = mm->get_position(); connecting_target = false; top_layer->update(); - connecting_valid = just_disconnected || click_pos.distance_to(connecting_to) > 20.0 * zoom; + minimap->update(); + connecting_valid = just_disconnected || click_pos.distance_to(connecting_to / zoom) > 20.0; if (connecting_valid) { - Ref<Texture2D> port = get_theme_icon("port", "GraphNode"); - Vector2 mpos = mm->get_position(); + 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)); if (!gn) { @@ -487,7 +666,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, 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(); @@ -499,7 +678,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, 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(); @@ -512,7 +691,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { } } - if (mb.is_valid() && mb->get_button_index() == BUTTON_LEFT && !mb->is_pressed()) { + if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_LEFT && !mb->is_pressed()) { if (connecting_valid) { if (connecting && connecting_target) { String from = connecting_from; @@ -524,7 +703,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; @@ -532,15 +711,16 @@ 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); } } } connecting = false; top_layer->update(); + minimap->update(); update(); connections_layer->update(); } @@ -569,9 +749,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++) { @@ -580,6 +776,11 @@ bool GraphEdit::is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos) { continue; } Rect2 rect = child->get_rect(); + + // To prevent intersections with other nodes. + rect.position *= zoom; + rect.size *= zoom; + if (rect.has_point(p_mouse_pos)) { //check sub-controls Vector2 subpos = p_mouse_pos - rect.position; @@ -600,73 +801,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); - } -} - -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) { - //cubic bezier code - float diff = p_to.x - p_from.x; - float cp_offset; - int cp_len = get_theme_constant("bezier_len_pos"); - int cp_neg_len = get_theme_constant("bezier_len_neg"); - - 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); +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; } - 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(2 * EDSCALE)); + p_where->draw_polyline_colors(scaled_points, colors, Math::floor(p_width * EDSCALE), lines_antialiased); #else - p_where->draw_polyline_colors(points, colors, 2); + 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()) { @@ -699,16 +868,16 @@ void GraphEdit::_connections_layer_draw() { continue; } - Vector2 frompos = gfrom->get_connection_output_position(E->get().from_port) + gfrom->get_offset() * zoom; + Vector2 frompos = gfrom->get_connection_output_position(E->get().from_port) + gfrom->get_position_offset() * zoom; Color color = gfrom->get_connection_output_color(E->get().from_port); - Vector2 topos = gto->get_connection_input_position(E->get().to_port) + gto->get_offset() * zoom; + Vector2 topos = gto->get_connection_input_position(E->get().to_port) + gto->get_position_offset() * zoom; Color tocolor = gto->get_connection_input_color(E->get().to_port); if (E->get().activity > 0) { 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); + _draw_connection_line(connections_layer, frompos, topos, color, tocolor, lines_thickness, zoom); } while (to_erase.size()) { @@ -747,13 +916,121 @@ void GraphEdit::_top_layer_draw() { if (!connecting_out) { SWAP(pos, topos); } - _draw_cos_line(top_layer, pos, topos, col, col); + _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); + } +} + +void GraphEdit::_minimap_draw() { + if (!is_minimap_enabled()) { + return; + } + + minimap->update_minimap(); + + // Draw the minimap background. + Rect2 minimap_rect = Rect2(Point2(), minimap->get_size()); + 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; + + // Draw comment graph nodes. + for (int i = get_child_count() - 1; i >= 0; i--) { + GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); + if (!gn || !gn->is_comment()) { + continue; + } + + Vector2 node_position = minimap->_convert_from_graph_position(gn->get_position_offset() * zoom - graph_offset) + minimap_offset; + 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(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"); + if (sbf.is_valid()) { + Color node_color = sbf->get_bg_color(); + sb_minimap->set_bg_color(node_color); + } + + minimap->draw_style_box(sb_minimap, node_rect); + } + + // Draw regular graph nodes. + for (int i = get_child_count() - 1; i >= 0; i--) { + GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); + if (!gn || gn->is_comment()) { + continue; + } + + Vector2 node_position = minimap->_convert_from_graph_position(gn->get_position_offset() * zoom - graph_offset) + minimap_offset; + 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(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"); + if (sbf.is_valid()) { + Color node_color = sbf->get_border_color(); + sb_minimap->set_bg_color(node_color); + } + + minimap->draw_style_box(sb_minimap, node_rect); + } + + // Draw node connections. + Color activity_color = get_theme_color(SNAME("activity")); + for (const Connection &E : connections) { + NodePath fromnp(E.from); + + Node *from = get_node(fromnp); + if (!from) { + continue; + } + GraphNode *gfrom = Object::cast_to<GraphNode>(from); + if (!gfrom) { + continue; + } + + NodePath tonp(E.to); + Node *to = get_node(tonp); + if (!to) { + continue; + } + GraphNode *gto = Object::cast_to<GraphNode>(to); + if (!gto) { + continue; + } + + 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.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.to_port); + + if (E.activity > 0) { + from_color = from_color.lerp(activity_color, E.activity); + to_color = to_color.lerp(activity_color, E.activity); + } + _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(SNAME("camera")), camera_rect); + + // Draw the resizer control. + 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); } void GraphEdit::set_selected(Node *p_child) { @@ -767,16 +1044,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() & BUTTON_MASK_MIDDLE || (mm->get_button_mask() & 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); + 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)))) { + 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; } @@ -789,12 +1069,12 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { // Snapping can be toggled temporarily by holding down Ctrl. // This is done here as to not toggle the grid when holding down Ctrl. - if (is_using_snap() ^ Input::get_singleton()->is_key_pressed(KEY_CONTROL)) { + if (is_using_snap() ^ Input::get_singleton()->is_key_pressed(KEY_CTRL)) { const int snap = get_snap(); pos = pos.snapped(Vector2(snap, snap)); } - gn->set_offset(pos); + gn->set_position_offset(pos); } } } @@ -819,28 +1099,29 @@ 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_unselected", gn); + emit_signal(SNAME("node_deselected"), gn); } gn->set_selected(box_selection_mode_additive); } else { - bool select = (previus_selected.find(gn) != nullptr); + bool select = (previous_selected.find(gn) != nullptr); if (gn->is_selected() && !select) { - emit_signal("node_unselected", 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); } } top_layer->update(); + minimap->update(); } Ref<InputEventMouseButton> b = p_ev; if (b.is_valid()) { - if (b->get_button_index() == BUTTON_RIGHT && b->is_pressed()) { + if (b->get_button_index() == MOUSE_BUTTON_RIGHT && b->is_pressed()) { if (box_selecting) { box_selecting = false; for (int i = get_child_count() - 1; i >= 0; i--) { @@ -849,27 +1130,29 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { continue; } - bool select = (previus_selected.find(gn) != nullptr); + bool select = (previous_selected.find(gn) != nullptr); if (gn->is_selected() && !select) { - emit_signal("node_unselected", 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); } top_layer->update(); + minimap->update(); } else { if (connecting) { connecting = false; top_layer->update(); + minimap->update(); } else { - emit_signal("popup_request", b->get_global_position()); + emit_signal(SNAME("popup_request"), b->get_global_position()); } } } - if (b->get_button_index() == BUTTON_LEFT && !b->is_pressed() && dragging) { - if (!just_selected && drag_accum == Vector2() && Input::get_singleton()->is_key_pressed(KEY_CONTROL)) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT && !b->is_pressed() && dragging) { + if (!just_selected && drag_accum == Vector2() && Input::get_singleton()->is_key_pressed(KEY_CTRL)) { //deselect current node for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); @@ -878,7 +1161,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_unselected", gn); + emit_signal(SNAME("node_deselected"), gn); gn->set_selected(false); } } @@ -895,18 +1178,19 @@ 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; } dragging = false; top_layer->update(); + minimap->update(); update(); connections_layer->update(); } - if (b->get_button_index() == BUTTON_LEFT && b->is_pressed()) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT && b->is_pressed()) { GraphNode *gn = nullptr; for (int i = get_child_count() - 1; i >= 0; i--) { @@ -917,7 +1201,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { continue; } - if (gn_selected->has_point(b->get_position() - gn_selected->get_position())) { + if (gn_selected->has_point((b->get_position() - gn_selected->get_position()) / zoom)) { gn = gn_selected; break; } @@ -932,7 +1216,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { dragging = true; drag_accum = Vector2(); just_selected = !gn->is_selected(); - if (!gn->is_selected() && !Input::get_singleton()->is_key_pressed(KEY_CONTROL)) { + if (!gn->is_selected() && !Input::get_singleton()->is_key_pressed(KEY_CTRL)) { for (int i = 0; i < get_child_count(); i++) { GraphNode *o_gn = Object::cast_to<GraphNode>(get_child(i)); if (o_gn) { @@ -940,7 +1224,7 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { o_gn->set_selected(true); } else { if (o_gn->is_selected()) { - emit_signal("node_unselected", o_gn); + emit_signal(SNAME("node_deselected"), o_gn); } o_gn->set_selected(false); } @@ -969,38 +1253,38 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { box_selecting = true; box_selecting_from = b->get_position(); - if (b->get_control()) { + if (b->is_ctrl_pressed()) { box_selection_mode_additive = true; - previus_selected.clear(); + previous_selected.clear(); for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn2 = Object::cast_to<GraphNode>(get_child(i)); if (!gn2 || !gn2->is_selected()) { continue; } - previus_selected.push_back(gn2); + previous_selected.push_back(gn2); } - } else if (b->get_shift()) { + } else if (b->is_shift_pressed()) { box_selection_mode_additive = false; - previus_selected.clear(); + previous_selected.clear(); for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn2 = Object::cast_to<GraphNode>(get_child(i)); if (!gn2 || !gn2->is_selected()) { continue; } - previus_selected.push_back(gn2); + previous_selected.push_back(gn2); } } else { box_selection_mode_additive = true; - previus_selected.clear(); + previous_selected.clear(); for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn2 = Object::cast_to<GraphNode>(get_child(i)); if (!gn2) { continue; } if (gn2->is_selected()) { - emit_signal("node_unselected", gn2); + emit_signal(SNAME("node_deselected"), gn2); } gn2->set_selected(false); } @@ -1008,55 +1292,43 @@ void GraphEdit::_gui_input(const Ref<InputEvent> &p_ev) { } } - if (b->get_button_index() == BUTTON_LEFT && !b->is_pressed() && box_selecting) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT && !b->is_pressed() && box_selecting) { box_selecting = false; - previus_selected.clear(); + box_selecting_rect = Rect2(); + previous_selected.clear(); top_layer->update(); + minimap->update(); } - if (b->get_button_index() == BUTTON_WHEEL_UP && b->is_pressed()) { - //too difficult to get right - //set_zoom(zoom*ZOOM_SCALE); - } - - if (b->get_button_index() == BUTTON_WHEEL_DOWN && b->is_pressed()) { - //too difficult to get right - //set_zoom(zoom/ZOOM_SCALE); - } - if (b->get_button_index() == 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); - } - if (b->get_button_index() == 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); - } - if (b->get_button_index() == BUTTON_WHEEL_RIGHT || (b->get_button_index() == 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); - } - if (b->get_button_index() == BUTTON_WHEEL_LEFT || (b->get_button_index() == 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()); + } } } - Ref<InputEventKey> k = p_ev; - - if (k.is_valid()) { - if (k->get_keycode() == KEY_D && k->is_pressed() && k->get_command()) { - emit_signal("duplicate_nodes_request"); + if (p_ev->is_pressed()) { + if (p_ev->is_action("ui_graph_duplicate")) { + emit_signal(SNAME("duplicate_nodes_request")); accept_event(); - } - - if (k->get_keycode() == KEY_C && k->is_pressed() && k->get_command()) { - emit_signal("copy_nodes_request"); + } else if (p_ev->is_action("ui_copy")) { + emit_signal(SNAME("copy_nodes_request")); accept_event(); - } - - if (k->get_keycode() == KEY_V && k->is_pressed() && k->get_command()) { - emit_signal("paste_nodes_request"); + } else if (p_ev->is_action("ui_paste")) { + emit_signal(SNAME("paste_nodes_request")); accept_event(); - } - - if (k->get_keycode() == KEY_DELETE && k->is_pressed()) { - emit_signal("delete_nodes_request"); + } else if (p_ev->is_action("ui_graph_delete")) { + emit_signal(SNAME("delete_nodes_request")); accept_event(); } } @@ -1074,14 +1346,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; } } @@ -1089,6 +1362,7 @@ void GraphEdit::set_connection_activity(const StringName &p_from, int p_from_por void GraphEdit::clear_connections() { connections.clear(); + minimap->update(); update(); connections_layer->update(); } @@ -1098,20 +1372,21 @@ 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(); if (is_visible_in_tree()) { @@ -1120,6 +1395,7 @@ void GraphEdit::set_zoom_custom(float p_zoom, const Vector2 &p_center) { v_scroll->set_value(ofs.y); } + _update_zoom_label(); update(); } @@ -1127,6 +1403,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; } @@ -1155,19 +1486,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() { @@ -1175,7 +1506,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) { @@ -1229,10 +1566,571 @@ void GraphEdit::_snap_value_changed(double) { update(); } +void GraphEdit::set_minimap_size(Vector2 p_size) { + minimap->set_size(p_size); + Vector2 minimap_size = minimap->get_size(); // The size might've been adjusted by the minimum size. + + minimap->set_anchors_preset(Control::PRESET_BOTTOM_RIGHT); + minimap->set_offset(Side::SIDE_LEFT, -minimap_size.x - MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_TOP, -minimap_size.y - MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_RIGHT, -MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_BOTTOM, -MINIMAP_OFFSET); + minimap->update(); +} + +Vector2 GraphEdit::get_minimap_size() const { + return minimap->get_size(); +} + +void GraphEdit::set_minimap_opacity(float p_opacity) { + minimap->set_modulate(Color(1, 1, 1, p_opacity)); + minimap->update(); +} + +float GraphEdit::get_minimap_opacity() const { + Color minimap_modulate = minimap->get_modulate(); + return minimap_modulate.a; +} + +void GraphEdit::set_minimap_enabled(bool p_enable) { + minimap_button->set_pressed(p_enable); + minimap->update(); +} + +bool GraphEdit::is_minimap_enabled() const { + return minimap_button->is_pressed(); +} + +void GraphEdit::_minimap_toggled() { + if (is_minimap_enabled()) { + minimap->set_visible(true); + minimap->update(); + } else { + minimap->set_visible(false); + } +} + +void GraphEdit::set_connection_lines_thickness(float p_thickness) { + lines_thickness = p_thickness; + update(); +} + +float GraphEdit::get_connection_lines_thickness() const { + return lines_thickness; +} + +void GraphEdit::set_connection_lines_antialiased(bool p_antialiased) { + lines_antialiased = p_antialiased; + update(); +} + +bool GraphEdit::is_connection_lines_antialiased() const { + return lines_antialiased; +} + 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); @@ -1250,31 +2148,76 @@ 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", "p_zoom"), &GraphEdit::set_zoom); + 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); ClassDB::bind_method(D_METHOD("set_use_snap", "enable"), &GraphEdit::set_use_snap); ClassDB::bind_method(D_METHOD("is_using_snap"), &GraphEdit::is_using_snap); + ClassDB::bind_method(D_METHOD("set_connection_lines_thickness", "pixels"), &GraphEdit::set_connection_lines_thickness); + ClassDB::bind_method(D_METHOD("get_connection_lines_thickness"), &GraphEdit::get_connection_lines_thickness); + + ClassDB::bind_method(D_METHOD("set_connection_lines_antialiased", "pixels"), &GraphEdit::set_connection_lines_antialiased); + ClassDB::bind_method(D_METHOD("is_connection_lines_antialiased"), &GraphEdit::is_connection_lines_antialiased); + + ClassDB::bind_method(D_METHOD("set_minimap_size", "size"), &GraphEdit::set_minimap_size); + ClassDB::bind_method(D_METHOD("get_minimap_size"), &GraphEdit::get_minimap_size); + ClassDB::bind_method(D_METHOD("set_minimap_opacity", "opacity"), &GraphEdit::set_minimap_opacity); + ClassDB::bind_method(D_METHOD("get_minimap_opacity"), &GraphEdit::get_minimap_opacity); + + ClassDB::bind_method(D_METHOD("set_minimap_enabled", "enable"), &GraphEdit::set_minimap_enabled); + ClassDB::bind_method(D_METHOD("is_minimap_enabled"), &GraphEdit::is_minimap_enabled); + 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_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"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "minimap_opacity"), "set_minimap_opacity", "get_minimap_opacity"); ADD_SIGNAL(MethodInfo("connection_request", PropertyInfo(Variant::STRING_NAME, "from"), PropertyInfo(Variant::INT, "from_slot"), PropertyInfo(Variant::STRING_NAME, "to"), PropertyInfo(Variant::INT, "to_slot"))); ADD_SIGNAL(MethodInfo("disconnection_request", PropertyInfo(Variant::STRING_NAME, "from"), PropertyInfo(Variant::INT, "from_slot"), PropertyInfo(Variant::STRING_NAME, "to"), PropertyInfo(Variant::INT, "to_slot"))); @@ -1283,7 +2226,7 @@ void GraphEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("copy_nodes_request")); ADD_SIGNAL(MethodInfo("paste_nodes_request")); ADD_SIGNAL(MethodInfo("node_selected", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); - ADD_SIGNAL(MethodInfo("node_unselected", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); + ADD_SIGNAL(MethodInfo("node_deselected", PropertyInfo(Variant::OBJECT, "node", PROPERTY_HINT_RESOURCE_TYPE, "Node"))); ADD_SIGNAL(MethodInfo("connection_to_empty", PropertyInfo(Variant::STRING_NAME, "from"), PropertyInfo(Variant::INT, "from_slot"), PropertyInfo(Variant::VECTOR2, "release_position"))); ADD_SIGNAL(MethodInfo("connection_from_empty", PropertyInfo(Variant::STRING_NAME, "to"), PropertyInfo(Variant::INT, "to_slot"), PropertyInfo(Variant::VECTOR2, "release_position"))); ADD_SIGNAL(MethodInfo("delete_nodes_request")); @@ -1295,12 +2238,17 @@ void GraphEdit::_bind_methods() { GraphEdit::GraphEdit() { set_focus_mode(FOCUS_ALL); - awaiting_scroll_offset_update = false; - top_layer = nullptr; + // 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); top_layer->set_mouse_filter(MOUSE_FILTER_PASS); - top_layer->set_anchors_and_margins_preset(Control::PRESET_WIDE); + 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)); @@ -1319,13 +2267,6 @@ GraphEdit::GraphEdit() { v_scroll->set_name("_v_scroll"); top_layer->add_child(v_scroll); - updating = false; - connecting = false; - right_disconnects = false; - - box_selecting = false; - dragging = false; - //set large minmax so it can scroll even if not resized yet h_scroll->set_min(-10000); h_scroll->set_max(10000); @@ -1336,12 +2277,22 @@ GraphEdit::GraphEdit() { h_scroll->connect("value_changed", callable_mp(this, &GraphEdit::_scroll_moved)); v_scroll->connect("value_changed", callable_mp(this, &GraphEdit::_scroll_moved)); - zoom = 1; - zoom_hb = memnew(HBoxContainer); 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); @@ -1380,7 +2331,38 @@ GraphEdit::GraphEdit() { snap_amount->connect("value_changed", callable_mp(this, &GraphEdit::_snap_value_changed)); zoom_hb->add_child(snap_amount); - setting_scroll_ofs = false; - just_disconnected = false; + minimap_button = memnew(Button); + minimap_button->set_flat(true); + minimap_button->set_toggle_mode(true); + minimap_button->set_tooltip(RTR("Enable grid minimap.")); + minimap_button->connect("pressed", callable_mp(this, &GraphEdit::_minimap_toggled)); + minimap_button->set_pressed(true); + 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; + + minimap = memnew(GraphEditMinimap(this)); + top_layer->add_child(minimap); + minimap->set_name("_minimap"); + minimap->set_modulate(Color(1, 1, 1, minimap_opacity)); + minimap->set_mouse_filter(MOUSE_FILTER_PASS); + minimap->set_custom_minimum_size(Vector2(50, 50)); + minimap->set_size(minimap_size); + minimap->set_anchors_preset(Control::PRESET_BOTTOM_RIGHT); + minimap->set_offset(Side::SIDE_LEFT, -minimap_size.x - MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_TOP, -minimap_size.y - MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_RIGHT, -MINIMAP_OFFSET); + minimap->set_offset(Side::SIDE_BOTTOM, -MINIMAP_OFFSET); + minimap->connect("draw", callable_mp(this, &GraphEdit::_minimap_draw)); + set_clip_contents(true); } diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h index d87bd41f27..44e50aa3c2 100644 --- a/scene/gui/graph_edit.h +++ b/scene/gui/graph_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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" @@ -45,6 +46,7 @@ class GraphEditFilter : public Control { GDCLASS(GraphEditFilter, Control); friend class GraphEdit; + friend class GraphEditMinimap; GraphEdit *ge; virtual bool has_point(const Point2 &p_point) const override; @@ -52,6 +54,43 @@ public: GraphEditFilter(GraphEdit *p_edit); }; +class GraphEditMinimap : public Control { + GDCLASS(GraphEditMinimap, Control); + + friend class GraphEdit; + friend class GraphEditFilter; + GraphEdit *ge; + +protected: +public: + GraphEditMinimap(GraphEdit *p_edit); + + void update_minimap(); + Rect2 get_camera_rect(); + +private: + Vector2 minimap_padding; + Vector2 minimap_offset; + Vector2 graph_proportions; + Vector2 graph_padding; + Vector2 camera_position; + Vector2 camera_size; + + bool is_pressing; + bool is_resizing; + + Vector2 _get_render_size(); + Vector2 _get_graph_offset(); + Vector2 _get_graph_size(); + + Vector2 _convert_from_graph_position(const Vector2 &p_position); + Vector2 _convert_to_graph_position(const Vector2 &p_position); + + virtual void gui_input(const Ref<InputEvent> &p_ev) override; + + void _adjust_graph_scroll(const Vector2 &p_offset); +}; + class GraphEdit : public Control { GDCLASS(GraphEdit, Control); @@ -59,12 +98,13 @@ public: struct Connection { StringName from; StringName to; - int from_port; - int to_port; - float activity; + int from_port = 0; + int to_port = 0; + float activity = 0.0; }; private: + Label *zoom_label; Button *zoom_minus; Button *zoom_reset; Button *zoom_plus; @@ -72,69 +112,82 @@ private: Button *snap_button; SpinBox *snap_amount; - void _zoom_minus(); - void _zoom_reset(); - void _zoom_plus(); + Button *minimap_button; + + Button *layout_button; HScrollBar *h_scroll; VScrollBar *v_scroll; - float port_grab_distance_horizontal; + float port_grab_distance_horizontal = 0.0; float port_grab_distance_vertical; - bool connecting; + bool connecting = false; String connecting_from; - bool connecting_out; - int connecting_index; - int connecting_type; + bool connecting_out = false; + int connecting_index = 0; + int connecting_type = 0; Color connecting_color; - bool connecting_target; + bool connecting_target = false; Vector2 connecting_to; String connecting_target_to; int connecting_target_index; - bool just_disconnected; - bool connecting_valid; + bool just_disconnected = false; + bool connecting_valid = false; Vector2 click_pos; - bool dragging; - bool just_selected; - bool moving_selection; + bool dragging = false; + bool just_selected = false; + bool moving_selection = false; Vector2 drag_accum; - float zoom; + 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; - bool box_selection_mode_additive; + bool box_selecting = false; + bool box_selection_mode_additive = false; Point2 box_selecting_from; Point2 box_selecting_to; Rect2 box_selecting_rect; - List<GraphNode *> previus_selected; + List<GraphNode *> previous_selected; - bool setting_scroll_ofs; - bool right_disconnects; - bool updating; - bool awaiting_scroll_offset_update; + bool setting_scroll_ofs = false; + bool right_disconnects = false; + bool updating = false; + bool awaiting_scroll_offset_update = false; List<Connection> connections; - 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; + float lines_thickness = 2.0f; + bool lines_antialiased = true; - void _draw_cos_line(CanvasItem *p_where, const Vector2 &p_from, const Vector2 &p_to, const Color &p_color, const Color &p_to_color); + 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); + void _graph_node_slot_updated(int p_index, Node *p_gn); 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(); + void _minimap_draw(); void _update_scroll_offset(); Array _get_connection_list() const; @@ -147,7 +200,7 @@ private: uint32_t type_a; uint32_t type_b; }; - uint64_t key; + uint64_t key = 0; }; bool operator<(const ConnType &p_type) const { @@ -171,14 +224,36 @@ private: void _snap_toggled(); void _snap_value_changed(double); + friend class GraphEditMinimap; + void _minimap_toggled(); + 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); @@ -196,7 +271,28 @@ 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); + float get_minimap_opacity() const; + + void set_minimap_enabled(bool p_enable); + bool is_minimap_enabled() const; + GraphEditFilter *get_top_layer() const { return top_layer; } + GraphEditMinimap *get_minimap() const { return minimap; } void get_connection_list(List<Connection> *r_connections) const; void set_right_disconnects(bool p_enable); @@ -219,8 +315,16 @@ public: int get_snap() const; void set_snap(int p_snap); + void set_connection_lines_thickness(float p_thickness); + float get_connection_lines_thickness() const; + + void set_connection_lines_antialiased(bool p_antialiased); + bool is_connection_lines_antialiased() const; + HBoxContainer *get_zoom_hbox(); + void arrange_nodes(); + GraphEdit(); }; diff --git a/scene/gui/graph_node.cpp b/scene/gui/graph_node.cpp index 4454e87017..08c8c60d7a 100644 --- a/scene/gui/graph_node.cpp +++ b/scene/gui/graph_node.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -30,13 +30,43 @@ #include "graph_node.h" +#include "core/string/translation.h" + +struct _MinSizeCache { + int min_size; + bool will_stretch; + int final_size; +}; + bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { - if (!p_name.operator String().begins_with("slot/")) { + 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); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + notify_property_list_changed(); + return true; + } + + if (!str.begins_with("slot/")) { return false; } - int idx = p_name.operator String().get_slice("/", 1).to_int(); - String what = p_name.operator String().get_slice("/", 2); + int idx = str.get_slice("/", 1).to_int(); + String what = str.get_slice("/", 2); Slot si; if (slot_info.has(idx)) { @@ -47,6 +77,8 @@ bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { si.enable_left = p_value; } else if (what == "left_type") { si.type_left = p_value; + } else if (what == "left_icon") { + si.custom_slot_left = p_value; } else if (what == "left_color") { si.color_left = p_value; } else if (what == "right_enabled") { @@ -55,22 +87,37 @@ bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { si.type_right = p_value; } else if (what == "right_color") { si.color_right = p_value; + } else if (what == "right_icon") { + si.custom_slot_right = p_value; } else { return false; } - set_slot(idx, si.enable_left, si.type_left, si.color_left, si.enable_right, si.type_right, si.color_right); + set_slot(idx, si.enable_left, si.type_left, si.color_left, si.enable_right, si.type_right, si.color_right, si.custom_slot_left, si.custom_slot_right); update(); return true; } bool GraphNode::_get(const StringName &p_name, Variant &r_ret) const { - if (!p_name.operator String().begins_with("slot/")) { + 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; + } + } + + if (!str.begins_with("slot/")) { return false; } - int idx = p_name.operator String().get_slice("/", 1).to_int(); - String what = p_name.operator String().get_slice("/", 2); + int idx = str.get_slice("/", 1).to_int(); + String what = str.get_slice("/", 2); Slot si; if (slot_info.has(idx)) { @@ -83,12 +130,16 @@ bool GraphNode::_get(const StringName &p_name, Variant &r_ret) const { r_ret = si.type_left; } else if (what == "left_color") { r_ret = si.color_left; + } else if (what == "left_icon") { + r_ret = si.custom_slot_left; } else if (what == "right_enabled") { r_ret = si.enable_right; } else if (what == "right_type") { r_ret = si.type_right; } else if (what == "right_color") { r_ret = si.color_right; + } else if (what == "right_icon") { + r_ret = si.custom_slot_right; } else { return false; } @@ -97,6 +148,12 @@ bool GraphNode::_get(const StringName &p_name, Variant &r_ret) const { } void GraphNode::_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)); + int idx = 0; for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); @@ -109,24 +166,34 @@ void GraphNode::_get_property_list(List<PropertyInfo> *p_list) const { p_list->push_back(PropertyInfo(Variant::BOOL, base + "left_enabled")); p_list->push_back(PropertyInfo(Variant::INT, base + "left_type")); p_list->push_back(PropertyInfo(Variant::COLOR, base + "left_color")); + p_list->push_back(PropertyInfo(Variant::OBJECT, base + "left_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL)); p_list->push_back(PropertyInfo(Variant::BOOL, base + "right_enabled")); p_list->push_back(PropertyInfo(Variant::INT, base + "right_type")); p_list->push_back(PropertyInfo(Variant::COLOR, base + "right_color")); + p_list->push_back(PropertyInfo(Variant::OBJECT, base + "right_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_STORE_IF_NULL)); idx++; } } void GraphNode::_resort() { - int sep = get_theme_constant("separation"); - Ref<StyleBox> sb = get_theme_stylebox("frame"); - bool first = true; + /** First pass, determine minimum size AND amount of stretchable elements */ - Size2 minsize; + Size2i new_size = get_size(); + Ref<StyleBox> sb = get_theme_stylebox(SNAME("frame")); + + int sep = get_theme_constant(SNAME("separation")); + + bool first = true; + int children_count = 0; + int stretch_min = 0; + int stretch_avail = 0; + float stretch_ratio_total = 0; + Map<Control *, _MinSizeCache> min_size_cache; for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); - if (!c) { + if (!c || !c->is_visible_in_tree()) { continue; } if (c->is_set_as_top_level()) { @@ -134,38 +201,120 @@ void GraphNode::_resort() { } Size2i size = c->get_combined_minimum_size(); + _MinSizeCache msc; - minsize.y += size.y; - minsize.x = MAX(minsize.x, size.x); + stretch_min += size.height; + msc.min_size = size.height; + msc.will_stretch = c->get_v_size_flags() & SIZE_EXPAND; - if (first) { - first = false; - } else { - minsize.y += sep; + if (msc.will_stretch) { + stretch_avail += msc.min_size; + stretch_ratio_total += c->get_stretch_ratio(); } + msc.final_size = msc.min_size; + min_size_cache[c] = msc; + children_count++; } - int vofs = 0; - int w = get_size().x - sb->get_minimum_size().x; + if (children_count == 0) { + return; + } + + int stretch_max = new_size.height - (children_count - 1) * sep; + int stretch_diff = stretch_max - stretch_min; + if (stretch_diff < 0) { + //avoid negative stretch space + stretch_diff = 0; + } + + stretch_avail += stretch_diff - sb->get_margin(SIDE_BOTTOM) - sb->get_margin(SIDE_TOP); //available stretch space. + /** Second, pass successively to discard elements that can't be stretched, this will run while stretchable + elements exist */ + while (stretch_ratio_total > 0) { // first of all, don't even be here if no stretchable objects exist + bool refit_successful = true; //assume refit-test will go well + + 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()) { + continue; + } + if (c->is_set_as_top_level()) { + continue; + } + + ERR_FAIL_COND(!min_size_cache.has(c)); + _MinSizeCache &msc = min_size_cache[c]; + + if (msc.will_stretch) { //wants to stretch + //let's see if it can really stretch + + int final_pixel_size = stretch_avail * c->get_stretch_ratio() / stretch_ratio_total; + if (final_pixel_size < msc.min_size) { + //if available stretching area is too small for widget, + //then remove it from stretching area + msc.will_stretch = false; + stretch_ratio_total -= c->get_stretch_ratio(); + refit_successful = false; + stretch_avail -= msc.min_size; + msc.final_size = msc.min_size; + break; + } else { + msc.final_size = final_pixel_size; + } + } + } + + if (refit_successful) { //uf refit went well, break + break; + } + } + + /** Final pass, draw and stretch elements **/ + + int ofs = sb->get_margin(SIDE_TOP); + + first = true; + int idx = 0; cache_y.clear(); + int w = new_size.width - sb->get_minimum_size().x; + for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); - if (!c) { + if (!c || !c->is_visible_in_tree()) { continue; } if (c->is_set_as_top_level()) { continue; } - Size2i size = c->get_combined_minimum_size(); + _MinSizeCache &msc = min_size_cache[c]; - Rect2 r(sb->get_margin(MARGIN_LEFT), sb->get_margin(MARGIN_TOP) + vofs, w, size.y); + if (first) { + first = false; + } else { + ofs += sep; + } + + int from = ofs; + int to = ofs + msc.final_size; + + if (msc.will_stretch && idx == children_count - 1) { + //adjust so the last one always fits perfect + //compensating for numerical imprecision + + to = new_size.height - sb->get_margin(SIDE_BOTTOM); + } + + int size = to - from; - fit_child_in_rect(c, r); - cache_y.push_back(vofs + size.y * 0.5); + Rect2 rect(sb->get_margin(SIDE_LEFT), from, w, size); - vofs += size.y + sep; + fit_child_in_rect(c, rect); + cache_y.push_back(from - sb->get_margin(SIDE_TOP) + size * 0.5); + + ofs = to; + idx++; } update(); @@ -174,14 +323,14 @@ 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; } - if (Rect2(0, 0, get_size().width, comment->get_margin(MARGIN_TOP)).has_point(p_point)) { + if (Rect2(0, 0, get_size().width, comment->get_margin(SIDE_TOP)).has_point(p_point)) { return true; } @@ -206,20 +355,19 @@ 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"); - Ref<Font> title_font = get_theme_font("title_font"); - 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"); - icofs.y += sb->get_margin(MARGIN_TOP); + int edgeofs = get_theme_constant(SNAME("port_offset")); + icofs.y += sb->get_margin(SIDE_TOP); draw_style_box(sb, Rect2(Point2(), get_size())); @@ -227,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; } @@ -241,9 +389,10 @@ void GraphNode::_notification(int p_what) { w -= close->get_width(); } - draw_string(title_font, Point2(sb->get_margin(MARGIN_LEFT) + title_h_offset, -title_font->get_height() + title_font->get_ascent() + title_offset), title, title_color, w); + title_buf->set_width(w); + title_buf->draw(get_canvas_item(), Point2(sb->get_margin(SIDE_LEFT) + title_h_offset, -title_buf->get_size().y + title_offset), title_color); if (show_close) { - Vector2 cpos = Point2(w + sb->get_margin(MARGIN_LEFT) + close_h_offset, -close->get_height() + close_offset); + Vector2 cpos = Point2(w + sb->get_margin(SIDE_LEFT) + close_h_offset, -close->get_height() + close_offset); draw_texture(close, cpos, close_color); close_rect.position = cpos; close_rect.size = close->get_size(); @@ -285,16 +434,36 @@ void GraphNode::_notification(int p_what) { _resort(); } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_THEME_CHANGED: { + _shape(); + minimum_size_changed(); + update(); } break; } } +void GraphNode::_shape() { + 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) { + title_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + title_buf->set_direction((TextServer::Direction)text_direction); + } + title_buf->add_string(title, font, font_size, opentype_features, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); +} + void GraphNode::set_slot(int p_idx, bool p_enable_left, int p_type_left, const Color &p_color_left, bool p_enable_right, int p_type_right, const Color &p_color_right, const Ref<Texture2D> &p_custom_left, const Ref<Texture2D> &p_custom_right) { - ERR_FAIL_COND(p_idx < 0); + ERR_FAIL_COND_MSG(p_idx < 0, vformat("Cannot set slot with p_idx (%d) lesser than zero.", p_idx)); - if (!p_enable_left && p_type_left == 0 && p_color_left == Color(1, 1, 1, 1) && !p_enable_right && p_type_right == 0 && p_color_right == Color(1, 1, 1, 1)) { + if (!p_enable_left && p_type_left == 0 && p_color_left == Color(1, 1, 1, 1) && + !p_enable_right && p_type_right == 0 && p_color_right == Color(1, 1, 1, 1) && + !p_custom_left.is_valid() && !p_custom_right.is_valid()) { slot_info.erase(p_idx); return; } @@ -311,6 +480,8 @@ void GraphNode::set_slot(int p_idx, bool p_enable_left, int p_type_left, const C slot_info[p_idx] = s; update(); connpos_dirty = true; + + emit_signal(SNAME("slot_updated"), p_idx); } void GraphNode::clear_slot(int p_idx) { @@ -332,6 +503,26 @@ bool GraphNode::is_slot_enabled_left(int p_idx) const { return slot_info[p_idx].enable_left; } +void GraphNode::set_slot_enabled_left(int p_idx, bool p_enable_left) { + ERR_FAIL_COND_MSG(p_idx < 0, vformat("Cannot set enable_left for the slot with p_idx (%d) lesser than zero.", p_idx)); + + slot_info[p_idx].enable_left = p_enable_left; + update(); + connpos_dirty = true; + + emit_signal(SNAME("slot_updated"), p_idx); +} + +void GraphNode::set_slot_type_left(int p_idx, int p_type_left) { + ERR_FAIL_COND_MSG(!slot_info.has(p_idx), vformat("Cannot set type_left for the slot '%d' because it hasn't been enabled.", p_idx)); + + slot_info[p_idx].type_left = p_type_left; + update(); + connpos_dirty = true; + + emit_signal(SNAME("slot_updated"), p_idx); +} + int GraphNode::get_slot_type_left(int p_idx) const { if (!slot_info.has(p_idx)) { return 0; @@ -339,6 +530,16 @@ int GraphNode::get_slot_type_left(int p_idx) const { return slot_info[p_idx].type_left; } +void GraphNode::set_slot_color_left(int p_idx, const Color &p_color_left) { + ERR_FAIL_COND_MSG(!slot_info.has(p_idx), vformat("Cannot set color_left for the slot '%d' because it hasn't been enabled.", p_idx)); + + slot_info[p_idx].color_left = p_color_left; + update(); + connpos_dirty = true; + + emit_signal(SNAME("slot_updated"), p_idx); +} + Color GraphNode::get_slot_color_left(int p_idx) const { if (!slot_info.has(p_idx)) { return Color(1, 1, 1, 1); @@ -353,6 +554,26 @@ bool GraphNode::is_slot_enabled_right(int p_idx) const { return slot_info[p_idx].enable_right; } +void GraphNode::set_slot_enabled_right(int p_idx, bool p_enable_right) { + ERR_FAIL_COND_MSG(p_idx < 0, vformat("Cannot set enable_right for the slot with p_idx (%d) lesser than zero.", p_idx)); + + slot_info[p_idx].enable_right = p_enable_right; + update(); + connpos_dirty = true; + + emit_signal(SNAME("slot_updated"), p_idx); +} + +void GraphNode::set_slot_type_right(int p_idx, int p_type_right) { + ERR_FAIL_COND_MSG(!slot_info.has(p_idx), vformat("Cannot set type_right for the slot '%d' because it hasn't been enabled.", p_idx)); + + slot_info[p_idx].type_right = p_type_right; + update(); + connpos_dirty = true; + + emit_signal(SNAME("slot_updated"), p_idx); +} + int GraphNode::get_slot_type_right(int p_idx) const { if (!slot_info.has(p_idx)) { return 0; @@ -360,6 +581,16 @@ int GraphNode::get_slot_type_right(int p_idx) const { return slot_info[p_idx].type_right; } +void GraphNode::set_slot_color_right(int p_idx, const Color &p_color_right) { + ERR_FAIL_COND_MSG(!slot_info.has(p_idx), vformat("Cannot set color_right for the slot '%d' because it hasn't been enabled.", p_idx)); + + slot_info[p_idx].color_right = p_color_right; + update(); + connpos_dirty = true; + + emit_signal(SNAME("slot_updated"), p_idx); +} + Color GraphNode::get_slot_color_right(int p_idx) const { if (!slot_info.has(p_idx)) { return Color(1, 1, 1, 1); @@ -368,16 +599,14 @@ Color GraphNode::get_slot_color_right(int p_idx) const { } Size2 GraphNode::get_minimum_size() const { - Ref<Font> title_font = get_theme_font("title_font"); - - 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_font->get_string_size(title).x; + 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(); } @@ -410,8 +639,9 @@ void GraphNode::set_title(const String &p_title) { return; } title = p_title; + _shape(); + update(); - _change_notify("title"); minimum_size_changed(); } @@ -419,14 +649,62 @@ String GraphNode::get_title() const { return title; } -void GraphNode::set_offset(const Vector2 &p_offset) { - offset = p_offset; - emit_signal("offset_changed"); +void GraphNode::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; + _shape(); + update(); + } +} + +Control::TextDirection GraphNode::get_text_direction() const { + return text_direction; +} + +void GraphNode::clear_opentype_features() { + opentype_features.clear(); + _shape(); update(); } -Vector2 GraphNode::get_offset() const { - return offset; +void GraphNode::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; + _shape(); + update(); + } +} + +int GraphNode::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]; +} + +void GraphNode::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + _shape(); + update(); + } +} + +String GraphNode::get_language() const { + return language; +} + +void GraphNode::set_position_offset(const Vector2 &p_offset) { + position_offset = p_offset; + emit_signal(SNAME("position_offset_changed")); + update(); +} + +Vector2 GraphNode::get_position_offset() const { + return position_offset; } void GraphNode::set_selected(bool p_selected) { @@ -440,9 +718,9 @@ bool GraphNode::is_selected() { void GraphNode::set_drag(bool p_drag) { if (p_drag) { - drag_from = get_offset(); + drag_from = get_position_offset(); } else { - emit_signal("dragged", drag_from, get_offset()); //useful for undo/redo + emit_signal(SNAME("dragged"), drag_from, get_position_offset()); //useful for undo/redo } } @@ -460,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; @@ -479,9 +757,9 @@ void GraphNode::_connpos_update() { continue; } - Size2i size = c->get_combined_minimum_size(); + Size2i size = c->get_rect().size; - int y = sb->get_margin(MARGIN_TOP) + vofs; + int y = sb->get_margin(SIDE_TOP) + vofs; int h = size.y; if (slot_info.has(idx)) { @@ -585,22 +863,24 @@ 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; if (mb.is_valid()) { ERR_FAIL_COND_MSG(get_parent_control() == nullptr, "GraphNode must be the child of a GraphEdit node."); - if (mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { Vector2 mpos = Vector2(mb->get_position().x, mb->get_position().y); 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; @@ -610,10 +890,10 @@ 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() == BUTTON_LEFT) { + if (!mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { resizing = false; } } @@ -624,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); } } @@ -658,20 +938,38 @@ bool GraphNode::is_resizable() const { void GraphNode::_bind_methods() { ClassDB::bind_method(D_METHOD("set_title", "title"), &GraphNode::set_title); ClassDB::bind_method(D_METHOD("get_title"), &GraphNode::get_title); - ClassDB::bind_method(D_METHOD("_gui_input"), &GraphNode::_gui_input); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &GraphNode::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &GraphNode::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &GraphNode::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &GraphNode::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &GraphNode::clear_opentype_features); + 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("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); + ClassDB::bind_method(D_METHOD("is_slot_enabled_left", "idx"), &GraphNode::is_slot_enabled_left); + ClassDB::bind_method(D_METHOD("set_slot_enabled_left", "idx", "enable_left"), &GraphNode::set_slot_enabled_left); + + ClassDB::bind_method(D_METHOD("set_slot_type_left", "idx", "type_left"), &GraphNode::set_slot_type_left); ClassDB::bind_method(D_METHOD("get_slot_type_left", "idx"), &GraphNode::get_slot_type_left); + + ClassDB::bind_method(D_METHOD("set_slot_color_left", "idx", "color_left"), &GraphNode::set_slot_color_left); ClassDB::bind_method(D_METHOD("get_slot_color_left", "idx"), &GraphNode::get_slot_color_left); + ClassDB::bind_method(D_METHOD("is_slot_enabled_right", "idx"), &GraphNode::is_slot_enabled_right); + ClassDB::bind_method(D_METHOD("set_slot_enabled_right", "idx", "enable_right"), &GraphNode::set_slot_enabled_right); + + ClassDB::bind_method(D_METHOD("set_slot_type_right", "idx", "type_right"), &GraphNode::set_slot_type_right); ClassDB::bind_method(D_METHOD("get_slot_type_right", "idx"), &GraphNode::get_slot_type_right); + + ClassDB::bind_method(D_METHOD("set_slot_color_right", "idx", "color_right"), &GraphNode::set_slot_color_right); ClassDB::bind_method(D_METHOD("get_slot_color_right", "idx"), &GraphNode::get_slot_color_right); - ClassDB::bind_method(D_METHOD("set_offset", "offset"), &GraphNode::set_offset); - ClassDB::bind_method(D_METHOD("get_offset"), &GraphNode::get_offset); + ClassDB::bind_method(D_METHOD("set_position_offset", "offset"), &GraphNode::set_position_offset); + ClassDB::bind_method(D_METHOD("get_position_offset"), &GraphNode::get_position_offset); ClassDB::bind_method(D_METHOD("set_comment", "comment"), &GraphNode::set_comment); ClassDB::bind_method(D_METHOD("is_comment"), &GraphNode::is_comment); @@ -699,14 +997,17 @@ void GraphNode::_bind_methods() { ClassDB::bind_method(D_METHOD("get_overlay"), &GraphNode::get_overlay); ADD_PROPERTY(PropertyInfo(Variant::STRING, "title"), "set_title", "get_title"); - ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset"), "set_offset", "get_offset"); + 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::VECTOR2, "position_offset"), "set_position_offset", "get_position_offset"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_close"), "set_show_close_button", "is_close_button_visible"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "resizable"), "set_resizable", "is_resizable"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "selected"), "set_selected", "is_selected"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "comment"), "set_comment", "is_comment"); ADD_PROPERTY(PropertyInfo(Variant::INT, "overlay", PROPERTY_HINT_ENUM, "Disabled,Breakpoint,Position"), "set_overlay", "get_overlay"); - ADD_SIGNAL(MethodInfo("offset_changed")); + ADD_SIGNAL(MethodInfo("position_offset_changed")); + ADD_SIGNAL(MethodInfo("slot_updated", PropertyInfo(Variant::INT, "idx"))); ADD_SIGNAL(MethodInfo("dragged", PropertyInfo(Variant::VECTOR2, "from"), PropertyInfo(Variant::VECTOR2, "to"))); ADD_SIGNAL(MethodInfo("raise_request")); ADD_SIGNAL(MethodInfo("close_request")); @@ -718,12 +1019,6 @@ void GraphNode::_bind_methods() { } GraphNode::GraphNode() { - overlay = OVERLAY_DISABLED; - show_close = false; - connpos_dirty = true; + title_buf.instantiate(); set_mouse_filter(MOUSE_FILTER_STOP); - comment = false; - resizable = false; - resizing = false; - selected = false; } diff --git a/scene/gui/graph_node.h b/scene/gui/graph_node.h index 0cf6d9b09a..c7c7006bfc 100644 --- a/scene/gui/graph_node.h +++ b/scene/gui/graph_node.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -32,6 +32,7 @@ #define GRAPH_NODE_H #include "scene/gui/container.h" +#include "scene/resources/text_line.h" class GraphNode : public Container { GDCLASS(GraphNode, Container); @@ -45,32 +46,29 @@ public: private: struct Slot { - bool enable_left; - int type_left; - Color color_left; - bool enable_right; - int type_right; - Color color_right; + bool enable_left = false; + int type_left = 0; + Color color_left = Color(1, 1, 1, 1); + bool enable_right = false; + int type_right = 0; + Color color_right = Color(1, 1, 1, 1); Ref<Texture2D> custom_slot_left; Ref<Texture2D> custom_slot_right; - - Slot() { - enable_left = false; - type_left = 0; - color_left = Color(1, 1, 1, 1); - enable_right = false; - type_right = 0; - color_right = Color(1, 1, 1, 1); - } }; String title; - bool show_close; - Vector2 offset; - bool comment; - bool resizable; + Ref<TextLine> title_buf; + + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + + bool show_close = false; + Vector2 position_offset; + bool comment = false; + bool resizable = false; - bool resizing; + bool resizing = false; Vector2 resizing_from; Vector2 resizing_from_size; @@ -80,7 +78,7 @@ private: struct ConnCache { Vector2 pos; - int type; + int type = 0; Color color; }; @@ -89,18 +87,19 @@ private: Map<int, Slot> slot_info; - bool connpos_dirty; + bool connpos_dirty = true; void _connpos_update(); void _resort(); + void _shape(); Vector2 drag_from; - bool selected; + bool selected = false; - Overlay overlay; + 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(); @@ -114,18 +113,40 @@ public: void set_slot(int p_idx, bool p_enable_left, int p_type_left, const Color &p_color_left, bool p_enable_right, int p_type_right, const Color &p_color_right, const Ref<Texture2D> &p_custom_left = Ref<Texture2D>(), const Ref<Texture2D> &p_custom_right = Ref<Texture2D>()); void clear_slot(int p_idx); void clear_all_slots(); + bool is_slot_enabled_left(int p_idx) const; + void set_slot_enabled_left(int p_idx, bool p_enable_left); + + void set_slot_type_left(int p_idx, int p_type_left); int get_slot_type_left(int p_idx) const; + + void set_slot_color_left(int p_idx, const Color &p_color_left); Color get_slot_color_left(int p_idx) const; + bool is_slot_enabled_right(int p_idx) const; + void set_slot_enabled_right(int p_idx, bool p_enable_right); + + void set_slot_type_right(int p_idx, int p_type_right); int get_slot_type_right(int p_idx) const; + + void set_slot_color_right(int p_idx, const Color &p_color_right); Color get_slot_color_right(int p_idx) const; void set_title(const String &p_title); String get_title() const; - void set_offset(const Vector2 &p_offset); - Vector2 get_offset() const; + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + + void set_position_offset(const Vector2 &p_offset); + Vector2 get_position_offset() const; void set_selected(bool p_selected); bool is_selected(); diff --git a/scene/gui/grid_container.cpp b/scene/gui/grid_container.cpp index 2f37461c4d..1107e3a4af 100644 --- a/scene/gui/grid_container.cpp +++ b/scene/gui/grid_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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); @@ -141,6 +141,7 @@ void GridContainer::_notification(int p_what) { // Finally, fit the nodes. int col_expand = col_expanded.size() > 0 ? remaining_space.width / col_expanded.size() : 0; int row_expand = row_expanded.size() > 0 ? remaining_space.height / row_expanded.size() : 0; + bool rtl = is_layout_rtl(); int col_ofs = 0; int row_ofs = 0; @@ -156,24 +157,37 @@ void GridContainer::_notification(int p_what) { valid_controls_index++; if (col == 0) { - col_ofs = 0; + if (rtl) { + col_ofs = get_size().width; + } else { + col_ofs = 0; + } if (row > 0) { row_ofs += (row_expanded.has(row - 1) ? row_expand : row_minh[row - 1]) + vsep; } } - Point2 p(col_ofs, row_ofs); - Size2 s(col_expanded.has(col) ? col_expand : col_minw[col], row_expanded.has(row) ? row_expand : row_minh[row]); - - fit_child_in_rect(c, Rect2(p, s)); - - col_ofs += s.width + hsep; + if (rtl) { + Size2 s(col_expanded.has(col) ? col_expand : col_minw[col], row_expanded.has(row) ? row_expand : row_minh[row]); + Point2 p(col_ofs - s.width, row_ofs); + fit_child_in_rect(c, Rect2(p, s)); + col_ofs -= s.width + hsep; + } else { + Point2 p(col_ofs, row_ofs); + Size2 s(col_expanded.has(col) ? col_expand : col_minw[col], row_expanded.has(row) ? row_expand : row_minh[row]); + fit_child_in_rect(c, Rect2(p, s)); + col_ofs += s.width + hsep; + } } } break; case NOTIFICATION_THEME_CHANGED: { minimum_size_changed(); } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + queue_sort(); + } break; } } @@ -199,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; @@ -247,6 +261,4 @@ Size2 GridContainer::get_minimum_size() const { return ms; } -GridContainer::GridContainer() { - columns = 1; -} +GridContainer::GridContainer() {} diff --git a/scene/gui/grid_container.h b/scene/gui/grid_container.h index 79d4aee284..9b43a5bc7e 100644 --- a/scene/gui/grid_container.h +++ b/scene/gui/grid_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,7 +36,7 @@ class GridContainer : public Container { GDCLASS(GridContainer, Container); - int columns; + int columns = 1; protected: void _notification(int p_what); diff --git a/scene/gui/item_list.cpp b/scene/gui/item_list.cpp index 6708b18e0a..8297de9f30 100644 --- a/scene/gui/item_list.cpp +++ b/scene/gui/item_list.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -31,47 +31,76 @@ #include "item_list.h" #include "core/config/project_settings.h" #include "core/os/os.h" +#include "core/string/translation.h" -void ItemList::add_item(const String &p_item, const Ref<Texture2D> &p_texture, bool p_selectable) { +void ItemList::_shape(int p_idx) { + Item &item = items.write[p_idx]; + + item.text_buf->clear(); + if (item.text_direction == Control::TEXT_DIRECTION_INHERITED) { + item.text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + item.text_buf->set_direction((TextServer::Direction)item.text_direction); + } + 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) { Item item; item.icon = p_texture; item.icon_transposed = false; item.icon_region = Rect2i(); item.icon_modulate = Color(1, 1, 1, 1); item.text = p_item; + item.text_buf.instantiate(); item.selectable = p_selectable; item.selected = false; item.disabled = false; item.tooltip_enabled = true; item.custom_bg = Color(0, 0, 0, 0); items.push_back(item); + int item_id = items.size() - 1; + + _shape(items.size() - 1); update(); shape_changed = true; + return item_id; } -void ItemList::add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable) { +int ItemList::add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable) { Item item; item.icon = p_item; item.icon_transposed = false; item.icon_region = Rect2i(); item.icon_modulate = Color(1, 1, 1, 1); //item.text=p_item; + item.text_buf.instantiate(); item.selectable = p_selectable; item.selected = false; item.disabled = false; item.tooltip_enabled = true; item.custom_bg = Color(0, 0, 0, 0); items.push_back(item); + int item_id = items.size() - 1; update(); shape_changed = true; + return item_id; } void ItemList::set_item_text(int p_idx, const String &p_text) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].text = p_text; + _shape(p_idx); update(); shape_changed = true; } @@ -81,6 +110,61 @@ String ItemList::get_item_text(int p_idx) const { return items[p_idx].text; } +void ItemList::set_item_text_direction(int p_idx, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_idx, items.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (items[p_idx].text_direction != p_text_direction) { + items.write[p_idx].text_direction = p_text_direction; + _shape(p_idx); + update(); + } +} + +Control::TextDirection ItemList::get_item_text_direction(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, items.size(), TEXT_DIRECTION_INHERITED); + return items[p_idx].text_direction; +} + +void ItemList::clear_item_opentype_features(int p_idx) { + ERR_FAIL_INDEX(p_idx, items.size()); + items.write[p_idx].opentype_features.clear(); + _shape(p_idx); + update(); +} + +void ItemList::set_item_opentype_feature(int p_idx, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_idx, items.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!items[p_idx].opentype_features.has(tag) || (int)items[p_idx].opentype_features[tag] != p_value) { + items.write[p_idx].opentype_features[tag] = p_value; + _shape(p_idx); + update(); + } +} + +int ItemList::get_item_opentype_feature(int p_idx, const String &p_name) const { + ERR_FAIL_INDEX_V(p_idx, items.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!items[p_idx].opentype_features.has(tag)) { + return -1; + } + return items[p_idx].opentype_features[tag]; +} + +void ItemList::set_item_language(int p_idx, const String &p_language) { + ERR_FAIL_INDEX(p_idx, items.size()); + if (items[p_idx].language != p_language) { + items.write[p_idx].language = p_language; + _shape(p_idx); + update(); + } +} + +String ItemList::get_item_language(int p_idx) const { + ERR_FAIL_INDEX_V(p_idx, items.size(), ""); + return items[p_idx].language; +} + void ItemList::set_item_tooltip_enabled(int p_idx, const bool p_enabled) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].tooltip_enabled = p_enabled; @@ -163,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 { @@ -175,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 { @@ -255,7 +341,7 @@ void ItemList::select(int p_idx, bool p_single) { update(); } -void ItemList::unselect(int p_idx) { +void ItemList::deselect(int p_idx) { ERR_FAIL_INDEX(p_idx, items.size()); if (select_mode != SELECT_MULTI) { @@ -267,7 +353,7 @@ void ItemList::unselect(int p_idx) { update(); } -void ItemList::unselect_all() { +void ItemList::deselect_all() { if (items.size() < 1) { return; } @@ -324,6 +410,9 @@ void ItemList::remove_item(int p_idx) { ERR_FAIL_INDEX(p_idx, items.size()); items.remove(p_idx); + if (current == p_idx) { + current = -1; + } update(); shape_changed = true; defer_select_single = -1; @@ -361,9 +450,19 @@ bool ItemList::is_same_column_width() const { void ItemList::set_max_text_lines(int p_lines) { ERR_FAIL_COND(p_lines < 1); - max_text_lines = p_lines; - update(); - shape_changed = true; + if (max_text_lines != p_lines) { + max_text_lines = 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); + } + } + shape_changed = true; + update(); + } } int ItemList::get_max_text_lines() const { @@ -392,9 +491,18 @@ ItemList::SelectMode ItemList::get_select_mode() const { void ItemList::set_icon_mode(IconMode p_mode) { ERR_FAIL_INDEX((int)p_mode, 2); - icon_mode = p_mode; - update(); - shape_changed = true; + if (icon_mode != p_mode) { + icon_mode = p_mode; + 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); + } else { + items.write[i].text_buf->set_flags(TextServer::BREAK_NONE); + } + } + shape_changed = true; + update(); + } } ItemList::IconMode ItemList::get_icon_mode() const { @@ -429,7 +537,9 @@ 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(); Ref<InputEventMouseMotion> mm = p_event; @@ -440,21 +550,25 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; - if (defer_select_single >= 0 && mb.is_valid() && mb->get_button_index() == BUTTON_LEFT && !mb->is_pressed()) { + 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; } - if (mb.is_valid() && (mb->get_button_index() == BUTTON_LEFT || (allow_rmb_select && mb->get_button_index() == BUTTON_RIGHT)) && mb->is_pressed()) { + 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(); + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } + int closest = -1; for (int i = 0; i < items.size(); i++) { @@ -472,11 +586,11 @@ void ItemList::_gui_input(const Ref<InputEvent> &p_event) { if (closest != -1) { int i = closest; - if (select_mode == SELECT_MULTI && items[i].selected && mb->get_command()) { - unselect(i); - emit_signal("multi_selected", i, false); + if (select_mode == SELECT_MULTI && items[i].selected && mb->is_command_pressed()) { + deselect(i); + emit_signal(SNAME("multi_selected"), i, false); - } else if (select_mode == SELECT_MULTI && mb->get_shift() && current >= 0 && current < items.size() && current != i) { + } else if (select_mode == SELECT_MULTI && mb->is_shift_pressed() && current >= 0 && current < items.size() && current != i) { int from = current; int to = i; if (i < current) { @@ -486,57 +600,57 @@ 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() == BUTTON_RIGHT) { - emit_signal("item_rmb_selected", i, get_local_mouse_position()); + if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { + emit_signal(SNAME("item_rmb_selected"), i, get_local_mouse_position()); } } else { - if (!mb->is_doubleclick() && !mb->get_command() && select_mode == SELECT_MULTI && items[i].selectable && !items[i].disabled && items[i].selected && mb->get_button_index() == BUTTON_LEFT) { + 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) { defer_select_single = i; return; } - if (items[i].selected && mb->get_button_index() == BUTTON_RIGHT) { - emit_signal("item_rmb_selected", i, get_local_mouse_position()); + if (items[i].selected && mb->get_button_index() == MOUSE_BUTTON_RIGHT) { + emit_signal(SNAME("item_rmb_selected"), i, get_local_mouse_position()); } else { bool selected = items[i].selected; - select(i, select_mode == SELECT_SINGLE || !mb->get_command()); + select(i, select_mode == SELECT_SINGLE || !mb->is_command_pressed()); 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() == BUTTON_RIGHT) { - emit_signal("item_rmb_selected", i, get_local_mouse_position()); - } else if (/*select_mode==SELECT_SINGLE &&*/ mb->is_doubleclick()) { - emit_signal("item_activated", i); + if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { + emit_signal(SNAME("item_rmb_selected"), i, get_local_mouse_position()); + } else if (/*select_mode==SELECT_SINGLE &&*/ mb->is_double_click()) { + emit_signal(SNAME("item_activated"), i); } } } return; } - if (mb->get_button_index() == BUTTON_RIGHT) { - emit_signal("rmb_clicked", mb->get_position()); + if (mb->get_button_index() == MOUSE_BUTTON_RIGHT) { + 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() == BUTTON_WHEEL_UP && mb->is_pressed()) { + 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); } - if (mb.is_valid() && mb->get_button_index() == BUTTON_WHEEL_DOWN && mb->is_pressed()) { + if (mb.is_valid() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && mb->is_pressed()) { scroll_bar->set_value(scroll_bar->get_value() + scroll_bar->get_page() * mb->get_factor() / 8); } @@ -552,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; @@ -567,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(); } @@ -582,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; } @@ -596,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(); } @@ -608,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; @@ -622,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(); @@ -636,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(); } @@ -647,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(); } @@ -657,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) { - unselect(current); - emit_signal("multi_selected", current, false); + deselect(current); + emit_signal(SNAME("multi_selected"), current, false); } } } else if (p_event->is_action("ui_accept")) { - search_string = ""; //any mousepress cance + 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; @@ -703,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; } @@ -749,14 +863,22 @@ void ItemList::_notification(int p_what) { update(); } + if ((p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED) || (p_what == NOTIFICATION_TRANSLATION_CHANGED) || (p_what == NOTIFICATION_THEME_CHANGED)) { + for (int i = 0; i < items.size(); i++) { + _shape(i); + } + shape_changed = true; + update(); + } + 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_margin(MARGIN_LEFT, ANCHOR_END, -mw); - scroll_bar->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - scroll_bar->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, bg->get_margin(MARGIN_TOP)); - scroll_bar->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, -bg->get_margin(MARGIN_BOTTOM)); + scroll_bar->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -mw); + scroll_bar->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + scroll_bar->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, bg->get_margin(SIDE_TOP)); + scroll_bar->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, -bg->get_margin(SIDE_BOTTOM)); Size2 size = get_size(); @@ -767,35 +889,29 @@ 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"); - - 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<Font> font = get_theme_font("font"); - Color guide_color = get_theme_color("guide_color"); - Color font_color = get_theme_color("font_color"); - Color font_color_selected = get_theme_color("font_color_selected"); - int font_height = font->get_height(); - Vector<int> line_size_cache; - Vector<int> line_limit_cache; - - if (max_text_lines) { - line_size_cache.resize(max_text_lines); - line_limit_cache.resize(max_text_lines); - } + 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(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(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); } if (shape_changed) { - float max_column_width = 0; + float max_column_width = 0.0; //1- compute item minimum sizes for (int i = 0; i < items.size(); i++) { @@ -817,13 +933,19 @@ void ItemList::_notification(int p_what) { } if (items[i].text != "") { - Size2 s = font->get_string_size(items[i].text); - //s.width=MIN(s.width,fixed_column_width); + 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(); if (icon_mode == ICON_MODE_TOP) { minsize.x = MAX(minsize.x, s.width); if (max_text_lines > 0) { - minsize.y += (font_height + line_separation) * max_text_lines; + minsize.y += s.height + line_separation * max_text_lines; } else { minsize.y += s.height; } @@ -986,6 +1108,10 @@ void ItemList::_notification(int p_what) { r.position.x -= hseparation / 2; r.size.x += hseparation; + if (rtl) { + r.position.x = size.width - r.position.x - r.size.x; + } + draw_style_box(sbsel, r); } if (items[i].custom_bg.a > 0.001) { @@ -998,6 +1124,10 @@ void ItemList::_notification(int p_what) { r.position.x -= hseparation / 2; r.size.x += hseparation; + if (rtl) { + r.position.x = size.width - r.position.x - r.size.x; + } + draw_rect(r, items[i].custom_bg); } @@ -1018,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; @@ -1049,17 +1176,25 @@ void ItemList::_notification(int p_what) { } Rect2 region = (items[i].icon_region.size.x == 0 || items[i].icon_region.size.y == 0) ? Rect2(Vector2(), items[i].icon->get_size()) : Rect2(items[i].icon_region); + + if (rtl) { + draw_rect.position.x = size.width - draw_rect.position.x - draw_rect.size.x; + } draw_texture_rect_region(items[i].icon, draw_rect, region, modulate, items[i].icon_transposed); } if (items[i].tag_icon.is_valid()) { - draw_texture(items[i].tag_icon, items[i].rect_cache.position + base_ofs); + Point2 draw_pos = items[i].rect_cache.position; + if (rtl) { + draw_pos.x = size.width - draw_pos.x - items[i].tag_icon->get_width(); + } + draw_texture(items[i].tag_icon, draw_pos + base_ofs); } if (items[i].text != "") { int max_len = -1; - Vector2 size2 = font->get_string_size(items[i].text); + Vector2 size2 = items[i].text_buf->get_size(); if (fixed_column_width) { max_len = fixed_column_width; } else if (same_column_width) { @@ -1068,51 +1203,26 @@ void ItemList::_notification(int p_what) { max_len = size2.x; } - Color modulate = items[i].selected ? font_color_selected : (items[i].custom_fg != Color() ? items[i].custom_fg : font_color); + Color modulate = items[i].selected ? font_selected_color : (items[i].custom_fg != Color() ? items[i].custom_fg : font_color); if (items[i].disabled) { modulate.a *= 0.5; } if (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { - int ss = items[i].text.length(); - float ofs = 0; - int line = 0; - for (int j = 0; j <= ss; j++) { - int cs = j < ss ? font->get_char_size(items[i].text[j], items[i].text[j + 1]).x : 0; - if (ofs + cs > max_len || j == ss) { - line_limit_cache.write[line] = j; - line_size_cache.write[line] = ofs; - line++; - ofs = 0; - if (line >= max_text_lines) { - break; - } - } else { - ofs += cs; - } - } - - line = 0; - ofs = 0; - - text_ofs.y += font->get_ascent(); - text_ofs = text_ofs.floor(); text_ofs += base_ofs; text_ofs += items[i].rect_cache.position; - FontDrawer drawer(font, Color(1, 1, 1)); - for (int j = 0; j < ss; j++) { - if (j == line_limit_cache[line]) { - line++; - ofs = 0; - if (line >= max_text_lines) { - break; - } - } - ofs += drawer.draw_char(get_canvas_item(), text_ofs + Vector2(ofs + (max_len - line_size_cache[line]) / 2, line * (font_height + line_separation)).floor(), items[i].text[j], items[i].text[j + 1], modulate); + if (rtl) { + text_ofs.x = size.width - text_ofs.x - max_len; } - //special multiline mode + items.write[i].text_buf->set_align(HALIGN_CENTER); + + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(get_canvas_item(), text_ofs, outline_size, font_outline_color); + } + + items[i].text_buf->draw(get_canvas_item(), text_ofs, modulate); } else { if (fixed_column_width > 0) { size2.x = MIN(size2.x, fixed_column_width); @@ -1124,12 +1234,26 @@ void ItemList::_notification(int p_what) { text_ofs.y += (items[i].rect_cache.size.height - size2.y) / 2; } - text_ofs.y += font->get_ascent(); - text_ofs = text_ofs.floor(); text_ofs += base_ofs; text_ofs += items[i].rect_cache.position; - draw_string(font, text_ofs, items[i].text, modulate, max_len + 1); + if (rtl) { + text_ofs.x = size.width - text_ofs.x - max_len; + } + + items.write[i].text_buf->set_width(max_len); + + if (rtl) { + items.write[i].text_buf->set_align(HALIGN_RIGHT); + } else { + items.write[i].text_buf->set_align(HALIGN_LEFT); + } + + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(get_canvas_item(), text_ofs, outline_size, font_outline_color); + } + + items[i].text_buf->draw(get_canvas_item(), text_ofs, modulate); } } @@ -1140,6 +1264,11 @@ void ItemList::_notification(int p_what) { r.size.y += vseparation; r.position.x -= hseparation / 2; r.size.x += hseparation; + + if (rtl) { + r.position.x = size.width - r.position.x - r.size.x; + } + draw_style_box(cursor, r); } } @@ -1166,7 +1295,7 @@ void ItemList::_notification(int p_what) { } const int y = base_ofs.y + separators[i]; - draw_line(Vector2(bg->get_margin(MARGIN_LEFT), y), Vector2(width, y), guide_color); + draw_line(Vector2(bg->get_margin(SIDE_LEFT), y), Vector2(width, y), guide_color); } } } @@ -1177,10 +1306,14 @@ 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(); + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } + int closest = -1; int closest_dist = 0x7FFFFFFF; @@ -1206,15 +1339,19 @@ int ItemList::get_item_at_position(const Point2 &p_pos, bool p_exact) const { } bool ItemList::is_pos_at_end_of_items(const Point2 &p_pos) const { - if (items.empty()) { + if (items.is_empty()) { return true; } 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(); + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } + Rect2 endrect = items[items.size() - 1].rect_cache; return (pos.y > endrect.position.y + endrect.size.y); } @@ -1356,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)); @@ -1366,6 +1518,16 @@ void ItemList::_bind_methods() { ClassDB::bind_method(D_METHOD("set_item_icon", "idx", "icon"), &ItemList::set_item_icon); ClassDB::bind_method(D_METHOD("get_item_icon", "idx"), &ItemList::get_item_icon); + ClassDB::bind_method(D_METHOD("set_item_text_direction", "idx", "direction"), &ItemList::set_item_text_direction); + ClassDB::bind_method(D_METHOD("get_item_text_direction", "idx"), &ItemList::get_item_text_direction); + + ClassDB::bind_method(D_METHOD("set_item_opentype_feature", "idx", "tag", "value"), &ItemList::set_item_opentype_feature); + ClassDB::bind_method(D_METHOD("get_item_opentype_feature", "idx", "tag"), &ItemList::get_item_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_item_opentype_features", "idx"), &ItemList::clear_item_opentype_features); + + ClassDB::bind_method(D_METHOD("set_item_language", "idx", "language"), &ItemList::set_item_language); + ClassDB::bind_method(D_METHOD("get_item_language", "idx"), &ItemList::get_item_language); + ClassDB::bind_method(D_METHOD("set_item_icon_transposed", "idx", "transposed"), &ItemList::set_item_icon_transposed); ClassDB::bind_method(D_METHOD("is_item_icon_transposed", "idx"), &ItemList::is_item_icon_transposed); @@ -1397,8 +1559,8 @@ void ItemList::_bind_methods() { ClassDB::bind_method(D_METHOD("get_item_tooltip", "idx"), &ItemList::get_item_tooltip); ClassDB::bind_method(D_METHOD("select", "idx", "single"), &ItemList::select, DEFVAL(true)); - ClassDB::bind_method(D_METHOD("unselect", "idx"), &ItemList::unselect); - ClassDB::bind_method(D_METHOD("unselect_all"), &ItemList::unselect_all); + ClassDB::bind_method(D_METHOD("deselect", "idx"), &ItemList::deselect); + ClassDB::bind_method(D_METHOD("deselect_all"), &ItemList::deselect_all); ClassDB::bind_method(D_METHOD("is_selected", "idx"), &ItemList::is_selected); ClassDB::bind_method(D_METHOD("get_selected_items"), &ItemList::get_selected_items); @@ -1452,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"); @@ -1464,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"); @@ -1491,34 +1655,12 @@ void ItemList::_bind_methods() { } ItemList::ItemList() { - current = -1; - - select_mode = SELECT_SINGLE; - icon_mode = ICON_MODE_LEFT; - - fixed_column_width = 0; - same_column_width = false; - max_text_lines = 1; - max_columns = 1; - auto_height = false; - auto_height_value = 0.0f; - scroll_bar = memnew(VScrollBar); add_child(scroll_bar); - shape_changed = true; scroll_bar->connect("value_changed", callable_mp(this, &ItemList::_scroll_changed)); set_focus_mode(FOCUS_ALL); - current_columns = 1; - search_time_msec = 0; - ensure_selected_visible = false; - defer_select_single = -1; - allow_rmb_select = false; - allow_reselect = false; - do_autoscroll_to_bottom = false; - - icon_scale = 1.0f; set_clip_contents(true); } diff --git a/scene/gui/item_list.h b/scene/gui/item_list.h index 03f477940c..148fa7ba9f 100644 --- a/scene/gui/item_list.h +++ b/scene/gui/item_list.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -33,6 +33,7 @@ #include "scene/gui/control.h" #include "scene/gui/scroll_bar.h" +#include "scene/resources/text_paragraph.h" class ItemList : public Control { GDCLASS(ItemList, Control); @@ -51,15 +52,20 @@ public: private: struct Item { Ref<Texture2D> icon; - bool icon_transposed; + bool icon_transposed = false; Rect2i icon_region; Color icon_modulate; Ref<Texture2D> tag_icon; String text; - bool selectable; - bool selected; - bool disabled; - bool tooltip_enabled; + Ref<TextParagraph> text_buf; + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + + bool selectable = false; + bool selected = false; + bool disabled = false; + bool tooltip_enabled = false; Variant metadata; String tooltip; Color custom_fg; @@ -73,62 +79,75 @@ private: bool operator<(const Item &p_another) const { return text < p_another.text; } }; - int current; + int current = -1; - bool shape_changed; + bool shape_changed = true; - bool ensure_selected_visible; - bool same_column_width; + bool ensure_selected_visible = false; + bool same_column_width = false; - bool auto_height; - float auto_height_value; + bool auto_height = false; + float auto_height_value = 0.0; Vector<Item> items; Vector<int> separators; - SelectMode select_mode; - IconMode icon_mode; + 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; + uint64_t search_time_msec = 0; String search_string; - int current_columns; - int fixed_column_width; - int max_text_lines; - int max_columns; + int current_columns = 1; + int fixed_column_width = 0; + int max_text_lines = 1; + int max_columns = 1; Size2 fixed_icon_size; Size2 max_item_size_cache; - int defer_select_single; + int defer_select_single = -1; - bool allow_rmb_select; + bool allow_rmb_select = false; - bool allow_reselect; + bool allow_reselect = false; - real_t icon_scale; + real_t icon_scale = 1.0; - bool do_autoscroll_to_bottom; + bool do_autoscroll_to_bottom = false; Array _get_items() const; 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: void _notification(int p_what); static void _bind_methods(); public: - void add_item(const String &p_item, const Ref<Texture2D> &p_texture = Ref<Texture2D>(), bool p_selectable = true); - void add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable = true); + 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); void set_item_text(int p_idx, const String &p_text); String get_item_text(int p_idx) const; + void set_item_text_direction(int p_idx, TextDirection p_text_direction); + TextDirection get_item_text_direction(int p_idx) const; + + void set_item_opentype_feature(int p_idx, const String &p_name, int p_value); + int get_item_opentype_feature(int p_idx, const String &p_name) const; + void clear_item_opentype_features(int p_idx); + + void set_item_language(int p_idx, const String &p_language); + String get_item_language(int p_idx) const; + void set_item_icon(int p_idx, const Ref<Texture2D> &p_icon); Ref<Texture2D> get_item_icon(int p_idx) const; @@ -165,9 +184,12 @@ 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 unselect(int p_idx); - void unselect_all(); + void deselect(int p_idx); + void deselect_all(); bool is_selected(int p_idx) const; Vector<int> get_selected_items(); bool is_anything_selected(); diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index 9df63a3c71..3f87003423 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -34,27 +34,28 @@ #include "core/string/print_string.h" #include "core/string/translation.h" -void Label::set_autowrap(bool p_autowrap) { - if (autowrap == p_autowrap) { - return; - } +#include "servers/text_server.h" - autowrap = p_autowrap; - word_cache_dirty = true; +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) { uppercase = p_uppercase; - word_cache_dirty = true; + dirty = true; + update(); } @@ -62,19 +63,198 @@ bool Label::is_uppercase() const { return uppercase; } -int Label::get_line_height() const { - return get_theme_font("font")->get_height(); +int Label::get_line_height(int p_line) const { + 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(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(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM); + } + return h; + } else { + return font->get_height(get_theme_font_size(SNAME("font_size"))); + } +} + +void Label::_shape() { + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal"), SNAME("Label")); + int width = (get_size().width - style->get_minimum_size().width); + + if (dirty) { + TS->shaped_text_clear(text_rid); + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + TS->shaped_text_set_direction(text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction); + } + 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(); + + 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); + } + } + + if (xl_text.length() == 0) { + minsize = Size2(1, get_line_height()); + return; + } + + 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) { + minsize.width = TS->shaped_text_get_size(lines_rid[i]).x; + } + } + } + + 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++) { + 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; + } + + _update_visible(); + + if (autowrap_mode == AUTOWRAP_OFF || !clip || overrun_behavior == OVERRUN_NO_TRIMMING) { + minimum_size_changed(); + } +} + +void Label::_update_visible() { + 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) { + lines_visible = max_lines_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(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 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); + } + 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); + } } 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; - regenerate_word_cache(); update(); } @@ -83,63 +263,72 @@ void Label::_notification(int p_what) { RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), true); } - if (word_cache_dirty) { - regenerate_word_cache(); + if (dirty || lines_dirty) { + _shape(); } RID ci = get_canvas_item(); 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_color_shadow = get_theme_color("font_color_shadow"); - bool use_outline = get_theme_constant("shadow_as_outline"); - 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_modulate = get_theme_color("font_outline_modulate"); + 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())); - RenderingServer::get_singleton()->canvas_item_set_distance_field_mode(get_canvas_item(), font.is_valid() && font->is_distance_field_hint()); - - int font_h = font->get_height() + line_spacing; - - int lines_visible = (size.y + line_spacing) / font_h; - - real_t space_w = font->get_char_size(' ').width; - int chars_total = 0; - - int vbegin = 0, vsep = 0; + float total_h = 0.0; + int lines_visible = 0; - if (lines_visible > line_count) { - lines_visible = line_count; + // 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(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; + } + lines_visible++; } if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { lines_visible = max_lines_visible; } + int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); + + // 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(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 - (lines_visible * font_h - line_spacing)) / 2; + vbegin = (size.y - (total_h - line_spacing)) / 2; vsep = 0; } break; case VALIGN_BOTTOM: { - vbegin = size.y - (lines_visible * font_h - line_spacing); + vbegin = size.y - (total_h - line_spacing); vsep = 0; } break; case VALIGN_FILL: { vbegin = 0; if (lines_visible > 1) { - vsep = (size.y - (lines_visible * font_h - line_spacing)) / (lines_visible - 1); + vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1); } else { vsep = 0; } @@ -148,210 +337,167 @@ void Label::_notification(int p_what) { } } - WordCache *wc = word_cache; - if (!wc) { - return; - } - - int line = 0; - int line_to = lines_skipped + (lines_visible > 0 ? lines_visible : 1); - FontDrawer drawer(font, font_outline_modulate); - while (wc) { - /* handle lines not meant to be drawn quickly */ - if (line >= line_to) { - break; - } - if (line < lines_skipped) { - while (wc && wc->char_pos >= 0) { - wc = wc->next; - } - if (wc) { - wc = wc->next; - } - line++; - continue; - } - - /* handle lines normally */ - - if (wc->char_pos < 0) { - //empty line - wc = wc->next; - line++; - continue; - } - - WordCache *from = wc; - WordCache *to = wc; - - int taken = 0; - int spaces = 0; - while (to && to->char_pos >= 0) { - taken += to->pixel_width; - if (to->space_count) { - spaces += to->space_count; + int visible_glyphs = -1; + int glyhps_drawn = 0; + if (percent_visible < 1) { + int total_glyphs = 0; + for (int i = lines_skipped; i < last_line; i++) { + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(lines_rid[i]); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); + for (int j = 0; j < gl_size; j++) { + if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + total_glyphs++; + } } - to = to->next; } - bool can_fill = to && to->char_pos == WordCache::CHAR_WRAPLINE; - - float x_ofs = 0; + visible_glyphs = MIN(total_glyphs, visible_chars); + } + 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(TextServer::SPACING_TOP); switch (align) { case ALIGN_FILL: + 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: { - x_ofs = style->get_offset().x; + ofs.x = style->get_offset().x; } break; case ALIGN_CENTER: { - x_ofs = int(size.width - (taken + spaces * space_w)) / 2; + ofs.x = int(size.width - line_size.width) / 2; } break; case ALIGN_RIGHT: { - x_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (taken + spaces * space_w)); + ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); } break; } - float y_ofs = style->get_offset().y; - y_ofs += (line - lines_skipped) * font_h + font->get_ascent(); - y_ofs += vbegin + line * vsep; - - while (from != to) { - // draw a word - int pos = from->char_pos; - if (from->char_pos < 0) { - ERR_PRINT("BUG"); - return; - } - if (from->space_count) { - /* spacing */ - x_ofs += space_w * from->space_count; - if (can_fill && align == ALIGN_FILL && spaces) { - x_ofs += int((size.width - (taken + space_w * spaces)) / spaces); + 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 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, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, ofs, shadow_ofs); + ofs.x += trim_data.ellipsis_glyph_buf[gl_idx].advance; } } + } - if (font_color_shadow.a > 0) { - int chars_total_shadow = chars_total; //save chars drawn - float x_ofs_shadow = x_ofs; - for (int i = 0; i < from->word_len; i++) { - if (visible_chars < 0 || chars_total_shadow < visible_chars) { - char32_t c = xl_text[i + pos]; - char32_t n = xl_text[i + pos + 1]; - if (uppercase) { - c = String::char_uppercase(c); - n = String::char_uppercase(n); + // 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; } + } + } - float move = drawer.draw_char(ci, Point2(x_ofs_shadow, y_ofs) + shadow_ofs, c, n, font_color_shadow); - if (use_outline) { - drawer.draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, shadow_ofs.y), c, n, font_color_shadow); - drawer.draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(shadow_ofs.x, -shadow_ofs.y), c, n, font_color_shadow); - drawer.draw_char(ci, Point2(x_ofs_shadow, y_ofs) + Vector2(-shadow_ofs.x, -shadow_ofs.y), c, n, font_color_shadow); + // 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; } - x_ofs_shadow += move; - chars_total_shadow++; } } - } - for (int i = 0; i < from->word_len; i++) { - if (visible_chars < 0 || chars_total < visible_chars) { - char32_t c = xl_text[i + pos]; - char32_t n = xl_text[i + pos + 1]; - if (uppercase) { - c = String::char_uppercase(c); - n = String::char_uppercase(n); - } - x_ofs += drawer.draw_char(ci, Point2(x_ofs, y_ofs), c, n, font_color); - chars_total++; + // Draw glyph outlines and shadow. + draw_glyph(glyphs[j], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, ofs, shadow_ofs); + ofs.x += glyphs[j].advance; + glyhps_drawn++; + } + } + // 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, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, ofs, shadow_ofs); + ofs.x += trim_data.ellipsis_glyph_buf[gl_idx].advance; } } - from = from->next; } - - wc = to ? to->next : nullptr; - line++; + ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing + font->get_spacing(TextServer::SPACING_BOTTOM); } } if (p_what == NOTIFICATION_THEME_CHANGED) { - word_cache_dirty = true; + dirty = true; update(); } if (p_what == NOTIFICATION_RESIZED) { - word_cache_dirty = true; + lines_dirty = true; } } Size2 Label::get_minimum_size() const { - Size2 min_style = get_theme_stylebox("normal")->get_minimum_size(); - // don't want to mutable everything - if (word_cache_dirty) { - const_cast<Label *>(this)->regenerate_word_cache(); + if (dirty || lines_dirty) { + const_cast<Label *>(this)->_shape(); } - if (autowrap) { - return Size2(1, clip ? 1 : minsize.height) + min_style; - } else { - Size2 ms = minsize; - if (clip) { - ms.width = 1; - } - return ms + min_style; - } -} + Size2 min_size = minsize; -int Label::get_longest_line_width() const { - Ref<Font> font = get_theme_font("font"); - real_t max_line_width = 0; - real_t line_width = 0; + 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)); - for (int i = 0; i < xl_text.size(); i++) { - char32_t current = xl_text[i]; - if (uppercase) { - current = String::char_uppercase(current); - } - - if (current < 32) { - if (current == '\n') { - if (line_width > max_line_width) { - max_line_width = line_width; - } - line_width = 0; - } - } else { - real_t char_width = font->get_char_size(current, xl_text[i + 1]).width; - line_width += char_width; + 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 || overrun_behavior != OVERRUN_NO_TRIMMING) { + min_size.width = 1; } + return min_size + min_style; } - - if (line_width > max_line_width) { - max_line_width = line_width; - } - - // ceiling to ensure autowrapping does not cut text - return Math::ceil(max_line_width); } int Label::get_line_count() const { if (!is_inside_tree()) { return 1; } - if (word_cache_dirty) { - const_cast<Label *>(this)->regenerate_word_cache(); + if (dirty || lines_dirty) { + const_cast<Label *>(this)->_shape(); } - return line_count; + return lines_rid.size(); } int Label::get_visible_line_count() const { - int line_spacing = get_theme_constant("line_spacing"); - int font_h = get_theme_font("font")->get_height() + line_spacing; - int lines_visible = (get_size().height - get_theme_stylebox("normal")->get_minimum_size().height + line_spacing) / font_h; + 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(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; + } + lines_visible++; + } - if (lines_visible > line_count) { - lines_visible = line_count; + if (lines_visible > lines_rid.size()) { + lines_visible = lines_rid.size(); } if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { @@ -361,171 +507,14 @@ int Label::get_visible_line_count() const { return lines_visible; } -void Label::regenerate_word_cache() { - while (word_cache) { - WordCache *current = word_cache; - word_cache = current->next; - memdelete(current); - } - - int width; - if (autowrap) { - Ref<StyleBox> style = get_theme_stylebox("normal"); - width = MAX(get_size().width, get_custom_minimum_size().width) - style->get_minimum_size().width; - } else { - width = get_longest_line_width(); - } - - Ref<Font> font = get_theme_font("font"); - - real_t current_word_size = 0; - int word_pos = 0; - real_t line_width = 0; - int space_count = 0; - real_t space_width = font->get_char_size(' ').width; - int line_spacing = get_theme_constant("line_spacing"); - line_count = 1; - total_char_cache = 0; - - WordCache *last = nullptr; - - for (int i = 0; i <= xl_text.length(); i++) { - char32_t current = i < xl_text.length() ? xl_text[i] : L' '; //always a space at the end, so the algo works - - if (uppercase) { - current = String::char_uppercase(current); - } - - // ranges taken from http://www.unicodemap.org/ - // if your language is not well supported, consider helping improve - // the unicode support in Godot. - bool separatable = (current >= 0x2E08 && current <= 0xFAFF) || (current >= 0xFE30 && current <= 0xFE4F); - //current>=33 && (current < 65||current >90) && (current<97||current>122) && (current<48||current>57); - bool insert_newline = false; - real_t char_width = 0; - - if (current < 33) { - if (current_word_size > 0) { - WordCache *wc = memnew(WordCache); - if (word_cache) { - last->next = wc; - } else { - word_cache = wc; - } - last = wc; - - wc->pixel_width = current_word_size; - wc->char_pos = word_pos; - wc->word_len = i - word_pos; - wc->space_count = space_count; - current_word_size = 0; - space_count = 0; - } else if ((i == xl_text.length() || current == '\n') && last != nullptr && space_count != 0) { - //in case there are trailing white spaces we add a placeholder word cache with just the spaces - WordCache *wc = memnew(WordCache); - if (word_cache) { - last->next = wc; - } else { - word_cache = wc; - } - last = wc; - - wc->pixel_width = 0; - wc->char_pos = 0; - wc->word_len = 0; - wc->space_count = space_count; - current_word_size = 0; - space_count = 0; - } - - if (current == '\n') { - insert_newline = true; - } else if (current != ' ') { - total_char_cache++; - } - - if (i < xl_text.length() && xl_text[i] == ' ') { - if (line_width > 0 || last == nullptr || last->char_pos != WordCache::CHAR_WRAPLINE) { - space_count++; - line_width += space_width; - } else { - space_count = 0; - } - } - - } else { - // latin characters - if (current_word_size == 0) { - word_pos = i; - } - char_width = font->get_char_size(current, xl_text[i + 1]).width; - current_word_size += char_width; - line_width += char_width; - total_char_cache++; - - // allow autowrap to cut words when they exceed line width - if (autowrap && (current_word_size > width)) { - separatable = true; - } - } - - if ((autowrap && (line_width >= width) && ((last && last->char_pos >= 0) || separatable)) || insert_newline) { - if (separatable) { - if (current_word_size > 0) { - WordCache *wc = memnew(WordCache); - if (word_cache) { - last->next = wc; - } else { - word_cache = wc; - } - last = wc; - - wc->pixel_width = current_word_size - char_width; - wc->char_pos = word_pos; - wc->word_len = i - word_pos; - wc->space_count = space_count; - current_word_size = char_width; - word_pos = i; - } - } - - WordCache *wc = memnew(WordCache); - if (word_cache) { - last->next = wc; - } else { - word_cache = wc; - } - last = wc; - - wc->pixel_width = 0; - wc->char_pos = insert_newline ? WordCache::CHAR_NEWLINE : WordCache::CHAR_WRAPLINE; - - line_width = current_word_size; - line_count++; - space_count = 0; - } - } - - if (!autowrap) { - minsize.width = width; - } - - if (max_lines_visible > 0 && line_count > max_lines_visible) { - minsize.height = (font->get_height() * max_lines_visible) + (line_spacing * (max_lines_visible - 1)); - } else { - minsize.height = (font->get_height() * line_count) + (line_spacing * (line_count - 1)); - } - - if (!autowrap || !clip) { - //helps speed up some labels that may change a lot, as no resizing is requested. Do not change. - minimum_size_changed(); - } - word_cache_dirty = false; -} - void Label::set_align(Align p_align) { ERR_FAIL_INDEX((int)p_align, 4); - align = p_align; + if (align != p_align) { + if (align == ALIGN_FILL || p_align == ALIGN_FILL) { + lines_dirty = true; // Reshape lines. + } + align = p_align; + } update(); } @@ -548,12 +537,83 @@ void Label::set_text(const String &p_string) { return; } text = p_string; - xl_text = tr(p_string); - word_cache_dirty = true; + 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) { + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (text_direction != p_text_direction) { + text_direction = p_text_direction; + dirty = true; + update(); + } +} + +void Label::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + dirty = true; + update(); + } +} + +Control::StructuredTextParser Label::get_structured_text_bidi_override() const { + return st_parser; +} + +void Label::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + dirty = true; + update(); +} + +Array Label::get_structured_text_bidi_override_options() const { + return st_args; +} + +Control::TextDirection Label::get_text_direction() const { + return text_direction; +} + +void Label::clear_opentype_features() { + opentype_features.clear(); + dirty = true; + update(); +} + +void Label::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; + dirty = true; + update(); + } +} + +int Label::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]; +} + +void Label::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + dirty = true; + update(); + } +} + +String Label::get_language() const { + return language; } void Label::set_clip_text(bool p_clip) { @@ -566,6 +626,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; } @@ -573,9 +648,13 @@ String Label::get_text() const { void Label::set_visible_characters(int p_amount) { visible_chars = p_amount; if (get_total_character_count() > 0) { - percent_visible = (float)p_amount / (float)total_char_cache; + percent_visible = (float)p_amount / (float)get_total_character_count(); + } else { + percent_visible = 1.0; + } + if (p_amount == -1) { + lines_dirty = true; } - _change_notify("percent_visible"); update(); } @@ -587,12 +666,11 @@ 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; } - _change_notify("visible_chars"); update(); } @@ -601,7 +679,9 @@ float Label::get_percent_visible() const { } void Label::set_lines_skipped(int p_lines) { + ERR_FAIL_COND(p_lines < 0); lines_skipped = p_lines; + _update_visible(); update(); } @@ -611,6 +691,7 @@ int Label::get_lines_skipped() const { void Label::set_max_lines_visible(int p_lines) { max_lines_visible = p_lines; + _update_visible(); update(); } @@ -619,11 +700,61 @@ int Label::get_max_lines_visible() const { } int Label::get_total_character_count() const { - if (word_cache_dirty) { - const_cast<Label *>(this)->regenerate_word_cache(); + if (dirty || lines_dirty) { + const_cast<Label *>(this)->_shape(); + } + + return xl_text.length(); +} + +bool Label::_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); + dirty = true; + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + dirty = true; + update(); + } + } + notify_property_list_changed(); + return true; } - return total_char_cache; + return false; +} + +bool Label::_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 Label::_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 Label::_bind_methods() { @@ -633,13 +764,22 @@ void Label::_bind_methods() { ClassDB::bind_method(D_METHOD("get_valign"), &Label::get_valign); ClassDB::bind_method(D_METHOD("set_text", "text"), &Label::set_text); ClassDB::bind_method(D_METHOD("get_text"), &Label::get_text); - 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_text_direction", "direction"), &Label::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &Label::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &Label::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &Label::get_opentype_feature); + 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_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"), &Label::get_line_height); + ClassDB::bind_method(D_METHOD("get_line_height", "line"), &Label::get_line_height, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_line_count"), &Label::get_line_count); ClassDB::bind_method(D_METHOD("get_visible_line_count"), &Label::get_visible_line_count); ClassDB::bind_method(D_METHOD("get_total_character_count"), &Label::get_total_character_count); @@ -651,6 +791,10 @@ void Label::_bind_methods() { ClassDB::bind_method(D_METHOD("get_lines_skipped"), &Label::get_lines_skipped); ClassDB::bind_method(D_METHOD("set_max_lines_visible", "lines_visible"), &Label::set_max_lines_visible); ClassDB::bind_method(D_METHOD("get_max_lines_visible"), &Label::get_max_lines_visible); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &Label::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &Label::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &Label::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &Label::get_structured_text_bidi_override_options); BIND_ENUM_CONSTANT(ALIGN_LEFT); BIND_ENUM_CONSTANT(ALIGN_CENTER); @@ -662,28 +806,47 @@ 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"); ADD_PROPERTY(PropertyInfo(Variant::INT, "lines_skipped", PROPERTY_HINT_RANGE, "0,999,1"), "set_lines_skipped", "get_lines_skipped"); ADD_PROPERTY(PropertyInfo(Variant::INT, "max_lines_visible", PROPERTY_HINT_RANGE, "-1,999,1"), "set_max_lines_visible", "get_max_lines_visible"); + 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"); } Label::Label(const String &p_text) { + text_rid = TS->create_shaped_text(); + set_mouse_filter(MOUSE_FILTER_IGNORE); set_text(p_text); set_v_size_flags(SIZE_SHRINK_CENTER); } Label::~Label() { - while (word_cache) { - WordCache *current = word_cache; - word_cache = current->next; - memdelete(current); + for (int i = 0; i < lines_rid.size(); i++) { + TS->free(lines_rid[i]); } + lines_rid.clear(); + TS->free(text_rid); } diff --git a/scene/gui/label.h b/scene/gui/label.h index 510a716f5d..8b48eb9670 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -38,7 +38,6 @@ class Label : public Control { public: enum Align { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -46,54 +45,67 @@ public: }; enum VAlign { - VALIGN_TOP, VALIGN_CENTER, VALIGN_BOTTOM, 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; - int line_count = 0; bool uppercase = false; - int get_longest_line_width() const; - - struct WordCache { - enum { - CHAR_NEWLINE = -1, - CHAR_WRAPLINE = -2 - }; - int char_pos = 0; // if -1, then newline - int word_len = 0; - int pixel_width = 0; - int space_count = 0; - WordCache *next = nullptr; - }; + bool lines_dirty = true; + bool dirty = true; + RID text_rid; + Vector<RID> lines_rid; - bool word_cache_dirty = true; - void regenerate_word_cache(); + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; - float percent_visible = 1; + float percent_visible = 1.0; - WordCache *word_cache = nullptr; - int total_char_cache = 0; int visible_chars = -1; int lines_skipped = 0; int max_lines_visible = -1; + void _update_visible(); + void _shape(); + protected: void _notification(int p_what); static void _bind_methods(); - // bind helpers + + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + public: virtual Size2 get_minimum_size() const override; @@ -106,8 +118,24 @@ public: void set_text(const String &p_string); String get_text() const; - void set_autowrap(bool p_autowrap); - bool has_autowrap() const; + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() 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_autowrap_mode(AutowrapMode p_mode); + AutowrapMode get_autowrap_mode() const; void set_uppercase(bool p_uppercase); bool is_uppercase() const; @@ -119,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; @@ -128,7 +159,7 @@ public: void set_max_lines_visible(int p_lines); int get_max_lines_visible() const; - int get_line_height() const; + int get_line_height(int p_line = -1) const; int get_line_count() const; int get_visible_line_count() const; @@ -138,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 649f5a5f66..d524e78caa 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -30,6 +30,7 @@ #include "line_edit.h" +#include "core/input/input_map.h" #include "core/object/message_queue.h" #include "core/os/keyboard.h" #include "core/os/os.h" @@ -37,63 +38,258 @@ #include "core/string/translation.h" #include "label.h" #include "servers/display_server.h" +#include "servers/text_server.h" #ifdef TOOLS_ENABLED #include "editor/editor_scale.h" #include "editor/editor_settings.h" #endif #include "scene/main/window.h" -static bool _is_text_char(char32_t c) { - return !is_symbol(c); + +void LineEdit::_swap_current_input_direction() { + if (input_direction == TEXT_DIRECTION_LTR) { + input_direction = TEXT_DIRECTION_RTL; + } else { + input_direction = TEXT_DIRECTION_LTR; + } + set_caret_column(get_caret_column()); + update(); +} + +void LineEdit::_move_caret_left(bool p_select, bool p_move_by_word) { + if (selection.enabled && !p_select) { + set_caret_column(selection.begin); + deselect(); + return; + } + + shift_selection_check_pre(p_select); + + if (p_move_by_word) { + int cc = caret_column; + + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < cc) { + cc = words[i].x; + break; + } + } + + set_caret_column(cc); + } else { + if (caret_mid_grapheme_enabled) { + set_caret_column(get_caret_column() - 1); + } else { + set_caret_column(TS->shaped_text_prev_grapheme_pos(text_rid, get_caret_column())); + } + } + + shift_selection_check_post(p_select); +} + +void LineEdit::_move_caret_right(bool p_select, bool p_move_by_word) { + if (selection.enabled && !p_select) { + set_caret_column(selection.end); + deselect(); + return; + } + + shift_selection_check_pre(p_select); + + if (p_move_by_word) { + int cc = caret_column; + + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_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 (caret_mid_grapheme_enabled) { + set_caret_column(get_caret_column() + 1); + } else { + set_caret_column(TS->shaped_text_next_grapheme_pos(text_rid, get_caret_column())); + } + } + + shift_selection_check_post(p_select); +} + +void LineEdit::_move_caret_start(bool p_select) { + shift_selection_check_pre(p_select); + set_caret_column(0); + shift_selection_check_post(p_select); +} + +void LineEdit::_move_caret_end(bool p_select) { + shift_selection_check_pre(p_select); + set_caret_column(text.length()); + shift_selection_check_post(p_select); +} + +void LineEdit::_backspace(bool p_word, bool p_all_to_left) { + if (!editable) { + return; + } + + if (p_all_to_left) { + deselect(); + text = text.substr(0, caret_column); + _text_changed(); + return; + } + + if (selection.enabled) { + selection_delete(); + return; + } + + if (p_word) { + int cc = caret_column; + + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = words.size() - 1; i >= 0; i--) { + if (words[i].x < cc) { + cc = words[i].x; + break; + } + } + + delete_text(cc, caret_column); + + set_caret_column(cc); + } else { + delete_char(); + } } -void LineEdit::_gui_input(Ref<InputEvent> p_event) { +void LineEdit::_delete(bool p_word, bool p_all_to_right) { + if (!editable) { + return; + } + + if (p_all_to_right) { + deselect(); + text = text.substr(caret_column, text.length() - caret_column); + _shape(); + set_caret_column(0); + _text_changed(); + return; + } + + if (selection.enabled) { + selection_delete(); + return; + } + + int text_len = text.length(); + + if (caret_column == text_len) { + return; // Nothing to do. + } + + if (p_word) { + int cc = caret_column; + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = 0; i < words.size(); i++) { + if (words[i].y > cc) { + cc = words[i].y; + break; + } + } + + delete_text(caret_column, cc); + set_caret_column(caret_column); + } else { + if (caret_mid_grapheme_enabled) { + set_caret_column(caret_column + 1); + delete_char(); + } else { + int cc = caret_column; + set_caret_column(TS->shaped_text_next_grapheme_pos(text_rid, caret_column)); + delete_text(cc, caret_column); + } + } +} + +void LineEdit::gui_input(const Ref<InputEvent> &p_event) { + ERR_FAIL_COND(p_event.is_null()); + Ref<InputEventMouseButton> b = p_event; if (b.is_valid()) { - if (b->is_pressed() && b->get_button_index() == BUTTON_RIGHT && context_menu_enabled) { + if (ime_text.length() != 0) { + // Ignore mouse clicks in IME input mode. + 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)); - //menu->set_scale(get_global_transform().get_scale()); menu->popup(); grab_focus(); accept_event(); return; } - if (b->get_button_index() != BUTTON_LEFT) { + if (b->get_button_index() != MOUSE_BUTTON_LEFT) { return; } _reset_caret_blink_timer(); if (b->is_pressed()) { accept_event(); //don't pass event further when clicked on text field - if (!text.empty() && is_editable() && _is_over_clear_button(b->get_position())) { + if (!text.is_empty() && is_editable() && _is_over_clear_button(b->get_position())) { clear_button_status.press_attempt = true; clear_button_status.pressing_inside = true; + update(); return; } - shift_selection_check_pre(b->get_shift()); + shift_selection_check_pre(b->is_shift_pressed()); - set_cursor_at_pixel_pos(b->get_position().x); + set_caret_at_pixel_pos(b->get_position().x); - if (b->get_shift()) { - selection_fill_at_cursor(); + if (b->is_shift_pressed()) { + selection_fill_at_caret(); selection.creating = true; } else { - if (b->is_doubleclick() && selecting_enabled) { - selection.enabled = true; - selection.begin = 0; - selection.end = text.length(); - selection.doubleclick = true; + if (selecting_enabled) { + if (!b->is_double_click() && (OS::get_singleton()->get_ticks_msec() - selection.last_dblclk) < 600) { + // Triple-click select all. + selection.enabled = true; + selection.begin = 0; + selection.end = text.length(); + selection.double_click = true; + selection.last_dblclk = 0; + caret_column = selection.begin; + } else if (b->is_double_click()) { + // Double-click select word. + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(text_rid); + for (int i = 0; i < words.size(); i++) { + if (words[i].x < caret_column && words[i].y > caret_column) { + selection.enabled = true; + selection.begin = words[i].x; + selection.end = words[i].y; + selection.double_click = true; + selection.last_dblclk = OS::get_singleton()->get_ticks_msec(); + caret_column = selection.end; + break; + } + } + } } selection.drag_attempt = false; - if ((cursor_pos < selection.begin) || (cursor_pos > selection.end) || !selection.enabled) { + if ((caret_column < selection.begin) || (caret_column > selection.end) || !selection.enabled) { deselect(); - selection.cursor_start = cursor_pos; + selection.start_column = caret_column; selection.creating = true; } else if (selection.enabled) { selection.drag_attempt = true; @@ -103,7 +299,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { update(); } else { - if (!text.empty() && is_editable() && clear_button_enabled) { + if (!text.is_empty() && is_editable() && clear_button_enabled) { bool press_attempt = clear_button_status.press_attempt; clear_button_status.press_attempt = false; if (press_attempt && clear_button_status.pressing_inside && _is_over_clear_button(b->get_position())) { @@ -112,19 +308,13 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { } } - if ((!selection.creating) && (!selection.doubleclick)) { + if ((!selection.creating) && (!selection.double_click)) { deselect(); } selection.creating = false; - selection.doubleclick = false; + selection.double_click = false; - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - if (selection.enabled) { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); - } else { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, cursor_pos); - } - } + show_virtual_keyboard(); } update(); @@ -133,7 +323,7 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventMouseMotion> m = p_event; if (m.is_valid()) { - if (!text.empty() && is_editable() && clear_button_enabled) { + if (!text.is_empty() && is_editable() && clear_button_enabled) { bool last_press_inside = clear_button_status.pressing_inside; clear_button_status.pressing_inside = clear_button_status.press_attempt && _is_over_clear_button(m->get_position()); if (last_press_inside != clear_button_status.pressing_inside) { @@ -141,10 +331,10 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { } } - if (m->get_button_mask() & BUTTON_LEFT) { + if (m->get_button_mask() & MOUSE_BUTTON_LEFT) { if (selection.creating) { - set_cursor_at_pixel_pos(m->get_position().x); - selection_fill_at_cursor(); + set_caret_at_pixel_pos(m->get_position().x); + selection_fill_at_caret(); } } } @@ -156,455 +346,172 @@ void LineEdit::_gui_input(Ref<InputEvent> p_event) { return; } -#ifdef APPLE_STYLE_KEYS - if (k->get_control() && !k->get_shift() && !k->get_alt() && !k->get_command()) { - uint32_t remap_key = KEY_UNKNOWN; - switch (k->get_keycode()) { - case KEY_F: { - remap_key = KEY_RIGHT; - } break; - case KEY_B: { - remap_key = KEY_LEFT; - } break; - case KEY_P: { - remap_key = KEY_UP; - } break; - case KEY_N: { - remap_key = KEY_DOWN; - } break; - case KEY_D: { - remap_key = KEY_DELETE; - } break; - case KEY_H: { - remap_key = KEY_BACKSPACE; - } break; - case KEY_A: { - remap_key = KEY_HOME; - } break; - case KEY_E: { - remap_key = KEY_END; - } break; + if (context_menu_enabled) { + if (k->is_action("ui_menu", true)) { + _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)); + menu->popup(); + menu->grab_focus(); } + } - if (remap_key != KEY_UNKNOWN) { - k->set_keycode(remap_key); - k->set_control(false); + // 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(); } } -#endif - - unsigned int code = k->get_keycode(); - - if (k->get_command() && is_shortcut_keys_enabled()) { - bool handled = true; - - switch (code) { - case (KEY_X): { // CUT. - - if (editable) { - cut_text(); - } - - } break; - - case (KEY_C): { // COPY. - - copy_text(); - - } break; - - case (KEY_V): { // PASTE. - - if (editable) { - paste_text(); - } - - } break; - - case (KEY_Z): { // Undo/redo. - if (editable) { - if (k->get_shift()) { - redo(); - } else { - undo(); - } - } - } break; - - case (KEY_U): { // Delete from start to cursor. - - if (editable) { - deselect(); - text = text.substr(cursor_pos, text.length() - cursor_pos); - update_cached_width(); - set_cursor_position(0); - _text_changed(); - } - - } break; - case (KEY_Y): { // PASTE (Yank for unix users). - - if (editable) { - paste_text(); - } - - } break; - case (KEY_K): { // Delete from cursor_pos to end. - - if (editable) { - deselect(); - text = text.substr(0, cursor_pos); - _text_changed(); - } + if (is_shortcut_keys_enabled()) { + if (k->is_action("ui_copy", true)) { + copy_text(); + accept_event(); + return; + } - } break; - case (KEY_A): { // Select all. - select(); + if (k->is_action("ui_text_select_all", true)) { + select(); + accept_event(); + return; + } - } break; -#ifdef APPLE_STYLE_KEYS - case (KEY_LEFT): { // Go to start of text - like HOME key. - shift_selection_check_pre(k->get_shift()); - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case (KEY_RIGHT): { // Go to end of text - like END key. - shift_selection_check_pre(k->get_shift()); - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } break; - case (KEY_BACKSPACE): { - if (!editable) - break; + // Cut / Paste + if (k->is_action("ui_cut", true)) { + cut_text(); + accept_event(); + return; + } - // If selected, delete the selection - if (selection.enabled) { - selection_delete(); - break; - } + if (k->is_action("ui_paste", true)) { + paste_text(); + accept_event(); + return; + } - // Otherwise delete from cursor to beginning of text edit - int current_pos = get_cursor_position(); - if (current_pos != 0) { - delete_text(0, current_pos); - } - } break; -#endif - default: { - handled = false; - } + // Undo / Redo + if (k->is_action("ui_undo", true)) { + undo(); + accept_event(); + return; } - if (handled) { + if (k->is_action("ui_redo", true)) { + redo(); accept_event(); return; } } - _reset_caret_blink_timer(); - if (!k->get_metakey()) { - bool handled = true; - switch (code) { - case KEY_KP_ENTER: - case KEY_ENTER: { - emit_signal("text_entered", text); - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - DisplayServer::get_singleton()->virtual_keyboard_hide(); - } - - } break; - - case KEY_BACKSPACE: { - if (!editable) { - break; - } - - if (selection.enabled) { - selection_delete(); - break; - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - int cc = cursor_pos; - bool prev_char = false; - - while (cc > 0) { - bool ischar = _is_text_char(text[cc - 1]); - - if (prev_char && !ischar) { - break; - } - - prev_char = ischar; - cc--; - } - - delete_text(cc, cursor_pos); - - set_cursor_position(cc); - - } else { - delete_char(); - } - - } break; - case KEY_KP_4: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_LEFT: { -#ifndef APPLE_STYLE_KEYS - if (!k->get_alt()) { -#endif - if (selection.enabled && !k->get_shift()) { - set_cursor_position(selection.begin); - deselect(); - handled = true; - break; - } - - shift_selection_check_pre(k->get_shift()); -#ifndef APPLE_STYLE_KEYS - } -#endif - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - set_cursor_position(0); - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - bool prev_char = false; - int cc = cursor_pos; - - while (cc > 0) { - bool ischar = _is_text_char(text[cc - 1]); - - if (prev_char && !ischar) { - break; - } - - prev_char = ischar; - cc--; - } - - set_cursor_position(cc); - - } else { - set_cursor_position(get_cursor_position() - 1); - } - - shift_selection_check_post(k->get_shift()); - - } break; - case KEY_KP_6: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_RIGHT: { -#ifndef APPLE_STYLE_KEYS - if (!k->get_alt()) { -#endif - if (selection.enabled && !k->get_shift()) { - set_cursor_position(selection.end); - deselect(); - handled = true; - break; - } - - shift_selection_check_pre(k->get_shift()); -#ifndef APPLE_STYLE_KEYS - } -#endif - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - set_cursor_position(text.length()); - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - bool prev_char = false; - int cc = cursor_pos; - - while (cc < text.length()) { - bool ischar = _is_text_char(text[cc]); - - if (prev_char && !ischar) { - break; - } - - prev_char = ischar; - cc++; - } - - set_cursor_position(cc); - - } else { - set_cursor_position(get_cursor_position() + 1); - } - - shift_selection_check_post(k->get_shift()); - - } break; - case KEY_UP: { - shift_selection_check_pre(k->get_shift()); - if (get_cursor_position() == 0) { - handled = false; - } - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_DOWN: { - shift_selection_check_pre(k->get_shift()); - if (get_cursor_position() == text.length()) { - handled = false; - } - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_DELETE: { - if (!editable) { - break; - } - - if (k->get_shift() && !k->get_command() && !k->get_alt()) { - cut_text(); - break; - } - - if (selection.enabled) { - selection_delete(); - break; - } - - int text_len = text.length(); - - if (cursor_pos == text_len) { - break; // Nothing to do. - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_alt()) { -#else - if (k->get_alt()) { - handled = false; - break; - } else if (k->get_command()) { -#endif - int cc = cursor_pos; + // BACKSPACE + if (k->is_action("ui_text_backspace_all_to_left", true)) { + _backspace(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace_word", true)) { + _backspace(true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace", true)) { + _backspace(); + accept_event(); + return; + } - bool prev_char = false; + // DELETE + if (k->is_action("ui_text_delete_all_to_right", true)) { + _delete(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete_word", true)) { + _delete(true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete", true)) { + _delete(); + accept_event(); + return; + } - while (cc < text.length()) { - bool ischar = _is_text_char(text[cc]); + // Cursor Movement - if (prev_char && !ischar) { - break; - } - prev_char = ischar; - cc++; - } + 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); - delete_text(cursor_pos, cc); + if (k->is_action("ui_text_caret_word_left", true)) { + _move_caret_left(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_left", true)) { + _move_caret_left(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_word_right", true)) { + _move_caret_right(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_right", true)) { + _move_caret_right(shift_pressed, false); + accept_event(); + return; + } - } else { - set_cursor_position(cursor_pos + 1); - delete_char(); - } + // Up = Home, Down = End + if (k->is_action("ui_text_caret_up", true) || k->is_action("ui_text_caret_line_start", true) || k->is_action("ui_text_caret_page_up", true)) { + _move_caret_start(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_down", true) || k->is_action("ui_text_caret_line_end", true) || k->is_action("ui_text_caret_page_down", true)) { + _move_caret_end(shift_pressed); + accept_event(); + return; + } - } break; - case KEY_KP_7: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_HOME: { - shift_selection_check_pre(k->get_shift()); - set_cursor_position(0); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_KP_1: { - if (k->get_unicode() != 0) { - handled = false; - break; - } - [[fallthrough]]; - } - case KEY_END: { - shift_selection_check_pre(k->get_shift()); - set_cursor_position(text.length()); - shift_selection_check_post(k->get_shift()); - } break; - case KEY_MENU: { - if (context_menu_enabled) { - Point2 pos = Point2(get_cursor_pixel_pos(), (get_size().y + get_theme_font("font")->get_height()) / 2); - menu->set_position(get_global_transform().xform(pos)); - menu->set_size(Vector2(1, 1)); - // menu->set_scale(get_global_transform().get_scale()); - menu->popup(); - menu->grab_focus(); - } - } break; + // Misc + if (k->is_action("ui_swap_input_direction", true)) { + _swap_current_input_direction(); + accept_event(); + return; + } - default: { - handled = false; - } break; - } + _reset_caret_blink_timer(); - if (handled) { - accept_event(); - } else if (!k->get_command() || (k->get_command() && k->get_alt())) { - if (k->get_unicode() >= 32 && k->get_keycode() != KEY_DELETE) { - if (editable) { - selection_delete(); - char32_t ucodestr[2] = { (char32_t)k->get_unicode(), 0 }; - int prev_len = text.length(); - append_at_cursor(ucodestr); - if (text.length() != prev_len) { - _text_changed(); - } - accept_event(); - } + // 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()); - } else { - return; - } + if (allow_unicode_handling && editable && k->get_unicode() >= 32) { + // Handle Unicode (if no modifiers active) + selection_delete(); + char32_t ucodestr[2] = { (char32_t)k->get_unicode(), 0 }; + int prev_len = text.length(); + insert_text_at_caret(ucodestr); + if (text.length() != prev_len) { + _text_changed(); } - - update(); + accept_event(); } - - return; } } void LineEdit::set_align(Align p_align) { ERR_FAIL_INDEX((int)p_align, 4); - align = p_align; + if (align != p_align) { + align = p_align; + _shape(); + } update(); } @@ -625,31 +532,32 @@ Variant LineEdit::get_drag_data(const Point2 &p_point) { } bool LineEdit::can_drop_data(const Point2 &p_point, const Variant &p_data) const { + bool drop_override = Control::can_drop_data(p_point, p_data); // In case user wants to drop custom data. + if (drop_override) { + return drop_override; + } + return p_data.get_type() == Variant::STRING; } void LineEdit::drop_data(const Point2 &p_point, const Variant &p_data) { + Control::drop_data(p_point, p_data); + if (p_data.get_type() == Variant::STRING) { - set_cursor_at_pixel_pos(p_point.x); + set_caret_at_pixel_pos(p_point.x); int selected = selection.end - selection.begin; - Ref<Font> font = get_theme_font("font"); - if (font != nullptr) { - for (int i = selection.begin; i < selection.end; i++) { - cached_width -= font->get_char_size(pass ? secret_character[0] : text[i]).width; - } - } - text.erase(selection.begin, selected); + _shape(); - append_at_cursor(p_data); - selection.begin = cursor_pos - selected; - selection.end = cursor_pos; + insert_text_at_caret(p_data); + selection.begin = caret_column - selected; + selection.end = caret_column; } } Control::CursorShape LineEdit::get_cursor_shape(const Point2 &p_pos) const { - if (!text.empty() && is_editable() && _is_over_clear_button(p_pos)) { + if ((!text.is_empty() && is_editable() && _is_over_clear_button(p_pos)) || (!is_editable() && (!is_selecting_enabled() || text.is_empty()))) { return CURSOR_ARROW; } return Control::get_cursor_shape(p_pos); @@ -659,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; } @@ -669,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)) { - cursor_set_blink_enabled(EDITOR_DEF("text_editor/cursor/caret_blink", false)); - cursor_set_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)); @@ -679,13 +587,18 @@ void LineEdit::_notification(int p_what) { } break; #endif case NOTIFICATION_RESIZED: { + _fit_to_width(); scroll_offset = 0; - set_cursor_position(get_cursor_position()); - + set_caret_column(get_caret_column()); + } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_THEME_CHANGED: { + _shape(); + update(); } break; case NOTIFICATION_TRANSLATION_CHANGED: { - placeholder_translated = tr(placeholder); - update_placeholder_width(); + placeholder_translated = atr(placeholder); + _shape(); update(); } break; case NOTIFICATION_WM_WINDOW_FOCUS_IN: { @@ -699,11 +612,12 @@ 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; } int width, height; + bool rtl = is_layout_rtl(); Size2 size = get_size(); width = size.width; @@ -711,55 +625,59 @@ 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.empty() && ime_text.empty(); - int cached_text_width = using_placeholder ? cached_placeholder_width : cached_width; + 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(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM); switch (align) { case ALIGN_FILL: case ALIGN_LEFT: { - x_ofs = style->get_offset().x; + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { if (scroll_offset != 0) { x_ofs = style->get_offset().x; } else { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - (cached_text_width)) / 2); + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - (text_width)) / 2); } } break; case ALIGN_RIGHT: { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_text_width))); + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - style->get_margin(SIDE_RIGHT) - (text_width))); + } } break; } - int ofs_max = width - style->get_margin(MARGIN_RIGHT); - int char_ofs = scroll_offset; + int ofs_max = width - style->get_margin(SIDE_RIGHT); int y_area = height - style->get_minimum_size().height; - int y_ofs = style->get_offset().y + (y_area - font->get_height()) / 2; - - int font_ascent = font->get_ascent(); + 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_color_uneditable"); - Color font_color_selected = get_theme_color("font_color_selected"); - Color cursor_color = get_theme_color("cursor_color"); + Color selection_color = get_theme_color(SNAME("selection_color")); + Color font_color = is_editable() ? get_theme_color(SNAME("font_color")) : get_theme_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")); - const String &t = using_placeholder ? placeholder_translated : text; // Draw placeholder color. if (using_placeholder) { font_color.a *= placeholder_alpha; @@ -767,160 +685,176 @@ 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")); } } - r_icon->draw(ci, Point2(width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT), height / 2 - r_icon->get_height() / 2), color_icon); + r_icon->draw(ci, Point2(width - r_icon->get_width() - style->get_margin(SIDE_RIGHT), height / 2 - r_icon->get_height() / 2), color_icon); if (align == ALIGN_CENTER) { if (scroll_offset == 0) { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), int(size.width - cached_text_width - r_icon->get_width() - style->get_margin(MARGIN_RIGHT) * 2) / 2); + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(size.width - text_width - r_icon->get_width() - style->get_margin(SIDE_RIGHT) * 2) / 2); } } else { - x_ofs = MAX(style->get_margin(MARGIN_LEFT), x_ofs - r_icon->get_width() - style->get_margin(MARGIN_RIGHT)); + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } ofs_max -= r_icon->get_width(); } - int caret_height = font->get_height() > y_area ? y_area : font->get_height(); - FontDrawer drawer(font, Color(1, 1, 1)); - while (true) { - // End of string, break. - if (char_ofs >= t.length()) { - break; - } - - if (char_ofs == cursor_pos) { - if (ime_text.length() > 0) { - int ofs = 0; - while (true) { - if (ofs >= ime_text.length()) { - break; - } - - char32_t cchar = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs]; - char32_t next = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs + 1]; - int im_char_width = font->get_char_size(cchar, next).width; - - if ((x_ofs + im_char_width) > ofs_max) { - break; - } +#ifdef TOOLS_ENABLED + int caret_width = Math::round(EDSCALE); +#else + int caret_width = 1; +#endif - bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 3)), font_color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color); + // Draw selections rects. + Vector2 ofs = Point2(x_ofs + scroll_offset, y_ofs); + if (selection.enabled) { + Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, selection.begin, selection.end); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); + if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { + continue; + } + if (rect.position.x < x_ofs) { + rect.size.x -= (x_ofs - rect.position.x); + rect.position.x = x_ofs; + } else if (rect.position.x + rect.size.x > ofs_max) { + rect.size.x = ofs_max - rect.position.x; + } + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, selection_color); + } + } + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(text_rid); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); + + // Draw text. + ofs.y += TS->shaped_text_get_ascent(text_rid); + 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++) { + for (int j = 0; j < glyphs[i].repeat; j++) { + if (ceil(oofs.x) >= x_ofs && (oofs.x + glyphs[i].advance) <= ofs_max) { + if (glyphs[i].font_rid != RID()) { + TS->font_draw_glyph_outline(glyphs[i].font_rid, ci, glyphs[i].font_size, outline_size, oofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, font_outline_color); } - - drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color); - - x_ofs += im_char_width; - ofs++; } + oofs.x += glyphs[i].advance; + } + if (oofs.x >= ofs_max) { + break; } } - - char32_t cchar = (pass && !text.empty()) ? secret_character[0] : t[char_ofs]; - char32_t next = (pass && !text.empty()) ? secret_character[0] : t[char_ofs + 1]; - int char_width = font->get_char_size(cchar, next).width; - - // End of widget, break. - if ((x_ofs + char_width) > ofs_max) { - break; - } - - bool selected = selection.enabled && char_ofs >= selection.begin && char_ofs < selection.end; - - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(char_width, caret_height)), selection_color); - } - - int yofs = y_ofs + (caret_height - font->get_height()) / 2; - drawer.draw_char(ci, Point2(x_ofs, yofs + font_ascent), cchar, next, selected ? font_color_selected : font_color); - - if (char_ofs == cursor_pos && draw_caret && !using_placeholder) { - if (ime_text.length() == 0) { -#ifdef TOOLS_ENABLED - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(Math::round(EDSCALE), caret_height)), cursor_color); -#else - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(1, caret_height)), cursor_color); -#endif + } + for (int i = 0; i < gl_size; i++) { + bool selected = selection.enabled && glyphs[i].start >= selection.begin && glyphs[i].end <= selection.end; + for (int j = 0; j < glyphs[i].repeat; j++) { + if (ceil(ofs.x) >= x_ofs && (ofs.x + glyphs[i].advance) <= ofs_max) { + if (glyphs[i].font_rid != RID()) { + TS->font_draw_glyph(glyphs[i].font_rid, ci, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, selected ? font_selected_color : font_color); + } else if ((glyphs[i].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + TS->draw_hex_code_box(ci, glyphs[i].font_size, ofs + Vector2(glyphs[i].x_off, glyphs[i].y_off), glyphs[i].index, selected ? font_selected_color : font_color); + } } + ofs.x += glyphs[i].advance; + } + if (ofs.x >= ofs_max) { + break; } - - x_ofs += char_width; - char_ofs++; } - if (char_ofs == cursor_pos) { - if (ime_text.length() > 0) { - int ofs = 0; - while (true) { - if (ofs >= ime_text.length()) { - break; + // Draw carets. + ofs.x = x_ofs + scroll_offset; + if (draw_caret) { + if (ime_text.length() == 0) { + // Normal caret. + Rect2 l_caret, t_caret; + TextServer::Direction l_dir, t_dir; + TS->shaped_text_get_carets(text_rid, caret_column, l_caret, l_dir, t_caret, t_dir); + + if (l_caret == Rect2() && t_caret == Rect2()) { + // No carets, add one at the start. + 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; + l_caret = Rect2(Vector2(ofs_max, y), Size2(caret_width, h)); + } else { + l_dir = TextServer::DIRECTION_LTR; + l_caret = Rect2(Vector2(x_ofs, y), Size2(caret_width, h)); } - - char32_t cchar = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs]; - char32_t next = (pass && !text.empty()) ? secret_character[0] : ime_text[ofs + 1]; - int im_char_width = font->get_char_size(cchar, next).width; - - if ((x_ofs + im_char_width) > ofs_max) { - break; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, l_caret, caret_color); + } else { + 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 += ofs; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, trect, caret_color); } - bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 3)), font_color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color); - } + l_caret.position += ofs; + l_caret.size.x = caret_width; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, l_caret, caret_color); - drawer.draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color); + t_caret.position += ofs; + t_caret.size.x = caret_width; - x_ofs += im_char_width; - ofs++; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, t_caret, caret_color); } - } - } - - if ((char_ofs == cursor_pos || using_placeholder) && draw_caret) { // May be at the end, or placeholder. - if (ime_text.length() == 0) { - int caret_x_ofs = x_ofs; - if (using_placeholder) { - switch (align) { - case ALIGN_LEFT: - case ALIGN_FILL: { - caret_x_ofs = style->get_offset().x; - } break; - case ALIGN_CENTER: { - caret_x_ofs = ofs_max / 2; - } break; - case ALIGN_RIGHT: { - caret_x_ofs = ofs_max; - } break; + } else { + { + // IME intermediate text range. + Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, caret_column, caret_column + ime_text.length()); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); + if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { + continue; + } + if (rect.position.x < x_ofs) { + rect.size.x -= (x_ofs - rect.position.x); + rect.position.x = x_ofs; + } else if (rect.position.x + rect.size.x > ofs_max) { + rect.size.x = ofs_max - rect.position.x; + } + rect.size.y = caret_width; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, caret_color); + } + } + { + // IME caret. + Vector<Vector2> sel = TS->shaped_text_get_selection(text_rid, caret_column + ime_selection.x, caret_column + ime_selection.x + ime_selection.y); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + ofs.x, ofs.y, sel[i].y - sel[i].x, text_height); + if (rect.position.x + rect.size.x <= x_ofs || rect.position.x > ofs_max) { + continue; + } + if (rect.position.x < x_ofs) { + rect.size.x -= (x_ofs - rect.position.x); + rect.position.x = x_ofs; + } else if (rect.position.x + rect.size.x > ofs_max) { + rect.size.x = ofs_max - rect.position.x; + } + rect.size.y = caret_width * 3; + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, caret_color); } } -#ifdef TOOLS_ENABLED - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(caret_x_ofs, y_ofs), Size2(Math::round(EDSCALE), caret_height)), cursor_color); -#else - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(caret_x_ofs, y_ofs), Size2(1, caret_height)), cursor_color); -#endif } } if (has_focus()) { - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + 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() + Point2(using_placeholder ? 0 : x_ofs, y_ofs + caret_height), get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + Point2(using_placeholder ? 0 : x_ofs, y_ofs + TS->shaped_text_get_size(text_rid).y), get_viewport()->get_window_id()); } } } break; @@ -935,32 +869,27 @@ void LineEdit::_notification(int p_what) { } } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + 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()); - Point2 cursor_pos = Point2(get_cursor_position(), 1) * get_minimum_size().height; - DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + cursor_pos, get_viewport()->get_window_id()); - } - - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - if (selection.enabled) { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); - } else { - DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, cursor_pos); - } + Point2 caret_column = Point2(get_caret_column(), 1) * get_minimum_size().height; + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + caret_column, get_viewport()->get_window_id()); } + show_virtual_keyboard(); } break; case NOTIFICATION_FOCUS_EXIT: { if (caret_blink_enabled && !caret_force_displayed) { caret_blink_timer->stop(); } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_position(Point2(), get_viewport()->get_window_id()); DisplayServer::get_singleton()->window_set_ime_active(false, get_viewport()->get_window_id()); } ime_text = ""; ime_selection = Point2(); + _shape(); + set_caret_column(caret_column); // Update scroll_offset if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); @@ -971,6 +900,9 @@ void LineEdit::_notification(int p_what) { if (has_focus()) { ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); + _shape(); + set_caret_column(caret_column); // Update scroll_offset + update(); } } break; @@ -984,13 +916,17 @@ void LineEdit::copy_text() { } void LineEdit::cut_text() { - if (selection.enabled && !pass) { + if (editable && selection.enabled && !pass) { DisplayServer::get_singleton()->clipboard_set(text.substr(selection.begin, selection.end - selection.begin)); selection_delete(); } } void LineEdit::paste_text() { + if (!editable) { + return; + } + // Strip escape characters like \n and \t as they can't be displayed on LineEdit. String paste_buffer = DisplayServer::get_singleton()->clipboard_get().strip_escapes(); @@ -999,7 +935,7 @@ void LineEdit::paste_text() { if (selection.enabled) { selection_delete(); } - append_at_cursor(paste_buffer); + insert_text_at_caret(paste_buffer); if (!text_changed_dirty) { if (is_inside_tree() && text.length() != prev_len) { @@ -1010,7 +946,22 @@ 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; + } + if (undo_stack_pos == nullptr) { if (undo_stack.size() <= 1) { return; @@ -1022,18 +973,18 @@ void LineEdit::undo() { undo_stack_pos = undo_stack_pos->prev(); TextOperation op = undo_stack_pos->get(); text = op.text; - cached_width = op.cached_width; scroll_offset = op.scroll_offset; - set_cursor_position(op.cursor_pos); - - if (expand_to_text_length) { - minimum_size_changed(); - } + set_caret_column(op.caret_column); + _shape(); _emit_text_change(); } void LineEdit::redo() { + if (!editable) { + return; + } + if (undo_stack_pos == nullptr) { return; } @@ -1043,20 +994,16 @@ void LineEdit::redo() { undo_stack_pos = undo_stack_pos->next(); TextOperation op = undo_stack_pos->get(); text = op.text; - cached_width = op.cached_width; scroll_offset = op.scroll_offset; - set_cursor_position(op.cursor_pos); - - if (expand_to_text_length) { - minimum_size_changed(); - } + set_caret_column(op.caret_column); + _shape(); _emit_text_change(); } void LineEdit::shift_selection_check_pre(bool p_shift) { if (!selection.enabled && p_shift) { - selection.cursor_start = cursor_pos; + selection.start_column = caret_column; } if (!p_shift) { deselect(); @@ -1065,110 +1012,150 @@ void LineEdit::shift_selection_check_pre(bool p_shift) { void LineEdit::shift_selection_check_post(bool p_shift) { if (p_shift) { - selection_fill_at_cursor(); + selection_fill_at_caret(); } } -void LineEdit::set_cursor_at_pixel_pos(int p_x) { - Ref<Font> font = get_theme_font("font"); - int ofs = scroll_offset; - Ref<StyleBox> style = get_theme_stylebox("normal"); - int pixel_ofs = 0; - Size2 size = get_size(); - bool display_clear_icon = !text.empty() && is_editable() && clear_button_enabled; - int r_icon_width = Control::get_theme_icon("clear")->get_width(); +void LineEdit::set_caret_at_pixel_pos(int p_x) { + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); + bool rtl = is_layout_rtl(); + int x_ofs = 0; + float text_width = TS->shaped_text_get_size(text_rid).x; switch (align) { case ALIGN_FILL: case ALIGN_LEFT: { - pixel_ofs = int(style->get_offset().x); + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { if (scroll_offset != 0) { - pixel_ofs = int(style->get_offset().x); + x_ofs = style->get_offset().x; } else { - pixel_ofs = int(size.width - (cached_width)) / 2; - } - - if (display_clear_icon) { - pixel_ofs -= int(r_icon_width / 2 + style->get_margin(MARGIN_RIGHT)); + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - (text_width)) / 2); } } break; case ALIGN_RIGHT: { - pixel_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_width)); - - if (display_clear_icon) { - pixel_ofs -= int(r_icon_width + style->get_margin(MARGIN_RIGHT)); + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); } } break; } - while (ofs < text.length()) { - int char_w = 0; - if (font != nullptr) { - char_w = font->get_char_size(pass ? secret_character[0] : text[ofs]).width; - } - pixel_ofs += char_w; - - if (pixel_ofs > p_x) { // Found what we look for. - break; + 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(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); + } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } - - ofs++; } - set_cursor_position(ofs); + int ofs = TS->shaped_text_hit_test_position(text_rid, p_x - x_ofs - scroll_offset); + set_caret_column(ofs); } -int LineEdit::get_cursor_pixel_pos() { - Ref<Font> font = get_theme_font("font"); - int ofs = scroll_offset; - Ref<StyleBox> style = get_theme_stylebox("normal"); - int pixel_ofs = 0; - Size2 size = get_size(); - bool display_clear_icon = !text.empty() && is_editable() && clear_button_enabled; - int r_icon_width = Control::get_theme_icon("clear")->get_width(); +Vector2i LineEdit::get_caret_pixel_pos() { + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); + bool rtl = is_layout_rtl(); + int x_ofs = 0; + float text_width = TS->shaped_text_get_size(text_rid).x; switch (align) { case ALIGN_FILL: case ALIGN_LEFT: { - pixel_ofs = int(style->get_offset().x); + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } } break; case ALIGN_CENTER: { if (scroll_offset != 0) { - pixel_ofs = int(style->get_offset().x); + x_ofs = style->get_offset().x; } else { - pixel_ofs = int(size.width - (cached_width)) / 2; - } - - if (display_clear_icon) { - pixel_ofs -= int(r_icon_width / 2 + style->get_margin(MARGIN_RIGHT)); + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - (text_width)) / 2); } } break; case ALIGN_RIGHT: { - pixel_ofs = int(size.width - style->get_margin(MARGIN_RIGHT) - (cached_width)); - - if (display_clear_icon) { - pixel_ofs -= int(r_icon_width + style->get_margin(MARGIN_RIGHT)); + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); } } break; } - while (ofs < cursor_pos) { - if (font != nullptr) { - pixel_ofs += font->get_char_size(pass ? secret_character[0] : text[ofs]).width; + 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(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); + } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } - ofs++; } - return pixel_ofs; + Vector2i ret; + Rect2 l_caret, t_caret; + TextServer::Direction l_dir, t_dir; + // Get position of the start of caret. + if (ime_text.length() != 0 && ime_selection.x != 0) { + TS->shaped_text_get_carets(text_rid, caret_column + ime_selection.x, l_caret, l_dir, t_caret, t_dir); + } else { + 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())) { + ret.x = x_ofs + l_caret.position.x + scroll_offset; + } else { + ret.x = x_ofs + t_caret.position.x + scroll_offset; + } + + // Get position of the end of caret. + if (ime_text.length() != 0) { + if (ime_selection.y != 0) { + TS->shaped_text_get_carets(text_rid, caret_column + ime_selection.x + ime_selection.y, l_caret, l_dir, t_caret, t_dir); + } else { + TS->shaped_text_get_carets(text_rid, caret_column + ime_text.size(), 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())) { + ret.y = x_ofs + l_caret.position.x + scroll_offset; + } else { + ret.y = x_ofs + t_caret.position.x + scroll_offset; + } + } else { + ret.y = ret.x; + } + + return ret; +} + +void LineEdit::set_caret_mid_grapheme_enabled(const bool p_enabled) { + caret_mid_grapheme_enabled = p_enabled; } -bool LineEdit::cursor_get_blink_enabled() const { +bool LineEdit::is_caret_mid_grapheme_enabled() const { + return caret_mid_grapheme_enabled; +} + +bool LineEdit::is_caret_blink_enabled() const { return caret_blink_enabled; } -void LineEdit::cursor_set_blink_enabled(const bool p_enabled) { +void LineEdit::set_caret_blink_enabled(const bool p_enabled) { caret_blink_enabled = p_enabled; if (has_focus() || caret_force_displayed) { @@ -1182,23 +1169,25 @@ void LineEdit::cursor_set_blink_enabled(const bool p_enabled) { } draw_caret = true; + + notify_property_list_changed(); } -bool LineEdit::cursor_get_force_displayed() const { +bool LineEdit::is_caret_force_displayed() const { return caret_force_displayed; } -void LineEdit::cursor_set_force_displayed(const bool p_enabled) { +void LineEdit::set_caret_force_displayed(const bool p_enabled) { caret_force_displayed = p_enabled; - cursor_set_blink_enabled(caret_blink_enabled); + set_caret_blink_enabled(caret_blink_enabled); update(); } -float LineEdit::cursor_get_blink_speed() const { +float LineEdit::get_caret_blink_speed() const { return caret_blink_timer->get_wait_time(); } -void LineEdit::cursor_set_blink_speed(const float p_speed) { +void LineEdit::set_caret_blink_speed(const float p_speed) { ERR_FAIL_COND(p_speed <= 0); caret_blink_timer->set_wait_time(p_speed); } @@ -1222,22 +1211,14 @@ void LineEdit::_toggle_draw_caret() { } void LineEdit::delete_char() { - if ((text.length() <= 0) || (cursor_pos == 0)) { + if ((text.length() <= 0) || (caret_column == 0)) { return; } - Ref<Font> font = get_theme_font("font"); - if (font != nullptr) { - cached_width -= font->get_char_size(pass ? secret_character[0] : text[cursor_pos - 1]).width; - } - - text.erase(cursor_pos - 1, 1); + text.erase(caret_column - 1, 1); + _shape(); - set_cursor_position(get_cursor_position() - 1); - - if (align == ALIGN_CENTER || align == ALIGN_RIGHT) { - scroll_offset = CLAMP(scroll_offset - 1, 0, MAX(text.length() - 1, 0)); - } + set_caret_column(get_caret_column() - 1); _text_changed(); } @@ -1245,29 +1226,14 @@ void LineEdit::delete_char() { void LineEdit::delete_text(int p_from_column, int p_to_column) { ERR_FAIL_COND_MSG(p_from_column < 0 || p_from_column > p_to_column || p_to_column > text.length(), vformat("Positional parameters (from: %d, to: %d) are inverted or outside the text length (%d).", p_from_column, p_to_column, text.length())); - if (text.size() > 0) { - Ref<Font> font = get_theme_font("font"); - if (font != nullptr) { - for (int i = p_from_column; i < p_to_column; i++) { - cached_width -= font->get_char_size(pass ? secret_character[0] : text[i]).width; - } - } - } else { - cached_width = 0; - } text.erase(p_from_column, p_to_column - p_from_column); - cursor_pos -= CLAMP(cursor_pos - p_from_column, 0, p_to_column - p_from_column); + _shape(); - if (cursor_pos >= text.length()) { - cursor_pos = text.length(); - } - if (scroll_offset > cursor_pos) { - scroll_offset = cursor_pos; - } + caret_column -= CLAMP(caret_column - p_from_column, 0, p_to_column - p_from_column); - if (align == ALIGN_CENTER || align == ALIGN_RIGHT) { - scroll_offset = CLAMP(scroll_offset - (p_to_column - p_from_column), 0, MAX(text.length() - 1, 0)); + if (caret_column >= text.length()) { + caret_column = text.length(); } if (!text_changed_dirty) { @@ -1280,20 +1246,127 @@ void LineEdit::delete_text(int p_from_column, int p_to_column) { void LineEdit::set_text(String p_text) { clear_internal(); - append_at_cursor(p_text); + insert_text_at_caret(p_text); + _create_undo_state(); - if (expand_to_text_length) { - minimum_size_changed(); + update(); + caret_column = 0; + scroll_offset = 0; +} + +void LineEdit::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; + } + _shape(); + + 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(); } +} +Control::TextDirection LineEdit::get_text_direction() const { + return text_direction; +} + +void LineEdit::clear_opentype_features() { + opentype_features.clear(); + _shape(); update(); - cursor_pos = 0; - scroll_offset = 0; +} + +void LineEdit::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; + _shape(); + update(); + } +} + +int LineEdit::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]; +} + +void LineEdit::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + _shape(); + update(); + } +} + +String LineEdit::get_language() const { + return language; +} + +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; + 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(); + } +} + +bool LineEdit::get_draw_control_chars() const { + return draw_control_chars; +} + +void LineEdit::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + _shape(); + update(); + } +} + +Control::StructuredTextParser LineEdit::get_structured_text_bidi_override() const { + return st_parser; +} + +void LineEdit::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + _shape(); + update(); +} + +Array LineEdit::get_structured_text_bidi_override_options() const { + return st_args; } void LineEdit::clear() { clear_internal(); _text_changed(); + + // This should reset virtual keyboard state if needed. + if (has_focus()) { + show_virtual_keyboard(); + } +} + +void LineEdit::show_virtual_keyboard() { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { + if (selection.enabled) { + DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, selection.begin, selection.end); + } else { + DisplayServer::get_singleton()->virtual_keyboard_show(text, get_global_rect(), false, max_length, caret_column); + } + } } String LineEdit::get_text() const { @@ -1302,8 +1375,8 @@ String LineEdit::get_text() const { void LineEdit::set_placeholder(String p_text) { placeholder = p_text; - placeholder_translated = tr(placeholder); - update_placeholder_width(); + placeholder_translated = atr(placeholder); + _shape(); update(); } @@ -1320,73 +1393,84 @@ float LineEdit::get_placeholder_alpha() const { return placeholder_alpha; } -void LineEdit::set_cursor_position(int p_pos) { - if (p_pos > (int)text.length()) { - p_pos = text.length(); +void LineEdit::set_caret_column(int p_column) { + if (p_column > (int)text.length()) { + p_column = text.length(); } - if (p_pos < 0) { - p_pos = 0; + if (p_column < 0) { + p_column = 0; } - cursor_pos = p_pos; + caret_column = p_column; + + // Fit to window. if (!is_inside_tree()) { - scroll_offset = cursor_pos; + scroll_offset = 0; return; } - Ref<StyleBox> style = get_theme_stylebox("normal"); - Ref<Font> font = get_theme_font("font"); - - if (cursor_pos <= scroll_offset) { - // Adjust window if cursor goes too much to the left. - set_scroll_offset(MAX(0, cursor_pos - 1)); - } else { - // Adjust window if cursor goes too much to the right. - int window_width = get_size().width - style->get_minimum_size().width; - bool display_clear_icon = !text.empty() && 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; - window_width -= r_icon->get_width(); - } - - if (window_width < 0) { - return; - } - int wp = scroll_offset; + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); + bool rtl = is_layout_rtl(); - if (font.is_valid()) { - int accum_width = 0; - - for (int i = cursor_pos; i >= scroll_offset; i--) { - if (i >= text.length()) { - // Do not do this, because if the cursor is at the end, its just fine that it takes no space. - // accum_width = font->get_char_size(' ').width; - } else { - if (pass) { - accum_width += font->get_char_size(secret_character[0], i + 1 < text.length() ? secret_character[0] : 0).width; - } else { - accum_width += font->get_char_size(text[i], i + 1 < text.length() ? text[i + 1] : 0).width; // Anything should do. - } - } - if (accum_width > window_width) { - break; - } + int x_ofs = 0; + float text_width = TS->shaped_text_get_size(text_rid).x; + switch (align) { + case ALIGN_FILL: + case ALIGN_LEFT: { + if (rtl) { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } else { + x_ofs = style->get_offset().x; + } + } break; + case ALIGN_CENTER: { + if (scroll_offset != 0) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - (text_width)) / 2); + } + } break; + case ALIGN_RIGHT: { + if (rtl) { + x_ofs = style->get_offset().x; + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), int(get_size().width - style->get_margin(SIDE_RIGHT) - (text_width))); + } + } break; + } - wp = i; + int ofs_max = get_size().width - style->get_margin(SIDE_RIGHT); + 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(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); } + } else { + x_ofs = MAX(style->get_margin(SIDE_LEFT), x_ofs - r_icon->get_width() - style->get_margin(SIDE_RIGHT)); } + ofs_max -= r_icon->get_width(); + } - if (wp != scroll_offset) { - set_scroll_offset(wp); - } + // Note: Use two coordinates to fit IME input range. + Vector2i primary_catret_offset = get_caret_pixel_pos(); + + if (MIN(primary_catret_offset.x, primary_catret_offset.y) <= x_ofs) { + scroll_offset += (x_ofs - MIN(primary_catret_offset.x, primary_catret_offset.y)); + } else if (MAX(primary_catret_offset.x, primary_catret_offset.y) >= ofs_max) { + scroll_offset += (ofs_max - MAX(primary_catret_offset.x, primary_catret_offset.y)); } + scroll_offset = MIN(0, scroll_offset); + update(); } -int LineEdit::get_cursor_position() const { - return cursor_pos; +int LineEdit::get_caret_column() const { + return caret_column; } void LineEdit::set_scroll_offset(int p_pos) { @@ -1400,54 +1484,62 @@ int LineEdit::get_scroll_offset() const { return scroll_offset; } -void LineEdit::append_at_cursor(String p_text) { - if ((max_length <= 0) || (text.length() + p_text.length() <= max_length)) { - String pre = text.substr(0, cursor_pos); - String post = text.substr(cursor_pos, text.length() - cursor_pos); - text = pre + p_text + post; - update_cached_width(); - set_cursor_position(cursor_pos + p_text.length()); - } else { - emit_signal("text_change_rejected"); +void LineEdit::insert_text_at_caret(String p_text) { + 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); + } + } + 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() { deselect(); _clear_undo_stack(); - cached_width = 0; - cursor_pos = 0; + caret_column = 0; scroll_offset = 0; undo_text = ""; text = ""; + _shape(); update(); } Size2 LineEdit::get_minimum_size() const { - Ref<StyleBox> style = get_theme_stylebox("normal"); - Ref<Font> font = get_theme_font("font"); + 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 space_size = font->get_char_size(' ').x; - min_size.width = get_theme_constant("minimum_spaces") * space_size; + int em_space_size = font->get_char_size('M', 0, font_size).x; + 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 cursor needs a bit more when at the end. - min_size.width = MAX(min_size.width, font->get_string_size(text).x + space_size); + // 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 = font->get_height(); + 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. - if (!text.empty() && is_editable() && clear_button_enabled) { - min_size.width = MAX(min_size.width, Control::get_theme_icon("clear")->get_width()); - min_size.height = MAX(min_size.height, Control::get_theme_icon("clear")->get_height()); - } - if (right_icon.is_valid()) { - min_size.width = MAX(min_size.width, right_icon->get_width()); - min_size.height = MAX(min_size.height, right_icon->get_height()); + 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(SNAME("clear")) : right_icon; + min_size.width += r_icon->get_width(); + min_size.height = MAX(min_size.height, r_icon->get_height()); } return style->get_minimum_size() + min_size; @@ -1456,10 +1548,10 @@ Size2 LineEdit::get_minimum_size() const { void LineEdit::deselect() { selection.begin = 0; selection.end = 0; - selection.cursor_start = 0; + selection.start_column = 0; selection.enabled = false; selection.creating = false; - selection.doubleclick = false; + selection.double_click = false; update(); } @@ -1481,13 +1573,13 @@ int LineEdit::get_max_length() const { return max_length; } -void LineEdit::selection_fill_at_cursor() { +void LineEdit::selection_fill_at_caret() { if (!selecting_enabled) { return; } - selection.begin = cursor_pos; - selection.end = selection.cursor_start; + selection.begin = caret_column; + selection.end = selection.start_column; if (selection.end < selection.begin) { int aux = selection.end; @@ -1519,7 +1611,6 @@ void LineEdit::set_editable(bool p_editable) { } editable = p_editable; - _generate_context_menu(); minimum_size_changed(); update(); @@ -1530,8 +1621,10 @@ bool LineEdit::is_editable() const { } void LineEdit::set_secret(bool p_secret) { - pass = p_secret; - update_cached_width(); + if (pass != p_secret) { + pass = p_secret; + _shape(); + } update(); } @@ -1544,8 +1637,10 @@ void LineEdit::set_secret_character(const String &p_string) { // It also wouldn't make sense to use multiple characters as the secret character. ERR_FAIL_COND_MSG(p_string.length() != 1, "Secret character must be exactly one character long (" + itos(p_string.length()) + " characters given)."); - secret_character = p_string; - update_cached_width(); + if (secret_character != p_string) { + secret_character = p_string; + _shape(); + } update(); } @@ -1582,7 +1677,7 @@ void LineEdit::select(int p_from, int p_to) { selection.begin = p_from; selection.end = p_to; selection.creating = false; - selection.doubleclick = false; + selection.double_click = false; update(); } @@ -1622,6 +1717,101 @@ void LineEdit::menu_option(int p_option) { if (editable) { 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)); + } } } } @@ -1634,24 +1824,29 @@ 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 - cursor_set_blink_enabled(EDITOR_DEF("text_editor/cursor/caret_blink", false)); - cursor_set_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 } -void LineEdit::set_expand_to_text_length(bool p_enabled) { +void LineEdit::set_expand_to_text_length_enabled(bool p_enabled) { expand_to_text_length = p_enabled; minimum_size_changed(); - set_scroll_offset(0); + set_caret_column(caret_column); } -bool LineEdit::get_expand_to_text_length() const { +bool LineEdit::is_expand_to_text_length_enabled() const { return expand_to_text_length; } @@ -1660,6 +1855,7 @@ void LineEdit::set_clear_button_enabled(bool p_enabled) { return; } clear_button_enabled = p_enabled; + _fit_to_width(); minimum_size_changed(); update(); } @@ -1670,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 { @@ -1692,8 +1886,6 @@ void LineEdit::set_selecting_enabled(bool p_enabled) { if (!selecting_enabled) { deselect(); } - - _generate_context_menu(); } bool LineEdit::is_selecting_enabled() const { @@ -1705,6 +1897,7 @@ void LineEdit::set_right_icon(const Ref<Texture2D> &p_icon) { return; } right_icon = p_icon; + _fit_to_width(); minimum_size_changed(); update(); } @@ -1714,38 +1907,65 @@ Ref<Texture2D> LineEdit::get_right_icon() { } void LineEdit::_text_changed() { - if (expand_to_text_length) { - minimum_size_changed(); - } - _emit_text_change(); _clear_redo(); } void LineEdit::_emit_text_change() { - emit_signal("text_changed", text); - _change_notify("text"); + emit_signal(SNAME("text_changed"), text); text_changed_dirty = false; } -void LineEdit::update_cached_width() { - Ref<Font> font = get_theme_font("font"); - cached_width = 0; - if (font != nullptr) { - String text = get_text(); - for (int i = 0; i < text.length(); i++) { - cached_width += font->get_char_size(pass ? secret_character[0] : text[i]).width; +void LineEdit::_shape() { + Size2 old_size = TS->shaped_text_get_size(text_rid); + TS->shaped_text_clear(text_rid); + + String t; + if (text.length() == 0) { + t = placeholder_translated; + } else if (pass) { + t = secret_character.repeat(text.length() + ime_text.length()); + } else { + if (ime_text.length() > 0) { + t = text.substr(0, caret_column) + ime_text + text.substr(caret_column, text.length()); + } else { + t = text; } } + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + TS->shaped_text_set_direction(text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction); + } + TS->shaped_text_set_preserve_control(text_rid, draw_control_chars); + + 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)); + + full_width = TS->shaped_text_get_size(text_rid).x; + _fit_to_width(); + + Size2 size = TS->shaped_text_get_size(text_rid); + + if ((expand_to_text_length && old_size.x != size.x) || (old_size.y != size.y)) { + minimum_size_changed(); + } } -void LineEdit::update_placeholder_width() { - Ref<Font> font = get_theme_font("font"); - cached_placeholder_width = 0; - if (font != nullptr) { - for (int i = 0; i < placeholder_translated.length(); i++) { - cached_placeholder_width += font->get_char_size(placeholder_translated[i]).width; +void LineEdit::_fit_to_width() { + if (align == ALIGN_FILL) { + 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(SNAME("clear")) : right_icon; + t_width -= r_icon->get_width(); } + TS->shaped_text_fit_to_width(text_rid, MAX(t_width, full_width)); } } @@ -1773,31 +1993,89 @@ void LineEdit::_clear_undo_stack() { void LineEdit::_create_undo_state() { TextOperation op; op.text = text; - op.cached_width = cached_width; - op.cursor_pos = cursor_pos; + op.caret_column = caret_column; op.scroll_offset = scroll_offset; undo_stack.push_back(op); } -void LineEdit::_generate_context_menu() { - // Reorganize context menu. - menu->clear(); - if (editable) { - menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_X : 0); +int LineEdit::_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; } - menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_C : 0); - if (editable) { - menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_V : 0); + + // Use first event in the list for the accelerator. + const List<Ref<InputEvent>>::Element *first_event = events->front(); + if (!first_event) { + return 0; } - menu->add_separator(); - if (is_selecting_enabled()) { - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_A : 0); + + const Ref<InputEventKey> event = first_event->get(); + if (event.is_null()) { + return 0; } - if (editable) { - menu->add_item(RTR("Clear"), MENU_CLEAR); - menu->add_separator(); - menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_Z : 0); - menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z : 0); + + // 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(); + } +} + +bool LineEdit::_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); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + notify_property_list_changed(); + return true; + } + + return false; +} + +bool LineEdit::_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 LineEdit::_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 LineEdit::_validate_property(PropertyInfo &property) const { + if (!caret_blink_enabled && property.name == "caret_blink_speed") { + property.usage = PROPERTY_USAGE_NOEDITOR; } } @@ -1807,32 +2085,46 @@ 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); ClassDB::bind_method(D_METHOD("deselect"), &LineEdit::deselect); ClassDB::bind_method(D_METHOD("set_text", "text"), &LineEdit::set_text); ClassDB::bind_method(D_METHOD("get_text"), &LineEdit::get_text); + ClassDB::bind_method(D_METHOD("get_draw_control_chars"), &LineEdit::get_draw_control_chars); + ClassDB::bind_method(D_METHOD("set_draw_control_chars", "enable"), &LineEdit::set_draw_control_chars); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &LineEdit::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &LineEdit::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &LineEdit::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &LineEdit::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &LineEdit::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &LineEdit::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &LineEdit::get_language); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &LineEdit::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &LineEdit::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &LineEdit::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &LineEdit::get_structured_text_bidi_override_options); ClassDB::bind_method(D_METHOD("set_placeholder", "text"), &LineEdit::set_placeholder); ClassDB::bind_method(D_METHOD("get_placeholder"), &LineEdit::get_placeholder); ClassDB::bind_method(D_METHOD("set_placeholder_alpha", "alpha"), &LineEdit::set_placeholder_alpha); ClassDB::bind_method(D_METHOD("get_placeholder_alpha"), &LineEdit::get_placeholder_alpha); - ClassDB::bind_method(D_METHOD("set_cursor_position", "position"), &LineEdit::set_cursor_position); - ClassDB::bind_method(D_METHOD("get_cursor_position"), &LineEdit::get_cursor_position); + ClassDB::bind_method(D_METHOD("set_caret_column", "position"), &LineEdit::set_caret_column); + ClassDB::bind_method(D_METHOD("get_caret_column"), &LineEdit::get_caret_column); ClassDB::bind_method(D_METHOD("get_scroll_offset"), &LineEdit::get_scroll_offset); - ClassDB::bind_method(D_METHOD("set_expand_to_text_length", "enabled"), &LineEdit::set_expand_to_text_length); - ClassDB::bind_method(D_METHOD("get_expand_to_text_length"), &LineEdit::get_expand_to_text_length); - ClassDB::bind_method(D_METHOD("cursor_set_blink_enabled", "enabled"), &LineEdit::cursor_set_blink_enabled); - ClassDB::bind_method(D_METHOD("cursor_get_blink_enabled"), &LineEdit::cursor_get_blink_enabled); - ClassDB::bind_method(D_METHOD("cursor_set_force_displayed", "enabled"), &LineEdit::cursor_set_force_displayed); - ClassDB::bind_method(D_METHOD("cursor_get_force_displayed"), &LineEdit::cursor_get_force_displayed); - ClassDB::bind_method(D_METHOD("cursor_set_blink_speed", "blink_speed"), &LineEdit::cursor_set_blink_speed); - ClassDB::bind_method(D_METHOD("cursor_get_blink_speed"), &LineEdit::cursor_get_blink_speed); + ClassDB::bind_method(D_METHOD("set_expand_to_text_length_enabled", "enabled"), &LineEdit::set_expand_to_text_length_enabled); + ClassDB::bind_method(D_METHOD("is_expand_to_text_length_enabled"), &LineEdit::is_expand_to_text_length_enabled); + ClassDB::bind_method(D_METHOD("set_caret_blink_enabled", "enabled"), &LineEdit::set_caret_blink_enabled); + ClassDB::bind_method(D_METHOD("is_caret_blink_enabled"), &LineEdit::is_caret_blink_enabled); + ClassDB::bind_method(D_METHOD("set_caret_mid_grapheme_enabled", "enabled"), &LineEdit::set_caret_mid_grapheme_enabled); + ClassDB::bind_method(D_METHOD("is_caret_mid_grapheme_enabled"), &LineEdit::is_caret_mid_grapheme_enabled); + ClassDB::bind_method(D_METHOD("set_caret_force_displayed", "enabled"), &LineEdit::set_caret_force_displayed); + ClassDB::bind_method(D_METHOD("is_caret_force_displayed"), &LineEdit::is_caret_force_displayed); + ClassDB::bind_method(D_METHOD("set_caret_blink_speed", "blink_speed"), &LineEdit::set_caret_blink_speed); + ClassDB::bind_method(D_METHOD("get_caret_blink_speed"), &LineEdit::get_caret_blink_speed); ClassDB::bind_method(D_METHOD("set_max_length", "chars"), &LineEdit::set_max_length); ClassDB::bind_method(D_METHOD("get_max_length"), &LineEdit::get_max_length); - ClassDB::bind_method(D_METHOD("append_at_cursor", "text"), &LineEdit::append_at_cursor); - ClassDB::bind_method(D_METHOD("delete_char_at_cursor"), &LineEdit::delete_char); + ClassDB::bind_method(D_METHOD("insert_text_at_caret", "text"), &LineEdit::insert_text_at_caret); + ClassDB::bind_method(D_METHOD("delete_char_at_caret"), &LineEdit::delete_char); ClassDB::bind_method(D_METHOD("delete_text", "from_column", "to_column"), &LineEdit::delete_text); ClassDB::bind_method(D_METHOD("set_editable", "enabled"), &LineEdit::set_editable); ClassDB::bind_method(D_METHOD("is_editable"), &LineEdit::is_editable); @@ -1842,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); @@ -1856,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); @@ -1871,73 +2164,155 @@ void LineEdit::_bind_methods() { 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); 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"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand_to_text_length"), "set_expand_to_text_length", "get_expand_to_text_length"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand_to_text_length"), "set_expand_to_text_length_enabled", "is_expand_to_text_length_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "context_menu_enabled"), "set_context_menu_enabled", "is_context_menu_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "virtual_keyboard_enabled"), "set_virtual_keyboard_enabled", "is_virtual_keyboard_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clear_button_enabled"), "set_clear_button_enabled", "is_clear_button_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::OBJECT, "right_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), "set_right_icon", "get_right_icon"); + 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_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"); ADD_GROUP("Placeholder", "placeholder_"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text"), "set_placeholder", "get_placeholder"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "placeholder_alpha", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_placeholder_alpha", "get_placeholder_alpha"); ADD_GROUP("Caret", "caret_"); - 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::INT, "caret_position"), "set_cursor_position", "get_cursor_position"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "caret_force_displayed"), "cursor_set_force_displayed", "cursor_get_force_displayed"); + 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", 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); + + 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() { - undo_stack_pos = nullptr; + text_rid = TS->create_shaped_text(); _create_undo_state(); - align = ALIGN_LEFT; - cached_width = 0; - cached_placeholder_width = 0; - cursor_pos = 0; - scroll_offset = 0; - window_has_focus = true; - max_length = 0; - pass = false; - secret_character = "*"; - text_changed_dirty = false; - placeholder_alpha = 0.6; - clear_button_enabled = false; - clear_button_status.press_attempt = false; - clear_button_status.pressing_inside = false; - shortcut_keys_enabled = true; - selecting_enabled = true; deselect(); set_focus_mode(FOCUS_ALL); set_default_cursor_shape(CURSOR_IBEAM); set_mouse_filter(MOUSE_FILTER_STOP); - draw_caret = true; - caret_blink_enabled = false; - caret_force_displayed = false; caret_blink_timer = memnew(Timer); add_child(caret_blink_timer); caret_blink_timer->set_wait_time(0.65); caret_blink_timer->connect("timeout", callable_mp(this, &LineEdit::_toggle_draw_caret)); - cursor_set_blink_enabled(false); + set_caret_blink_enabled(false); - context_menu_enabled = true; - menu = memnew(PopupMenu); - add_child(menu); - editable = false; // Initialise to opposite first, so we get past the early-out in set_editable. - set_editable(true); - menu->connect("id_pressed", callable_mp(this, &LineEdit::menu_option)); - expand_to_text_length = false; + set_editable(true); // Initialise to opposite first, so we get past the early-out in set_editable. } LineEdit::~LineEdit() { + TS->free(text_rid); } diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index a5e5b6988f..e364a79c83 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -39,7 +39,6 @@ class LineEdit : public Control { public: enum Align { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -54,101 +53,133 @@ public: 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 - }; private: - Align align; + Align align = ALIGN_LEFT; - bool editable; - bool pass; - bool text_changed_dirty; + bool editable = false; + bool pass = false; + bool text_changed_dirty = false; String undo_text; String text; String placeholder; String placeholder_translated; - String secret_character; - float placeholder_alpha; + String secret_character = "*"; + float placeholder_alpha = 0.6; String ime_text; Point2 ime_selection; - bool selecting_enabled; + RID text_rid; + float full_width = 0.0; + + bool selecting_enabled = true; + + bool context_menu_enabled = true; + PopupMenu *menu = nullptr; + PopupMenu *menu_dir = nullptr; + PopupMenu *menu_ctl = nullptr; - bool context_menu_enabled; - PopupMenu *menu; + bool caret_mid_grapheme_enabled = false; - int cursor_pos; - int scroll_offset; - int max_length; // 0 for no maximum. + int caret_column = 0; + int scroll_offset = 0; + int max_length = 0; // 0 for no maximum. - int cached_width; - int cached_placeholder_width; + 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; - bool clear_button_enabled; + bool expand_to_text_length = false; + bool window_has_focus = true; - bool shortcut_keys_enabled; + bool clear_button_enabled = false; + + bool shortcut_keys_enabled = true; bool virtual_keyboard_enabled = true; Ref<Texture2D> right_icon; struct Selection { - int begin; - int end; - int cursor_start; - bool enabled; - bool creating; - bool doubleclick; - bool drag_attempt; + int begin = 0; + int end = 0; + int start_column = 0; + bool enabled = false; + bool creating = false; + bool double_click = false; + bool drag_attempt = false; + uint64_t last_dblclk = 0; } selection; struct TextOperation { - int cursor_pos; - int scroll_offset; - int cached_width; + int caret_column = 0; + int scroll_offset = 0; + int cached_width = 0; String text; }; List<TextOperation> undo_stack; - List<TextOperation>::Element *undo_stack_pos; + List<TextOperation>::Element *undo_stack_pos = nullptr; struct ClearButtonStatus { - bool press_attempt; - bool pressing_inside; + bool press_attempt = false; + bool pressing_inside = false; } clear_button_status; + bool caret_blink_enabled = false; + bool caret_force_displayed = false; + bool draw_caret = true; + Timer *caret_blink_timer = nullptr; + bool _is_over_clear_button(const Point2 &p_pos) const; void _clear_undo_stack(); void _clear_redo(); void _create_undo_state(); - void _generate_context_menu(); - - Timer *caret_blink_timer; + int _get_menu_action_accelerator(const String &p_action); + void _shape(); + void _fit_to_width(); void _text_changed(); void _emit_text_change(); - bool expand_to_text_length; - - void update_cached_width(); - void update_placeholder_width(); - - bool caret_blink_enabled; - bool caret_force_displayed; - bool draw_caret; - bool window_has_focus; void shift_selection_check_pre(bool); void shift_selection_check_post(bool); - void selection_fill_at_cursor(); + void selection_fill_at_caret(); void set_scroll_offset(int p_pos); int get_scroll_offset() const; - void set_cursor_at_pixel_pos(int p_x); - int get_cursor_pixel_pos(); + void set_caret_at_pixel_pos(int p_x); + Vector2i get_caret_pixel_pos(); void _reset_caret_blink_timer(); void _toggle_draw_caret(); @@ -158,11 +189,25 @@ private: void _editor_settings_changed(); - void _gui_input(Ref<InputEvent> p_event); - void _notification(int p_what); + void _swap_current_input_direction(); + 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_start(bool p_select); + void _move_caret_end(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 _ensure_menu(); protected: + void _notification(int p_what); static void _bind_methods(); + 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; + void _get_property_list(List<PropertyInfo> *p_list) const; + void _validate_property(PropertyInfo &property) const override; public: void set_align(Align p_align); @@ -178,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(); @@ -186,31 +232,61 @@ public: void delete_char(); void delete_text(int p_from_column, int p_to_column); + void set_text(String p_text); String get_text() const; + + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + 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_placeholder(String p_text); String get_placeholder() const; + void set_placeholder_alpha(float p_alpha); float get_placeholder_alpha() const; - void set_cursor_position(int p_pos); - int get_cursor_position() const; + + void set_caret_column(int p_column); + int get_caret_column() const; + void set_max_length(int p_max_length); int get_max_length() const; - void append_at_cursor(String p_text); + + void insert_text_at_caret(String p_text); void clear(); - bool cursor_get_blink_enabled() const; - void cursor_set_blink_enabled(const bool p_enabled); + void set_caret_mid_grapheme_enabled(const bool p_enabled); + bool is_caret_mid_grapheme_enabled() const; - float cursor_get_blink_speed() const; - void cursor_set_blink_speed(const float p_speed); + bool is_caret_blink_enabled() const; + void set_caret_blink_enabled(const bool p_enabled); - bool cursor_get_force_displayed() const; - void cursor_set_force_displayed(const bool p_enabled); + float get_caret_blink_speed() const; + void set_caret_blink_speed(const float p_speed); + + void set_caret_force_displayed(const bool p_enabled); + bool is_caret_force_displayed() const; void copy_text(); void cut_text(); void paste_text(); + bool has_undo() const; + bool has_redo() const; void undo(); void redo(); @@ -225,8 +301,8 @@ public: virtual Size2 get_minimum_size() const override; - void set_expand_to_text_length(bool p_enabled); - bool get_expand_to_text_length() const; + void set_expand_to_text_length_enabled(bool p_enabled); + bool is_expand_to_text_length_enabled() const; void set_clear_button_enabled(bool p_enabled); bool is_clear_button_enabled() const; @@ -244,6 +320,9 @@ public: Ref<Texture2D> get_right_icon(); virtual bool is_text_field() const override; + + void show_virtual_keyboard(); + LineEdit(); ~LineEdit(); }; diff --git a/scene/gui/link_button.cpp b/scene/gui/link_button.cpp index 27a60945c8..419d49bccf 100644 --- a/scene/gui/link_button.cpp +++ b/scene/gui/link_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -29,17 +29,103 @@ /*************************************************************************/ #include "link_button.h" +#include "core/string/translation.h" + +void LinkButton::_shape() { + 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) { + text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } 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()); +} void LinkButton::set_text(const String &p_text) { text = p_text; - update(); + _shape(); minimum_size_changed(); + update(); } String LinkButton::get_text() const { return text; } +void LinkButton::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + _shape(); + update(); + } +} + +Control::StructuredTextParser LinkButton::get_structured_text_bidi_override() const { + return st_parser; +} + +void LinkButton::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + _shape(); + update(); +} + +Array LinkButton::get_structured_text_bidi_override_options() const { + return st_args; +} + +void LinkButton::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; + _shape(); + update(); + } +} + +Control::TextDirection LinkButton::get_text_direction() const { + return text_direction; +} + +void LinkButton::clear_opentype_features() { + opentype_features.clear(); + _shape(); + update(); +} + +void LinkButton::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; + _shape(); + update(); + } +} + +int LinkButton::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]; +} + +void LinkButton::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + _shape(); + update(); + } +} + +String LinkButton::get_language() const { + return language; +} + void LinkButton::set_underline_mode(UnderlineMode p_underline_mode) { underline_mode = p_underline_mode; update(); @@ -50,11 +136,20 @@ LinkButton::UnderlineMode LinkButton::get_underline_mode() const { } Size2 LinkButton::get_minimum_size() const { - return get_theme_font("font")->get_string_size(text); + return text_buf->get_size(); } void LinkButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + update(); + } break; + case NOTIFICATION_THEME_CHANGED: { + _shape(); + minimum_size_changed(); + update(); + } break; case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); Size2 size = get_size(); @@ -63,69 +158,150 @@ 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_color_pressed")) { - color = get_theme_color("font_color_pressed"); + 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_color_hover"); + color = get_theme_color(SNAME("font_hover_color")); do_underline = underline_mode != UNDERLINE_MODE_NEVER; } break; case DRAW_DISABLED: { - color = get_theme_color("font_color_disabled"); + 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)); } - Ref<Font> font = get_theme_font("font"); + int width = text_buf->get_line_width(); - draw_string(font, Vector2(0, font->get_ascent()), text, color); + 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); + } + text_buf->draw(get_canvas_item(), Vector2(size.width - width, 0), color); + } else { + if (outline_size > 0 && font_outline_color.a > 0) { + text_buf->draw_outline(get_canvas_item(), Vector2(0, 0), outline_size, font_outline_color); + } + text_buf->draw(get_canvas_item(), Vector2(0, 0), color); + } if (do_underline) { - int underline_spacing = get_theme_constant("underline_spacing") + font->get_underline_position(); - int width = font->get_string_size(text).width; - int y = font->get_ascent() + underline_spacing; + int underline_spacing = get_theme_constant(SNAME("underline_spacing")) + text_buf->get_line_underline_position(); + int y = text_buf->get_line_ascent() + underline_spacing; - draw_line(Vector2(0, y), Vector2(width, y), color, font->get_underline_thickness()); + if (is_layout_rtl()) { + draw_line(Vector2(size.width - width, y), Vector2(size.width, y), color, text_buf->get_line_underline_thickness()); + } else { + draw_line(Vector2(0, y), Vector2(width, y), color, text_buf->get_line_underline_thickness()); + } } } break; } } +bool LinkButton::_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); + _shape(); + update(); + } + } else { + if ((double)opentype_features[tag] != value) { + opentype_features[tag] = value; + _shape(); + update(); + } + } + notify_property_list_changed(); + return true; + } + + return false; +} + +bool LinkButton::_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 LinkButton::_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 LinkButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_text", "text"), &LinkButton::set_text); ClassDB::bind_method(D_METHOD("get_text"), &LinkButton::get_text); - + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &LinkButton::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &LinkButton::get_text_direction); + ClassDB::bind_method(D_METHOD("set_opentype_feature", "tag", "value"), &LinkButton::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "tag"), &LinkButton::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features"), &LinkButton::clear_opentype_features); + ClassDB::bind_method(D_METHOD("set_language", "language"), &LinkButton::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &LinkButton::get_language); ClassDB::bind_method(D_METHOD("set_underline_mode", "underline_mode"), &LinkButton::set_underline_mode); ClassDB::bind_method(D_METHOD("get_underline_mode"), &LinkButton::get_underline_mode); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &LinkButton::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &LinkButton::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &LinkButton::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &LinkButton::get_structured_text_bidi_override_options); BIND_ENUM_CONSTANT(UNDERLINE_MODE_ALWAYS); BIND_ENUM_CONSTANT(UNDERLINE_MODE_ON_HOVER); BIND_ENUM_CONSTANT(UNDERLINE_MODE_NEVER); ADD_PROPERTY(PropertyInfo(Variant::STRING, "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::INT, "underline", PROPERTY_HINT_ENUM, "Always,On Hover,Never"), "set_underline_mode", "get_underline_mode"); + 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"); } LinkButton::LinkButton() { - underline_mode = UNDERLINE_MODE_ALWAYS; + 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 b8469b529a..7eaa9f88b6 100644 --- a/scene/gui/link_button.h +++ b/scene/gui/link_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -33,6 +33,7 @@ #include "scene/gui/base_button.h" #include "scene/resources/bit_map.h" +#include "scene/resources/text_line.h" class LinkButton : public BaseButton { GDCLASS(LinkButton, BaseButton); @@ -46,17 +47,46 @@ public: private: String text; - UnderlineMode underline_mode; + Ref<TextLine> text_buf; + UnderlineMode underline_mode = UNDERLINE_MODE_ALWAYS; + + Dictionary opentype_features; + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; + + void _shape(); protected: virtual Size2 get_minimum_size() const override; void _notification(int p_what); static void _bind_methods(); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; + public: void set_text(const String &p_text); String get_text() 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_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); + + void set_language(const String &p_language); + String get_language() const; + void set_underline_mode(UnderlineMode p_underline_mode); UnderlineMode get_underline_mode() const; diff --git a/scene/gui/margin_container.cpp b/scene/gui/margin_container.cpp index b674b492d8..50b4d192a9 100644 --- a/scene/gui/margin_container.cpp +++ b/scene/gui/margin_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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/margin_container.h b/scene/gui/margin_container.h index 12e230d9d7..b782976ada 100644 --- a/scene/gui/margin_container.h +++ b/scene/gui/margin_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index d65e98ea46..e312aaed57 100644 --- a/scene/gui/menu_button.cpp +++ b/scene/gui/menu_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -33,26 +33,60 @@ #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()) { + return; + } + if (disable_shortcuts) { 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; } - //bool global_only = (get_viewport()->get_modal_stack_top() && !get_viewport()->get_modal_stack_top()->is_a_parent_of(this)); - //if (popup->activate_item_by_event(p_event, global_only)) - // accept_event(); if (popup->activate_item_by_event(p_event, false)) { accept_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(); @@ -66,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 { @@ -91,16 +125,27 @@ 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; } } void MenuButton::_bind_methods() { ClassDB::bind_method(D_METHOD("get_popup"), &MenuButton::get_popup); - ClassDB::bind_method(D_METHOD("_unhandled_key_input"), &MenuButton::_unhandled_key_input); ClassDB::bind_method(D_METHOD("_set_items"), &MenuButton::_set_items); ClassDB::bind_method(D_METHOD("_get_items"), &MenuButton::_get_items); ClassDB::bind_method(D_METHOD("set_switch_on_hover", "enable"), &MenuButton::set_switch_on_hover); @@ -118,18 +163,18 @@ void MenuButton::set_disable_shortcuts(bool p_disabled) { } MenuButton::MenuButton() { - switch_on_hover = false; set_flat(true); set_toggle_mode(true); set_disable_shortcuts(false); set_process_unhandled_key_input(true); + set_focus_mode(FOCUS_NONE); set_action_mode(ACTION_MODE_BUTTON_PRESS); 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)); + 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 6330899ad3..730495b65d 100644 --- a/scene/gui/menu_button.h +++ b/scene/gui/menu_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -37,20 +37,24 @@ class MenuButton : public Button { GDCLASS(MenuButton, Button); - bool clicked; - bool switch_on_hover; - bool disable_shortcuts; + bool clicked = false; + bool switch_on_hover = false; + bool disable_shortcuts = false; PopupMenu *popup; - void _unhandled_key_input(Ref<InputEvent> p_event); + 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(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 bc71ae94f5..8bf25ac915 100644 --- a/scene/gui/nine_patch_rect.cpp +++ b/scene/gui/nine_patch_rect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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) { @@ -44,12 +45,12 @@ void NinePatchRect::_notification(int p_what) { texture->get_rect_region(rect, src_rect, rect, src_rect); RID ci = get_canvas_item(); - RS::get_singleton()->canvas_item_add_nine_patch(ci, rect, src_rect, texture->get_rid(), Vector2(margin[MARGIN_LEFT], margin[MARGIN_TOP]), Vector2(margin[MARGIN_RIGHT], margin[MARGIN_BOTTOM]), RS::NinePatchAxisMode(axis_h), RS::NinePatchAxisMode(axis_v), draw_center); + RS::get_singleton()->canvas_item_add_nine_patch(ci, rect, src_rect, texture->get_rid(), Vector2(margin[SIDE_LEFT], margin[SIDE_TOP]), Vector2(margin[SIDE_RIGHT], margin[SIDE_BOTTOM]), RS::NinePatchAxisMode(axis_h), RS::NinePatchAxisMode(axis_v), draw_center); } } Size2 NinePatchRect::get_minimum_size() const { - return Size2(margin[MARGIN_LEFT] + margin[MARGIN_RIGHT], margin[MARGIN_TOP] + margin[MARGIN_BOTTOM]); + return Size2(margin[SIDE_LEFT] + margin[SIDE_RIGHT], margin[SIDE_TOP] + margin[SIDE_BOTTOM]); } void NinePatchRect::_bind_methods() { @@ -73,10 +74,10 @@ void NinePatchRect::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::RECT2, "region_rect"), "set_region_rect", "get_region_rect"); ADD_GROUP("Patch Margin", "patch_margin_"); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_left", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_top", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_right", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_bottom", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", MARGIN_BOTTOM); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_left", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_top", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_right", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "patch_margin_bottom", PROPERTY_HINT_RANGE, "0,16384,1"), "set_patch_margin", "get_patch_margin", SIDE_BOTTOM); ADD_GROUP("Axis Stretch", "axis_stretch_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "axis_stretch_horizontal", PROPERTY_HINT_ENUM, "Stretch,Tile,Tile Fit"), "set_h_axis_stretch_mode", "get_h_axis_stretch_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "axis_stretch_vertical", PROPERTY_HINT_ENUM, "Stretch,Tile,Tile Fit"), "set_v_axis_stretch_mode", "get_v_axis_stretch_mode"); @@ -97,38 +98,23 @@ 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"); - _change_notify("texture"); + emit_signal(SceneStringNames::get_singleton()->texture_changed); } Ref<Texture2D> NinePatchRect::get_texture() const { return texture; } -void NinePatchRect::set_patch_margin(Margin p_margin, int p_size) { - ERR_FAIL_INDEX((int)p_margin, 4); - margin[p_margin] = p_size; +void NinePatchRect::set_patch_margin(Side p_side, int p_size) { + ERR_FAIL_INDEX((int)p_side, 4); + margin[p_side] = p_size; update(); minimum_size_changed(); - switch (p_margin) { - case MARGIN_LEFT: - _change_notify("patch_margin_left"); - break; - case MARGIN_TOP: - _change_notify("patch_margin_top"); - break; - case MARGIN_RIGHT: - _change_notify("patch_margin_right"); - break; - case MARGIN_BOTTOM: - _change_notify("patch_margin_bottom"); - break; - } } -int NinePatchRect::get_patch_margin(Margin p_margin) const { - ERR_FAIL_INDEX_V((int)p_margin, 4, 0); - return margin[p_margin]; +int NinePatchRect::get_patch_margin(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, 0); + return margin[p_side]; } void NinePatchRect::set_region_rect(const Rect2 &p_region_rect) { @@ -139,7 +125,6 @@ void NinePatchRect::set_region_rect(const Rect2 &p_region_rect) { region_rect = p_region_rect; item_rect_changed(); - _change_notify("region_rect"); } Rect2 NinePatchRect::get_region_rect() const { @@ -174,16 +159,7 @@ NinePatchRect::AxisStretchMode NinePatchRect::get_v_axis_stretch_mode() const { } NinePatchRect::NinePatchRect() { - margin[MARGIN_LEFT] = 0; - margin[MARGIN_RIGHT] = 0; - margin[MARGIN_BOTTOM] = 0; - margin[MARGIN_TOP] = 0; - set_mouse_filter(MOUSE_FILTER_IGNORE); - draw_center = true; - - axis_h = AXIS_STRETCH_MODE_STRETCH; - axis_v = AXIS_STRETCH_MODE_STRETCH; } NinePatchRect::~NinePatchRect() { diff --git a/scene/gui/nine_patch_rect.h b/scene/gui/nine_patch_rect.h index a539ad43c0..f9a3f31fe5 100644 --- a/scene/gui/nine_patch_rect.h +++ b/scene/gui/nine_patch_rect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -43,12 +43,13 @@ public: AXIS_STRETCH_MODE_TILE_FIT, }; - bool draw_center; - int margin[4]; + bool draw_center = true; + int margin[4] = {}; Rect2 region_rect; Ref<Texture2D> texture; - AxisStretchMode axis_h, axis_v; + AxisStretchMode axis_h = AXIS_STRETCH_MODE_STRETCH; + AxisStretchMode axis_v = AXIS_STRETCH_MODE_STRETCH; protected: void _notification(int p_what); @@ -59,8 +60,8 @@ public: void set_texture(const Ref<Texture2D> &p_tex); Ref<Texture2D> get_texture() const; - void set_patch_margin(Margin p_margin, int p_size); - int get_patch_margin(Margin p_margin) const; + void set_patch_margin(Side p_side, int p_size); + int get_patch_margin(Side p_side) const; void set_region_rect(const Rect2 &p_region_rect); Rect2 get_region_rect() const; diff --git a/scene/gui/option_button.cpp b/scene/gui/option_button.cpp index f0e69a94a4..cd55f258b3 100644 --- a/scene/gui/option_button.cpp +++ b/scene/gui/option_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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,37 +52,50 @@ 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_color_pressed"); + clr = get_theme_color(SNAME("font_pressed_color")); break; case DRAW_HOVER: - clr = get_theme_color("font_color_hover"); + clr = get_theme_color(SNAME("font_hover_color")); break; case DRAW_DISABLED: - clr = get_theme_color("font_color_disabled"); + clr = get_theme_color(SNAME("font_disabled_color")); break; default: - clr = get_theme_color("font_color"); + clr = get_theme_color(SNAME("font_color")); } } Size2 size = get_size(); - Point2 ofs(size.width - arrow->get_width() - get_theme_constant("arrow_margin"), int(Math::abs((size.height - arrow->get_height()) / 2))); + Point2 ofs; + if (is_layout_rtl()) { + 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(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")) { - _set_internal_margin(MARGIN_RIGHT, Control::get_theme_icon("arrow")->get_width()); + if (has_theme_icon(SNAME("arrow"))) { + if (is_layout_rtl()) { + _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(SNAME("arrow"))->get_width()); + } } } break; case NOTIFICATION_VISIBILITY_CHANGED: { @@ -94,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) { @@ -207,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); } } @@ -323,13 +336,18 @@ void OptionButton::_bind_methods() { } OptionButton::OptionButton() { - current = -1; set_toggle_mode(true); set_text_align(ALIGN_LEFT); - set_action_mode(ACTION_MODE_BUTTON_PRESS); - if (has_theme_icon("arrow")) { - _set_internal_margin(MARGIN_RIGHT, Control::get_theme_icon("arrow")->get_width()); + if (is_layout_rtl()) { + if (has_theme_icon(SNAME("arrow"))) { + _set_internal_margin(SIDE_LEFT, Control::get_theme_icon(SNAME("arrow"))->get_width()); + } + } else { + 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(); diff --git a/scene/gui/option_button.h b/scene/gui/option_button.h index fec7695969..d846e395ad 100644 --- a/scene/gui/option_button.h +++ b/scene/gui/option_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -38,7 +38,7 @@ class OptionButton : public Button { GDCLASS(OptionButton, Button); PopupMenu *popup; - int current; + int current = -1; void _focused(int p_which); void _selected(int p_which); diff --git a/scene/gui/panel.cpp b/scene/gui/panel.cpp index acbb6d7ab5..e8e7e3d997 100644 --- a/scene/gui/panel.cpp +++ b/scene/gui/panel.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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())); } } @@ -63,6 +63,3 @@ Panel::Panel() { // Has visible stylebox, so stop by default. set_mouse_filter(MOUSE_FILTER_STOP); } - -Panel::~Panel() { -} diff --git a/scene/gui/panel.h b/scene/gui/panel.h index a68c3d3f0c..84fd6aaead 100644 --- a/scene/gui/panel.h +++ b/scene/gui/panel.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -54,7 +54,6 @@ public: Mode get_mode() const; Panel(); - ~Panel(); }; VARIANT_ENUM_CAST(Panel::Mode) diff --git a/scene/gui/panel_container.cpp b/scene/gui/panel_container.cpp index 051b4de825..d910e1e882 100644 --- a/scene/gui/panel_container.cpp +++ b/scene/gui/panel_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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/panel_container.h b/scene/gui/panel_container.h index 92743f2c47..f27ca7fad7 100644 --- a/scene/gui/panel_container.h +++ b/scene/gui/panel_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ diff --git a/scene/gui/popup.cpp b/scene/gui/popup.cpp index 49ddd5c3ee..f7e7e1cd60 100644 --- a/scene/gui/popup.cpp +++ b/scene/gui/popup.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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; @@ -93,7 +94,7 @@ void Popup::_notification(int p_what) { } void Popup::_parent_focused() { - if (popped_up) { + if (popped_up && close_on_parent_focus) { _close_pressed(); } } @@ -103,16 +104,28 @@ 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() { set_size(get_contents_minimum_size()); } +void Popup::set_close_on_parent_focus(bool p_close) { + close_on_parent_focus = p_close; +} + +bool Popup::get_close_on_parent_focus() { + return close_on_parent_focus; +} + void Popup::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_close_on_parent_focus", "close"), &Popup::set_close_on_parent_focus); + ClassDB::bind_method(D_METHOD("get_close_on_parent_focus"), &Popup::get_close_on_parent_focus); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "close_on_parent_focus"), "set_close_on_parent_focus", "get_close_on_parent_focus"); + ADD_SIGNAL(MethodInfo("popup_hide")); } diff --git a/scene/gui/popup.h b/scene/gui/popup.h index 44577811ff..c7090e7231 100644 --- a/scene/gui/popup.h +++ b/scene/gui/popup.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -35,11 +35,14 @@ #include "core/templates/local_vector.h" +class Panel; + class Popup : public Window { GDCLASS(Popup, Window); LocalVector<Window *> visible_parents; bool popped_up = false; + bool close_on_parent_focus = true; void _input_from_window(const Ref<InputEvent> &p_event); @@ -57,6 +60,10 @@ protected: public: void set_as_minsize(); + + void set_close_on_parent_focus(bool p_close); + bool get_close_on_parent_focus(); + Popup(); ~Popup(); }; diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index 0a469d8373..c37095a9f8 100644 --- a/scene/gui/popup_menu.cpp +++ b/scene/gui/popup_menu.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,29 +36,25 @@ #include "core/string/print_string.h" #include "core/string/translation.h" -String PopupMenu::_get_accel_text(int p_item) const { - ERR_FAIL_INDEX_V(p_item, items.size(), String()); - - if (items[p_item].shortcut.is_valid()) { - return items[p_item].shortcut->get_as_text(); - } else if (items[p_item].accel) { - return keycode_get_string(items[p_item].accel); +String PopupMenu::_get_accel_text(const Item &p_item) const { + if (p_item.shortcut.is_valid()) { + return p_item.shortcut->get_as_text(); + } else if (p_item.accel) { + return keycode_get_string(p_item.accel); } return String(); } 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 - Ref<Font> font = get_theme_font("font"); - float max_w = 0; - float icon_w = 0; - int font_h = font->get_height(); - int check_w = MAX(get_theme_icon("checked")->get_width(), get_theme_icon("radio_checked")->get_width()) + hseparation; + float max_w = 0.0; + float icon_w = 0.0; + 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; @@ -66,7 +62,7 @@ Size2 PopupMenu::_get_contents_minimum_size() const { Size2 size; Size2 icon_size = items[i].get_icon_size(); - size.height = MAX(icon_size.height, font_h); + size.height = _get_item_height(i); icon_w = MAX(icon_size.width, icon_w); size.width += items[i].h_ofs; @@ -75,20 +71,17 @@ Size2 PopupMenu::_get_contents_minimum_size() const { has_check = true; } - String text = items[i].xl_text; - size.width += font->get_string_size(text).width; - if (i > 0) { - size.height += vseparation; - } + 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 += font->get_string_size(_get_accel_text(i)).width; + 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); @@ -96,7 +89,9 @@ Size2 PopupMenu::_get_contents_minimum_size() const { minsize.height += size.height; } - minsize.width += max_w + icon_w + accel_max_w; + 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) { minsize.width += check_w; } @@ -111,14 +106,35 @@ Size2 PopupMenu::_get_contents_minimum_size() const { return minsize; } +int PopupMenu::_get_item_height(int p_item) const { + ERR_FAIL_INDEX_V(p_item, items.size(), 0); + ERR_FAIL_COND_V(p_item < 0, 0); + + 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(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(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(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 font_height = get_theme_font("font")->get_height(); - 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; for (int i = 0; i < items.size(); i++) { - items_total_height += MAX(items[i].get_icon_size().height, font_height) + vsep; + items_total_height += _get_item_height(i) + vsep; } // Subtract a separator which is not needed for the last item. @@ -147,10 +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"); - float font_h = get_theme_font("font")->get_height(); + int vseparation = get_theme_constant(SNAME("vseparation")); Point2 ofs = style->get_offset() + Point2(0, vseparation / 2); @@ -159,11 +174,9 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { } for (int i = 0; i < items.size(); i++) { - if (i > 0) { - ofs.y += vseparation; - } + ofs.y += i > 0 ? vseparation : (float)vseparation / 2; - ofs.y += MAX(items[i].get_icon_size().height, font_h); + ofs.y += _get_item_height(i); if (p_over.y - control->get_position().y < ofs.y) { return i; @@ -173,48 +186,59 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { return -1; } -void PopupMenu::_activate_submenu(int over) { - Node *n = get_node(items[over].submenu); - ERR_FAIL_COND_MSG(!n, "Item subnode does not exist: " + items[over].submenu + "."); +void PopupMenu::_activate_submenu(int p_over) { + Node *n = get_node(items[p_over].submenu); + ERR_FAIL_COND_MSG(!n, "Item subnode does not exist: " + items[p_over].submenu + "."); Popup *submenu_popup = Object::cast_to<Popup>(n); - ERR_FAIL_COND_MSG(!submenu_popup, "Item subnode is not a Popup: " + items[over].submenu + "."); + ERR_FAIL_COND_MSG(!submenu_popup, "Item subnode is not a Popup: " + items[p_over].submenu + "."); if (submenu_popup->is_visible()) { 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()); float scroll_offset = control->get_position().y; - Point2 submenu_pos = this_pos + Point2(this_rect.size.width, items[over]._ofs_cache + scroll_offset); + Point2 submenu_pos; Size2 submenu_size = submenu_popup->get_size(); + if (control->is_layout_rtl()) { + submenu_pos = this_pos + Point2(-submenu_size.width, items[p_over]._ofs_cache + scroll_offset); + } else { + submenu_pos = this_pos + Point2(this_rect.size.width, items[p_over]._ofs_cache + scroll_offset); + } // Fix pos if going outside parent rect + if (submenu_pos.x < get_parent_rect().position.x) { + submenu_pos.x = this_pos.x + submenu_size.width; + } + if (submenu_pos.x + submenu_size.width > get_parent_rect().size.width) { submenu_pos.x = this_pos.x - submenu_size.width; } + submenu_popup->set_close_on_parent_focus(false); submenu_popup->set_position(submenu_pos); - submenu_popup->set_as_minsize(); // Shrink the popup size to it's contents. + submenu_popup->set_as_minsize(); // Shrink the popup size to its contents. submenu_popup->popup(); // 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(); // Autohide area above the submenu item submenu_pum->clear_autohide_areas(); - submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y, this_rect.size.x, items[over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2)); + submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y, this_rect.size.x, items[p_over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2)); // If there is an area below the submenu item, add an autohide area there. - if (items[over]._ofs_cache + items[over]._height_cache + scroll_offset <= control->get_size().height) { - int from = items[over]._ofs_cache + items[over]._height_cache + scroll_offset + vsep / 2 + style->get_offset().height; + if (items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset <= control->get_size().height) { + int from = items[p_over]._ofs_cache + items[p_over]._height_cache + scroll_offset + vsep / 2 + style->get_offset().height; submenu_pum->add_autohide_area(Rect2(this_rect.position.x, this_rect.position.y + from, this_rect.size.x, this_rect.size.y - from)); } } @@ -228,39 +252,73 @@ void PopupMenu::_submenu_timeout() { submenu_over = -1; } -void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { - if (p_event->is_action("ui_down") && p_event->is_pressed() && mouse_over != items.size() - 1) { +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()) { int search_from = mouse_over + 1; if (search_from >= items.size()) { search_from = 0; } + bool match_found = false; 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(); + match_found = true; break; } } - } else if (p_event->is_action("ui_up") && p_event->is_pressed() && mouse_over != 0) { + + if (!match_found) { + // If the last item is not selectable, try re-searching from the start. + for (int i = 0; i < search_from; i++) { + if (!items[i].separator && !items[i].disabled) { + mouse_over = i; + emit_signal(SNAME("id_focused"), i); + _scroll_to_item(i); + control->update(); + set_input_as_handled(); + break; + } + } + } + } else if (p_event->is_action("ui_up") && p_event->is_pressed()) { int search_from = mouse_over - 1; if (search_from < 0) { search_from = items.size() - 1; } + bool match_found = false; 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(); + match_found = true; break; } } + + if (!match_found) { + // If the first item is not selectable, try re-searching from the end. + for (int i = items.size() - 1; i >= search_from; i--) { + if (!items[i].separator && !items[i].disabled) { + mouse_over = i; + emit_signal(SNAME("id_focused"), i); + _scroll_to_item(i); + control->update(); + set_input_as_handled(); + break; + } + } + } } else if (p_event->is_action("ui_left") && p_event->is_pressed()) { Node *n = get_parent(); if (n && Object::cast_to<PopupMenu>(n)) { @@ -286,7 +344,11 @@ void PopupMenu::_gui_input(const Ref<InputEvent> &p_event) { // Make an area which does not include v scrollbar, so that items are not activated when dragging scrollbar. Rect2 item_clickable_area = scroll_container->get_rect(); if (scroll_container->get_v_scrollbar()->is_visible_in_tree()) { - item_clickable_area.size.width -= scroll_container->get_v_scrollbar()->get_size().width; + if (is_layout_rtl()) { + item_clickable_area.position.x += scroll_container->get_v_scrollbar()->get_size().width; + } else { + item_clickable_area.size.width -= scroll_container->get_v_scrollbar()->get_size().width; + } } Ref<InputEventMouseButton> b = p_event; @@ -297,10 +359,11 @@ 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 (button_idx == BUTTON_LEFT || (initial_button_mask & (1 << (button_idx - 1)))) { + 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; initial_button_mask = 0; @@ -336,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); @@ -398,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(); @@ -413,27 +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(SNAME("item_start_padding")); + int item_end_padding = get_theme_constant(SNAME("item_end_padding")); - Ref<StyleBox> style = get_theme_stylebox("panel"); - Ref<StyleBox> hover = get_theme_stylebox("hover"); - Ref<Font> font = get_theme_font("font"); + bool rtl = control->is_layout_rtl(); + 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> submenu = get_theme_icon("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"); - - int vseparation = get_theme_constant("vseparation"); - int hseparation = get_theme_constant("hseparation"); - Color font_color = get_theme_color("font_color"); - Color font_color_disabled = get_theme_color("font_color_disabled"); - Color font_color_accel = get_theme_color("font_color_accel"); - Color font_color_hover = get_theme_color("font_color_hover"); - float font_h = font->get_height(); + 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(SNAME("submenu_mirrored")); + } else { + submenu = get_theme_icon(SNAME("submenu")); + } + + 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(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; @@ -454,24 +527,28 @@ 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(); // Loop through all items and draw each. for (int i = 0; i < items.size(); i++) { - // If not the first item, add the separation space between items. - if (i > 0) { - ofs.y += vseparation; - } + // For the first item only add half a separation. For all other items, add a whole separation to the offset. + ofs.y += i > 0 ? vseparation : (float)vseparation / 2; + + _shape_item(i); Point2 item_ofs = ofs; Size2 icon_size = items[i].get_icon_size(); - float h = MAX(icon_size.height, font_h); + float h = _get_item_height(i); if (i == mouse_over) { - hover->draw(ci, Rect2(item_ofs + Point2(-hseparation, -vseparation / 2), Size2(display_width + hseparation * 2, h + vseparation))); + if (rtl) { + hover->draw(ci, Rect2(item_ofs + Point2(scroll_width, -vseparation / 2), Size2(display_width, h + vseparation))); + } else { + hover->draw(ci, Rect2(item_ofs + Point2(0, -vseparation / 2), Size2(display_width, h + vseparation))); + } } String text = items[i].xl_text; @@ -480,57 +557,97 @@ void PopupMenu::_draw_items() { item_ofs.x += items[i].h_ofs; if (items[i].separator) { int sep_h = separator->get_center_size().height + separator->get_minimum_size().height; + int sep_ofs = Math::floor((h - sep_h) / 2.0); if (text != String()) { - int text_size = font->get_string_size(text).width; + int text_size = items[i].text_buf->get_size().width; int text_center = display_width / 2; int text_left = text_center - text_size / 2; int text_right = text_center + text_size / 2; if (text_left > item_ofs.x) { - labeled_separator_left->draw(ci, Rect2(item_ofs + Point2(0, Math::floor((h - sep_h) / 2.0)), Size2(MAX(0, text_left - item_ofs.x), sep_h))); + labeled_separator_left->draw(ci, Rect2(item_ofs + Point2(0, sep_ofs), Size2(MAX(0, text_left - item_ofs.x), sep_h))); } if (text_right < display_width) { - labeled_separator_right->draw(ci, Rect2(Point2(text_right, item_ofs.y + Math::floor((h - sep_h) / 2.0)), Size2(MAX(0, display_width - text_right), sep_h))); + labeled_separator_right->draw(ci, Rect2(Point2(text_right, item_ofs.y + sep_ofs), Size2(MAX(0, display_width - text_right), sep_h))); } } else { - separator->draw(ci, Rect2(item_ofs + Point2(0, Math::floor((h - sep_h) / 2.0)), Size2(display_width, sep_h))); + separator->draw(ci, Rect2(item_ofs + Point2(0, sep_ofs), Size2(display_width, sep_h))); } } Color icon_color(1, 1, 1, items[i].disabled ? 0.5 : 1); + // For non-separator items, add some padding for the content. + item_ofs.x += item_start_padding; + // Checkboxes if (items[i].checkable_type) { Texture2D *icon = (items[i].checked ? check[items[i].checkable_type - 1] : uncheck[items[i].checkable_type - 1]).ptr(); - icon->draw(ci, item_ofs + Point2(0, Math::floor((h - icon->get_height()) / 2.0)), icon_color); + if (rtl) { + icon->draw(ci, Size2(control->get_size().width - item_ofs.x - icon->get_width(), item_ofs.y) + Point2(0, Math::floor((h - icon->get_height()) / 2.0)), icon_color); + } else { + icon->draw(ci, item_ofs + Point2(0, Math::floor((h - icon->get_height()) / 2.0)), icon_color); + } } // Icon if (!items[i].icon.is_null()) { - items[i].icon->draw(ci, item_ofs + Size2(check_ofs, 0) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color); + if (rtl) { + items[i].icon->draw(ci, Size2(control->get_size().width - item_ofs.x - check_ofs - icon_size.width, item_ofs.y) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color); + } else { + items[i].icon->draw(ci, item_ofs + Size2(check_ofs, 0) + Point2(0, Math::floor((h - icon_size.height) / 2.0)), icon_color); + } } // Submenu arrow on right hand side if (items[i].submenu != "") { - submenu->draw(ci, Point2(display_width - submenu->get_width(), item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color); + if (rtl) { + submenu->draw(ci, Point2(scroll_width + style->get_margin(SIDE_LEFT) + item_end_padding, item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color); + } else { + submenu->draw(ci, Point2(display_width - style->get_margin(SIDE_RIGHT) - submenu->get_width() - item_end_padding, item_ofs.y + Math::floor(h - submenu->get_height()) / 2), icon_color); + } } // Text - item_ofs.y += font->get_ascent(); + 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 - font->get_string_size(text).width) / 2; - font->draw(ci, Point2(center, item_ofs.y + Math::floor((h - font_h) / 2.0)), text, font_color_disabled); + int center = (display_width - items[i].text_buf->get_size().width) / 2; + Vector2 text_pos = Point2(center, item_ofs.y + Math::floor((h - items[i].text_buf->get_size().y) / 2.0)); + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + items[i].text_buf->draw(ci, text_pos, font_separator_color); } } else { item_ofs.x += icon_ofs + check_ofs; - font->draw(ci, item_ofs + Point2(0, Math::floor((h - font_h) / 2.0)), text, items[i].disabled ? font_color_disabled : (i == mouse_over ? font_color_hover : font_color)); + if (rtl) { + Vector2 text_pos = Size2(control->get_size().width - items[i].text_buf->get_size().width - item_ofs.x, item_ofs.y) + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0)); + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + items[i].text_buf->draw(ci, text_pos, items[i].disabled ? font_disabled_color : (i == mouse_over ? font_hover_color : font_color)); + } else { + Vector2 text_pos = item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0)); + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + items[i].text_buf->draw(ci, text_pos, items[i].disabled ? font_disabled_color : (i == mouse_over ? font_hover_color : font_color)); + } } // Accelerator / Shortcut - if (items[i].accel || (items[i].shortcut.is_valid() && items[i].shortcut->is_valid())) { - String sc_text = _get_accel_text(i); - item_ofs.x = display_width - font->get_string_size(sc_text).width; - font->draw(ci, item_ofs + Point2(0, Math::floor((h - font_h) / 2.0)), sc_text, i == mouse_over ? font_color_hover : font_color_accel); + 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 { + item_ofs.x = display_width - style->get_margin(SIDE_RIGHT) - items[i].accel_text_buf->get_size().x - item_end_padding; + } + Vector2 text_pos = item_ofs + Point2(0, Math::floor((h - items[i].text_buf->get_size().y) / 2.0)); + if (outline_size > 0 && font_outline_color.a > 0) { + items[i].accel_text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + items[i].accel_text_buf->draw(ci, text_pos, i == mouse_over ? font_hover_color : font_accelerator_color); } // Cache the item vertical offset from the first item and the height @@ -542,11 +659,57 @@ 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())); } +void PopupMenu::_minimum_lifetime_timeout() { + close_allowed = true; + // If the mouse still isn't in this popup after timer expires, close. + if (!get_visible_rect().has_point(get_mouse_position())) { + _close_pressed(); + } +} + +void PopupMenu::_close_pressed() { + // Only apply minimum lifetime to submenus. + PopupMenu *parent_pum = Object::cast_to<PopupMenu>(get_parent()); + if (!parent_pum) { + Popup::_close_pressed(); + return; + } + + // If the timer has expired, close. If timer is still running, do nothing. + if (close_allowed) { + close_allowed = false; + Popup::_close_pressed(); + } else if (minimum_lifetime_timer->is_stopped()) { + minimum_lifetime_timer->start(); + } +} + +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(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); + } else { + items.write[p_item].text_buf->set_direction((TextServer::Direction)items[p_item].text_direction); + } + items.write[p_item].text_buf->add_string(items.write[p_item].xl_text, font, font_size, items[p_item].opentype_features, (items[p_item].language != "") ? items[p_item].language : TranslationServer::get_singleton()->get_tool_locale()); + + items.write[p_item].accel_text_buf->clear(); + items.write[p_item].accel_text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + items.write[p_item].accel_text_buf->add_string(_get_accel_text(items.write[p_item]), font, font_size, Dictionary(), TranslationServer::get_singleton()->get_tool_locale()); + items.write[p_item].dirty = false; + } +} + void PopupMenu::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { @@ -557,16 +720,20 @@ void PopupMenu::_notification(int p_what) { set_submenu_popup_delay(pm_delay); } } break; + case NOTIFICATION_THEME_CHANGED: + 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); } child_controls_changed(); control->update(); } break; case NOTIFICATION_WM_MOUSE_ENTER: { - //grab_focus(); + grab_focus(); } break; case NOTIFICATION_WM_MOUSE_EXIT: { if (mouse_over >= 0 && (items[mouse_over].submenu == "" || submenu_over != -1)) { @@ -582,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; } @@ -621,16 +788,16 @@ 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"); - margin_container->add_theme_constant_override("margin_top", panel_style->get_margin(Margin::MARGIN_TOP)); - margin_container->add_theme_constant_override("margin_bottom", panel_style->get_margin(Margin::MARGIN_BOTTOM)); - margin_container->add_theme_constant_override("margin_left", panel_style->get_margin(Margin::MARGIN_LEFT)); - margin_container->add_theme_constant_override("margin_right", panel_style->get_margin(Margin::MARGIN_RIGHT)); + 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)); + margin_container->add_theme_constant_override("margin_right", panel_style->get_margin(Side::SIDE_RIGHT)); } } break; } @@ -642,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; @@ -650,6 +817,7 @@ void PopupMenu::add_item(const String &p_label, int p_id, uint32_t p_accel) { Item item; ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -659,6 +827,7 @@ void PopupMenu::add_icon_item(const Ref<Texture2D> &p_icon, const String &p_labe ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); item.icon = p_icon; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -668,6 +837,7 @@ void PopupMenu::add_check_item(const String &p_label, int p_id, uint32_t p_accel ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -678,6 +848,7 @@ void PopupMenu::add_icon_check_item(const Ref<Texture2D> &p_icon, const String & item.icon = p_icon; item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -687,6 +858,7 @@ void PopupMenu::add_radio_check_item(const String &p_label, int p_id, uint32_t p ITEM_SETUP_WITH_ACCEL(p_label, p_id, p_accel); item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -697,6 +869,7 @@ void PopupMenu::add_icon_radio_check_item(const Ref<Texture2D> &p_icon, const St item.icon = p_icon; item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -707,6 +880,7 @@ void PopupMenu::add_multistate_item(const String &p_label, int p_max_states, int item.max_states = p_max_states; item.state = p_default_state; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -715,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; @@ -724,6 +898,7 @@ void PopupMenu::add_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bool p_g Item item; ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global); items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -733,6 +908,7 @@ void PopupMenu::add_icon_shortcut(const Ref<Texture2D> &p_icon, const Ref<Shortc ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global); item.icon = p_icon; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -742,6 +918,7 @@ void PopupMenu::add_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_id, bo ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global); item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -752,6 +929,7 @@ void PopupMenu::add_icon_check_shortcut(const Ref<Texture2D> &p_icon, const Ref< item.icon = p_icon; item.checkable_type = Item::CHECKABLE_TYPE_CHECK_BOX; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -761,6 +939,7 @@ void PopupMenu::add_radio_check_shortcut(const Ref<Shortcut> &p_shortcut, int p_ ITEM_SETUP_WITH_SHORTCUT(p_shortcut, p_id, p_global); item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -771,6 +950,7 @@ void PopupMenu::add_icon_radio_check_shortcut(const Ref<Texture2D> &p_icon, cons item.icon = p_icon; item.checkable_type = Item::CHECKABLE_TYPE_RADIO_BUTTON; items.push_back(item); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -778,10 +958,11 @@ 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); + _shape_item(items.size() - 1); control->update(); child_controls_changed(); } @@ -794,12 +975,49 @@ 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(); child_controls_changed(); } +void PopupMenu::set_item_text_direction(int p_item, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_item, items.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (items[p_item].text_direction != p_text_direction) { + items.write[p_item].text_direction = p_text_direction; + items.write[p_item].dirty = true; + control->update(); + } +} + +void PopupMenu::clear_item_opentype_features(int p_item) { + ERR_FAIL_INDEX(p_item, items.size()); + items.write[p_item].opentype_features.clear(); + items.write[p_item].dirty = true; + control->update(); +} + +void PopupMenu::set_item_opentype_feature(int p_item, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_item, items.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!items[p_item].opentype_features.has(tag) || (int)items[p_item].opentype_features[tag] != p_value) { + items.write[p_item].opentype_features[tag] = p_value; + items.write[p_item].dirty = true; + control->update(); + } +} + +void PopupMenu::set_item_language(int p_item, const String &p_language) { + ERR_FAIL_INDEX(p_item, items.size()); + if (items[p_item].language != p_language) { + items.write[p_item].language = p_language; + items.write[p_item].dirty = true; + control->update(); + } +} + void PopupMenu::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].icon = p_icon; @@ -828,6 +1046,7 @@ void PopupMenu::set_item_id(int p_idx, int p_id) { void PopupMenu::set_item_accelerator(int p_idx, uint32_t p_accel) { ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].accel = p_accel; + items.write[p_idx].dirty = true; control->update(); child_controls_changed(); @@ -866,6 +1085,25 @@ String PopupMenu::get_item_text(int p_idx) const { return items[p_idx].text; } +Control::TextDirection PopupMenu::get_item_text_direction(int p_item) const { + ERR_FAIL_INDEX_V(p_item, items.size(), Control::TEXT_DIRECTION_INHERITED); + return items[p_item].text_direction; +} + +int PopupMenu::get_item_opentype_feature(int p_item, const String &p_name) const { + ERR_FAIL_INDEX_V(p_item, items.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!items[p_item].opentype_features.has(tag)) { + return -1; + } + return items[p_item].opentype_features[tag]; +} + +String PopupMenu::get_item_language(int p_item) const { + ERR_FAIL_INDEX_V(p_item, items.size(), ""); + return items[p_item].language; +} + int PopupMenu::get_item_idx_from_text(const String &text) const { for (int idx = 0; idx < items.size(); idx++) { if (items[idx].text == text) { @@ -972,6 +1210,7 @@ void PopupMenu::set_item_shortcut(int p_idx, const Ref<Shortcut> &p_shortcut, bo } items.write[p_idx].shortcut = p_shortcut; items.write[p_idx].shortcut_is_global = p_global; + items.write[p_idx].dirty = true; if (items[p_idx].shortcut.is_valid()) { _ref_shortcut(items[p_idx].shortcut); @@ -1037,24 +1276,24 @@ 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->get_control()) { + if (k->is_ctrl_pressed()) { code |= KEY_MASK_CTRL; } - if (k->get_alt()) { + if (k->is_alt_pressed()) { code |= KEY_MASK_ALT; } - if (k->get_metakey()) { + if (k->is_meta_pressed()) { code |= KEY_MASK_META; } - if (k->get_shift()) { + if (k->is_shift_pressed()) { code |= KEY_MASK_SHIFT; } } @@ -1064,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; } @@ -1139,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(); @@ -1159,13 +1398,13 @@ void PopupMenu::remove_item(int p_idx) { child_controls_changed(); } -void PopupMenu::add_separator(const String &p_text) { +void PopupMenu::add_separator(const String &p_text, int p_id) { Item sep; sep.separator = true; - sep.id = -1; + 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(); @@ -1343,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)); @@ -1364,6 +1601,9 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("add_submenu_item", "label", "submenu", "id"), &PopupMenu::add_submenu_item, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("set_item_text", "idx", "text"), &PopupMenu::set_item_text); + ClassDB::bind_method(D_METHOD("set_item_text_direction", "idx", "direction"), &PopupMenu::set_item_text_direction); + ClassDB::bind_method(D_METHOD("set_item_opentype_feature", "idx", "tag", "value"), &PopupMenu::set_item_opentype_feature); + ClassDB::bind_method(D_METHOD("set_item_language", "idx", "language"), &PopupMenu::set_item_language); ClassDB::bind_method(D_METHOD("set_item_icon", "idx", "icon"), &PopupMenu::set_item_icon); ClassDB::bind_method(D_METHOD("set_item_checked", "idx", "checked"), &PopupMenu::set_item_checked); ClassDB::bind_method(D_METHOD("set_item_id", "idx", "id"), &PopupMenu::set_item_id); @@ -1383,6 +1623,10 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("toggle_item_multistate", "idx"), &PopupMenu::toggle_item_multistate); ClassDB::bind_method(D_METHOD("get_item_text", "idx"), &PopupMenu::get_item_text); + ClassDB::bind_method(D_METHOD("get_item_text_direction", "idx"), &PopupMenu::get_item_text_direction); + ClassDB::bind_method(D_METHOD("get_item_opentype_feature", "idx", "tag"), &PopupMenu::get_item_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_item_opentype_features", "idx"), &PopupMenu::clear_item_opentype_features); + ClassDB::bind_method(D_METHOD("get_item_language", "idx"), &PopupMenu::get_item_language); ClassDB::bind_method(D_METHOD("get_item_icon", "idx"), &PopupMenu::get_item_icon); ClassDB::bind_method(D_METHOD("is_item_checked", "idx"), &PopupMenu::is_item_checked); ClassDB::bind_method(D_METHOD("get_item_id", "idx"), &PopupMenu::get_item_id); @@ -1403,7 +1647,7 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_item", "idx"), &PopupMenu::remove_item); - ClassDB::bind_method(D_METHOD("add_separator", "label"), &PopupMenu::add_separator, DEFVAL(String())); + ClassDB::bind_method(D_METHOD("add_separator", "label", "id"), &PopupMenu::add_separator, DEFVAL(String()), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("clear"), &PopupMenu::clear); ClassDB::bind_method(D_METHOD("_set_items"), &PopupMenu::_set_items); @@ -1439,14 +1683,13 @@ void PopupMenu::_bind_methods() { void PopupMenu::popup(const Rect2 &p_bounds) { moved = Vector2(); popup_time_msec = OS::get_singleton()->get_ticks_msec(); - set_as_minsize(); Popup::popup(p_bounds); } PopupMenu::PopupMenu() { // Margin Container margin_container = memnew(MarginContainer); - margin_container->set_anchors_and_margins_preset(Control::PRESET_WIDE); + margin_container->set_anchors_and_offsets_preset(Control::PRESET_WIDE); add_child(margin_container); margin_container->connect("draw", callable_mp(this, &PopupMenu::_draw_background)); @@ -1458,32 +1701,25 @@ PopupMenu::PopupMenu() { // The control which will display the items control = memnew(Control); control->set_clip_contents(false); - control->set_anchors_and_margins_preset(Control::PRESET_WIDE); + 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); control->connect("draw", callable_mp(this, &PopupMenu::_draw_items)); - connect("window_input", callable_mp(this, &PopupMenu::_gui_input)); - - mouse_over = -1; - submenu_over = -1; - initial_button_mask = 0; - during_grabbed_click = false; - - allow_search = true; - search_time_msec = 0; - search_string = ""; - - set_hide_on_item_selection(true); - set_hide_on_checkable_item_selection(true); - set_hide_on_multistate_item_selection(false); + 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); + + 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); } PopupMenu::~PopupMenu() { diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index e8f82ba869..5c427e43bc 100644 --- a/scene/gui/popup_menu.h +++ b/scene/gui/popup_menu.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -31,10 +31,11 @@ #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 { GDCLASS(PopupMenu, Popup); @@ -43,27 +44,35 @@ class PopupMenu : public Popup { Ref<Texture2D> icon; String text; String xl_text; - bool checked; + Ref<TextLine> text_buf; + Ref<TextLine> accel_text_buf; + + Dictionary opentype_features; + String language; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_AUTO; + + bool checked = false; enum { CHECKABLE_TYPE_NONE, CHECKABLE_TYPE_CHECK_BOX, CHECKABLE_TYPE_RADIO_BUTTON, } checkable_type; - int max_states; - int state; - bool separator; - bool disabled; - int id; + int max_states = 0; + int state = 0; + bool separator = false; + bool disabled = false; + bool dirty = true; + int id = 0; Variant metadata; String submenu; String tooltip; - uint32_t accel; - int _ofs_cache; - int _height_cache; - int h_ofs; + uint32_t accel = 0; + int _ofs_cache = 0; + int _height_cache = 0; + int h_ofs = 0; Ref<Shortcut> shortcut; - bool shortcut_is_global; - bool shortcut_is_disabled; + bool shortcut_is_global = false; + bool shortcut_is_disabled = false; // Returns (0,0) if icon is null. Size2 get_icon_size() const { @@ -71,44 +80,41 @@ class PopupMenu : public Popup { } Item() { - checked = false; + text_buf.instantiate(); + accel_text_buf.instantiate(); checkable_type = CHECKABLE_TYPE_NONE; - separator = false; - max_states = 0; - state = 0; - accel = 0; - disabled = false; - _ofs_cache = 0; - _height_cache = 0; - h_ofs = 0; - shortcut_is_global = false; - shortcut_is_disabled = false; } }; + bool close_allowed = false; + + Timer *minimum_lifetime_timer = nullptr; Timer *submenu_timer; List<Rect2> autohide_areas; Vector<Item> items; - int initial_button_mask; - bool during_grabbed_click; - int mouse_over; - int submenu_over; + int initial_button_mask = 0; + bool during_grabbed_click = false; + int mouse_over = -1; + int submenu_over = -1; Rect2 parent_rect; - String _get_accel_text(int p_item) const; + String _get_accel_text(const Item &p_item) const; int _get_mouse_over(const Point2 &p_over) const; virtual Size2 _get_contents_minimum_size() const override; + int _get_item_height(int p_item) const; int _get_items_total_height() const; void _scroll_to_item(int p_item); - void _gui_input(const Ref<InputEvent> &p_event); - void _activate_submenu(int over); + void _shape_item(int p_item); + + virtual void gui_input(const Ref<InputEvent> &p_event); + void _activate_submenu(int p_over); void _submenu_timeout(); uint64_t popup_time_msec = 0; - bool hide_on_item_selection; - bool hide_on_checkable_item_selection; - bool hide_on_multistate_item_selection; + bool hide_on_item_selection = true; + bool hide_on_checkable_item_selection = true; + bool hide_on_multistate_item_selection = false; Vector2 moved; Array _get_items() const; @@ -119,9 +125,9 @@ class PopupMenu : public Popup { void _ref_shortcut(Ref<Shortcut> p_sc); void _unref_shortcut(Ref<Shortcut> p_sc); - bool allow_search; - uint64_t search_time_msec; - String search_string; + bool allow_search = true; + uint64_t search_time_msec = 0; + String search_string = ""; MarginContainer *margin_container; ScrollContainer *scroll_container; @@ -130,8 +136,10 @@ class PopupMenu : public Popup { void _draw_items(); void _draw_background(); + void _minimum_lifetime_timeout(); + void _close_pressed(); + protected: - friend class MenuButton; void _notification(int p_what); static void _bind_methods(); @@ -155,6 +163,11 @@ public: void add_submenu_item(const String &p_label, const String &p_submenu, int p_id = -1); void set_item_text(int p_idx, const String &p_text); + + void set_item_text_direction(int p_idx, Control::TextDirection p_text_direction); + void set_item_opentype_feature(int p_idx, const String &p_name, int p_value); + void clear_item_opentype_features(int p_idx); + void set_item_language(int p_idx, const String &p_language); void set_item_icon(int p_idx, const Ref<Texture2D> &p_icon); void set_item_checked(int p_idx, bool p_checked); void set_item_id(int p_idx, int p_id); @@ -175,6 +188,9 @@ public: void toggle_item_checked(int p_idx); String get_item_text(int p_idx) const; + Control::TextDirection get_item_text_direction(int p_idx) const; + int get_item_opentype_feature(int p_idx, const String &p_name) const; + String get_item_language(int p_idx) const; int get_item_idx_from_text(const String &text) const; Ref<Texture2D> get_item_icon(int p_idx) const; bool is_item_checked(int p_idx) const; @@ -200,7 +216,7 @@ public: void remove_item(int p_idx); - void add_separator(const String &p_text = String()); + void add_separator(const String &p_text = String(), int p_id = -1); void clear(); diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index 9246f1723d..2cfaaa2fde 100644 --- a/scene/gui/progress_bar.cpp +++ b/scene/gui/progress_bar.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -29,17 +29,21 @@ /*************************************************************************/ #include "progress_bar.h" +#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"); + 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); minimum_size.width = MAX(minimum_size.width, fg->get_minimum_size().width); if (percent_visible) { - minimum_size.height = MAX(minimum_size.height, bg->get_minimum_size().height + font->get_height()); + String txt = "100%"; + TextLine tl = TextLine(txt, font, font_size); + minimum_size.height = MAX(minimum_size.height, bg->get_minimum_size().height + tl.get_size().y); } else { // this is needed, else the progressbar will collapse minimum_size.width = MAX(minimum_size.width, 1); minimum_size.height = MAX(minimum_size.height, 1); @@ -49,22 +53,34 @@ 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"); - 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(); int mp = fg->get_minimum_size().width; int p = r * (get_size().width - mp); if (p > 0) { - draw_style_box(fg, Rect2(Point2(), Size2(p + fg->get_minimum_size().width, get_size().height))); + if (is_layout_rtl()) { + draw_style_box(fg, Rect2(Point2(p, 0), Size2(fg->get_minimum_size().width, get_size().height))); + } else { + draw_style_box(fg, Rect2(Point2(0, 0), Size2(p + fg->get_minimum_size().width, get_size().height))); + } } if (percent_visible) { - String txt = itos(int(get_as_ratio() * 100)) + "%"; - font->draw_halign(get_canvas_item(), Point2(0, font->get_ascent() + (get_size().height - font->get_height()) / 2), HALIGN_CENTER, get_size().width, txt, font_color); + 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(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); + } + tl.draw(get_canvas_item(), text_pos, font_color); } } } @@ -88,5 +104,4 @@ void ProgressBar::_bind_methods() { ProgressBar::ProgressBar() { set_v_size_flags(0); set_step(0.01); - percent_visible = true; } diff --git a/scene/gui/progress_bar.h b/scene/gui/progress_bar.h index f00f993adf..fb6060d932 100644 --- a/scene/gui/progress_bar.h +++ b/scene/gui/progress_bar.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,7 +36,7 @@ class ProgressBar : public Range { GDCLASS(ProgressBar, Range); - bool percent_visible; + bool percent_visible = true; protected: void _notification(int p_what); diff --git a/scene/gui/range.cpp b/scene/gui/range.cpp index bdb9f408f0..92d4261d8d 100644 --- a/scene/gui/range.cpp +++ b/scene/gui/range.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -30,24 +30,20 @@ #include "range.h" -String Range::get_configuration_warning() const { - String warning = Control::get_configuration_warning(); +TypedArray<String> Range::get_configuration_warnings() const { + TypedArray<String> warnings = Node::get_configuration_warnings(); if (shared->exp_ratio && shared->min <= 0) { - if (!warning.empty()) { - warning += "\n\n"; - } - warning += TTR("If \"Exp Edit\" is enabled, \"Min Value\" must be greater than 0."); + warnings.push_back(TTR("If \"Exp Edit\" is enabled, \"Min Value\" must be greater than 0.")); } - return warning; + return warnings; } void Range::_value_changed_notify() { _value_changed(shared->val); - emit_signal("value_changed", shared->val); + emit_signal(SNAME("value_changed"), shared->val); update(); - _change_notify("value"); } void Range::Shared::emit_value_changed() { @@ -61,9 +57,8 @@ void Range::Shared::emit_value_changed() { } void Range::_changed_notify(const char *p_what) { - emit_signal("changed"); + emit_signal(SNAME("changed")); update(); - _change_notify(p_what); } void Range::Shared::emit_changed(const char *p_what) { @@ -108,7 +103,7 @@ void Range::set_min(double p_min) { shared->emit_changed("min"); - update_configuration_warning(); + update_configuration_warnings(); } void Range::set_max(double p_max) { @@ -171,7 +166,10 @@ void Range::set_as_ratio(double p_value) { } double Range::get_as_ratio() const { - ERR_FAIL_COND_V_MSG(Math::is_equal_approx(get_max(), get_min()), 0.0, "Cannot get ratio when minimum and maximum value are equal."); + if (Math::is_equal_approx(get_max(), get_min())) { + // Avoid division by zero. + return 1.0; + } if (shared->exp_ratio && get_min() >= 0) { double exp_min = get_min() == 0 ? 0.0 : Math::log(get_min()) / Math::log((double)2); @@ -180,7 +178,6 @@ double Range::get_as_ratio() const { double v = Math::log(value) / Math::log((double)2); return CLAMP((v - exp_min) / (exp_max - exp_min), 0, 1); - } else { float value = CLAMP(get_value(), shared->min, shared->max); return CLAMP((value - get_min()) / (get_max() - get_min()), 0, 1); @@ -268,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"); @@ -286,7 +283,7 @@ bool Range::is_using_rounded_values() const { void Range::set_exp_ratio(bool p_enable) { shared->exp_ratio = p_enable; - update_configuration_warning(); + update_configuration_warnings(); } bool Range::is_ratio_exp() const { @@ -311,17 +308,7 @@ bool Range::is_lesser_allowed() const { Range::Range() { shared = memnew(Shared); - shared->min = 0; - shared->max = 100; - shared->val = 0; - shared->step = 1; - shared->page = 0; shared->owners.insert(this); - shared->exp_ratio = false; - shared->allow_greater = false; - shared->allow_lesser = false; - - _rounded_values = false; } Range::~Range() { diff --git a/scene/gui/range.h b/scene/gui/range.h index 9ba367aaa4..7a129e88d6 100644 --- a/scene/gui/range.h +++ b/scene/gui/range.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -37,11 +37,14 @@ class Range : public Control { GDCLASS(Range, Control); struct Shared { - double val, min, max; - double step, page; - bool exp_ratio; - bool allow_greater; - bool allow_lesser; + double val = 0.0; + double min = 0.0; + double max = 100.0; + double step = 1.0; + double page = 0.0; + bool exp_ratio = false; + bool allow_greater = false; + bool allow_lesser = false; Set<Range *> owners; void emit_value_changed(); void emit_changed(const char *p_what = ""); @@ -62,7 +65,7 @@ protected: static void _bind_methods(); - bool _rounded_values; + bool _rounded_values = false; public: void set_value(double p_val); @@ -94,7 +97,7 @@ public: void share(Range *p_range); void unshare(); - virtual String get_configuration_warning() const override; + TypedArray<String> get_configuration_warnings() const override; Range(); ~Range(); diff --git a/scene/gui/reference_rect.cpp b/scene/gui/reference_rect.cpp index 773acb2713..6d7f2cfd57 100644 --- a/scene/gui/reference_rect.cpp +++ b/scene/gui/reference_rect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ diff --git a/scene/gui/reference_rect.h b/scene/gui/reference_rect.h index becbbf47c5..7097e83a15 100644 --- a/scene/gui/reference_rect.h +++ b/scene/gui/reference_rect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ diff --git a/scene/gui/rich_text_effect.cpp b/scene/gui/rich_text_effect.cpp index 76ca8abcc7..236d106af8 100644 --- a/scene/gui/rich_text_effect.cpp +++ b/scene/gui/rich_text_effect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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,26 +56,18 @@ 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() { } void CharFXTransform::_bind_methods() { - ClassDB::bind_method(D_METHOD("get_relative_index"), &CharFXTransform::get_relative_index); - ClassDB::bind_method(D_METHOD("set_relative_index", "index"), &CharFXTransform::set_relative_index); - - ClassDB::bind_method(D_METHOD("get_absolute_index"), &CharFXTransform::get_absolute_index); - ClassDB::bind_method(D_METHOD("set_absolute_index", "index"), &CharFXTransform::set_absolute_index); + ClassDB::bind_method(D_METHOD("get_range"), &CharFXTransform::get_range); + ClassDB::bind_method(D_METHOD("set_range", "range"), &CharFXTransform::set_range); ClassDB::bind_method(D_METHOD("get_elapsed_time"), &CharFXTransform::get_elapsed_time); ClassDB::bind_method(D_METHOD("set_elapsed_time", "time"), &CharFXTransform::set_elapsed_time); @@ -76,6 +75,9 @@ void CharFXTransform::_bind_methods() { ClassDB::bind_method(D_METHOD("is_visible"), &CharFXTransform::is_visible); ClassDB::bind_method(D_METHOD("set_visibility", "visibility"), &CharFXTransform::set_visibility); + ClassDB::bind_method(D_METHOD("is_outline"), &CharFXTransform::is_outline); + ClassDB::bind_method(D_METHOD("set_outline", "outline"), &CharFXTransform::set_outline); + ClassDB::bind_method(D_METHOD("get_offset"), &CharFXTransform::get_offset); ClassDB::bind_method(D_METHOD("set_offset", "offset"), &CharFXTransform::set_offset); @@ -85,29 +87,19 @@ void CharFXTransform::_bind_methods() { ClassDB::bind_method(D_METHOD("get_environment"), &CharFXTransform::get_environment); ClassDB::bind_method(D_METHOD("set_environment", "environment"), &CharFXTransform::set_environment); - ClassDB::bind_method(D_METHOD("get_character"), &CharFXTransform::get_character); - ClassDB::bind_method(D_METHOD("set_character", "character"), &CharFXTransform::set_character); + ClassDB::bind_method(D_METHOD("get_glyph_index"), &CharFXTransform::get_glyph_index); + ClassDB::bind_method(D_METHOD("set_glyph_index", "glyph_index"), &CharFXTransform::set_glyph_index); + + ClassDB::bind_method(D_METHOD("get_font"), &CharFXTransform::get_font); + ClassDB::bind_method(D_METHOD("set_font", "font"), &CharFXTransform::set_font); - ADD_PROPERTY(PropertyInfo(Variant::INT, "relative_index"), "set_relative_index", "get_relative_index"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "absolute_index"), "set_absolute_index", "get_absolute_index"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2I, "range"), "set_range", "get_range"); ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "elapsed_time"), "set_elapsed_time", "get_elapsed_time"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visibility", "is_visible"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "outline"), "set_outline", "is_outline"); ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "offset"), "set_offset", "get_offset"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "color"), "set_color", "get_color"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "env"), "set_environment", "get_environment"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "character"), "set_character", "get_character"); -} - -CharFXTransform::CharFXTransform() { - relative_index = 0; - absolute_index = 0; - visibility = true; - offset = Point2(); - color = Color(); - character = 0; - elapsed_time = 0.0f; -} - -CharFXTransform::~CharFXTransform() { - environment.clear(); + ADD_PROPERTY(PropertyInfo(Variant::INT, "glyph_index"), "set_glyph_index", "get_glyph_index"); + ADD_PROPERTY(PropertyInfo(Variant::RID, "font"), "set_font", "get_font"); } diff --git a/scene/gui/rich_text_effect.h b/scene/gui/rich_text_effect.h index e6b9f09e4d..f5506542bb 100644 --- a/scene/gui/rich_text_effect.h +++ b/scene/gui/rich_text_effect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -32,56 +32,65 @@ #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(); public: - uint64_t relative_index; - uint64_t absolute_index; - bool visibility; + Vector2i range; + bool visibility = true; + bool outline = false; Point2 offset; Color color; - char32_t character; - float elapsed_time; + double elapsed_time = 0.0f; Dictionary environment; + uint32_t glyph_index = 0; + RID font; CharFXTransform(); ~CharFXTransform(); - uint64_t get_relative_index() { return relative_index; } - void set_relative_index(uint64_t p_index) { relative_index = p_index; } - uint64_t get_absolute_index() { return absolute_index; } - void set_absolute_index(uint64_t p_index) { absolute_index = p_index; } - float get_elapsed_time() { return elapsed_time; } - void set_elapsed_time(float p_elapsed_time) { elapsed_time = p_elapsed_time; } + Vector2i get_range() { return range; } + void set_range(const Vector2i &p_range) { range = p_range; } + 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_vis) { visibility = p_vis; } + void set_visibility(bool p_visibility) { visibility = p_visibility; } + bool is_outline() { return outline; } + void set_outline(bool p_outline) { outline = p_outline; } Point2 get_offset() { return offset; } void set_offset(Point2 p_offset) { offset = p_offset; } Color get_color() { return color; } void set_color(Color p_color) { color = p_color; } - int get_character() { return (int)character; } - void set_character(int p_char) { character = (char32_t)p_char; } + + 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; }; + Dictionary get_environment() { return environment; } 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 e8acac172c..b7519af4aa 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -45,7 +45,7 @@ #include "editor/editor_scale.h" #endif -RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) { +RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) const { if (p_free) { if (p_item->subitems.size()) { return p_item->subitems.front()->get(); @@ -90,7 +90,7 @@ RichTextLabel::Item *RichTextLabel::_get_next_item(Item *p_item, bool p_free) { return nullptr; } -RichTextLabel::Item *RichTextLabel::_get_prev_item(Item *p_item, bool p_free) { +RichTextLabel::Item *RichTextLabel::_get_prev_item(Item *p_item, bool p_free) const { if (p_free) { if (p_item->subitems.size()) { return p_item->subitems.back()->get(); @@ -136,745 +136,1153 @@ RichTextLabel::Item *RichTextLabel::_get_prev_item(Item *p_item, bool p_free) { } 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()); } -int RichTextLabel::_process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &y, int p_width, int p_line, ProcessMode p_mode, const Ref<Font> &p_base_font, const Color &p_base_color, const Color &p_font_color_shadow, bool p_shadow_as_outline, const Point2 &shadow_ofs, const Point2i &p_click_pos, Item **r_click_item, int *r_click_char, bool *r_outside, int p_char_count) { - ERR_FAIL_INDEX_V((int)p_mode, 3, 0); - - RID ci; - if (r_outside) { - *r_outside = false; +RichTextLabel::Item *RichTextLabel::_get_item_at_pos(RichTextLabel::Item *p_item_from, RichTextLabel::Item *p_item_to, int p_position) { + int offset = 0; + for (Item *it = p_item_from; it && it != p_item_to; it = _get_next_item(it)) { + switch (it->type) { + case ITEM_TEXT: { + ItemText *t = (ItemText *)it; + offset += t->text.length(); + if (offset > p_position) { + return it; + } + } break; + case ITEM_NEWLINE: + case ITEM_IMAGE: + case ITEM_TABLE: { + offset += 1; + } break; + default: + break; + } } - if (p_mode == PROCESS_DRAW) { - ci = get_canvas_item(); + return p_item_from; +} - if (r_click_item) { - *r_click_item = nullptr; - } +String RichTextLabel::_roman(int p_num, bool p_capitalize) const { + if (p_num > 3999) { + return "ERR"; + }; + String s; + if (p_capitalize) { + String M[] = { "", "M", "MM", "MMM" }; + String C[] = { "", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM" }; + String X[] = { "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC" }; + String I[] = { "", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX" }; + s = M[p_num / 1000] + C[(p_num % 1000) / 100] + X[(p_num % 100) / 10] + I[p_num % 10]; + } else { + String M[] = { "", "m", "mm", "mmm" }; + String C[] = { "", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm" }; + String X[] = { "", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc" }; + String I[] = { "", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix" }; + s = M[p_num / 1000] + C[(p_num % 1000) / 100] + X[(p_num % 100) / 10] + I[p_num % 10]; } - Line &l = p_frame->lines.write[p_line]; - Item *it = l.from; + return s; +} - int line_ofs = 0; - int margin = _find_margin(it, p_base_font); - Align align = _find_align(it); - int line = 0; - int spaces = 0; +String RichTextLabel::_letters(int p_num, bool p_capitalize) const { + int64_t n = p_num; + + int chars = 0; + do { + n /= 24; + chars++; + } while (n); + + String s; + s.resize(chars + 1); + char32_t *c = s.ptrw(); + c[chars] = 0; + n = p_num; + do { + int mod = ABS(n % 24); + char a = (p_capitalize ? 'A' : 'a'); + c[--chars] = a + mod - 1; + + n /= 24; + } while (n); + + return s; +} - int height = get_size().y; +void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width) { + ERR_FAIL_COND(p_frame == nullptr); + ERR_FAIL_COND(p_line < 0 || p_line >= p_frame->lines.size()); - if (p_mode != PROCESS_CACHE) { - ERR_FAIL_INDEX_V(line, l.offset_caches.size(), 0); - line_ofs = l.offset_caches[line]; - } + Line &l = p_frame->lines.write[p_line]; + + l.offset.x = _find_margin(l.from, p_base_font, p_base_font_size); + l.text_buf->set_width(p_width - l.offset.x); - if (p_mode == PROCESS_CACHE) { - l.offset_caches.clear(); - l.height_caches.clear(); - l.ascent_caches.clear(); - l.descent_caches.clear(); - l.char_count = 0; - l.minimum_width = 0; - l.maximum_width = 0; + if (tab_size > 0) { // Align inline tabs. + Vector<float> tabs; + tabs.push_back(tab_size * p_base_font->get_char_size(' ', 0, p_base_font_size).width); + l.text_buf->tab_align(tabs); } - int wofs = margin; - int spaces_size = 0; - int align_ofs = 0; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + switch (it->type) { + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + int hseparation = get_theme_constant(SNAME("table_hseparation")); + int vseparation = get_theme_constant(SNAME("table_vseparation")); + int col_count = table->columns.size(); - if (p_mode != PROCESS_CACHE && align != ALIGN_FILL) { - wofs += line_ofs; - } + for (int i = 0; i < col_count; i++) { + table->columns.write[i].width = 0; + } - int begin = margin; + int idx = 0; + 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); + } + idx++; + } - Ref<Font> cfont = _find_font(it); - if (cfont.is_null()) { - cfont = p_base_font; - } + // Compute minimum width for each cell. + const int available_width = p_width - hseparation * (col_count - 1); - //line height should be the font height for the first time, this ensures that an empty line will never have zero height and successive newlines are displayed - int line_height = cfont->get_height(); - int line_ascent = cfont->get_ascent(); - int line_descent = cfont->get_descent(); + // Compute available width and total ratio (for expanders). + int total_ratio = 0; + int remaining_width = available_width; + table->total_width = hseparation; - int backtrack = 0; // for dynamic hidden content. + for (int i = 0; i < col_count; i++) { + remaining_width -= table->columns[i].min_width; + if (table->columns[i].max_width > table->columns[i].min_width) { + table->columns.write[i].expand = true; + } + if (table->columns[i].expand) { + total_ratio += table->columns[i].expand_ratio; + } + } - int nonblank_line_count = 0; //number of nonblank lines as counted during PROCESS_DRAW + // Assign actual widths. + for (int i = 0; i < col_count; i++) { + table->columns.write[i].width = table->columns[i].min_width; + if (table->columns[i].expand && total_ratio > 0) { + table->columns.write[i].width += table->columns[i].expand_ratio * remaining_width / total_ratio; + } + table->total_width += table->columns[i].width + hseparation; + } - Variant meta; + // Resize to max_width if needed and distribute the remaining space. + bool table_need_fit = true; + while (table_need_fit) { + table_need_fit = false; + // Fit slim. + for (int i = 0; i < col_count; i++) { + if (!table->columns[i].expand) { + continue; + } + int dif = table->columns[i].width - table->columns[i].max_width; + if (dif > 0) { + table_need_fit = true; + table->columns.write[i].width = table->columns[i].max_width; + table->total_width -= dif; + total_ratio -= table->columns[i].expand_ratio; + } + } + // Grow. + remaining_width = available_width - table->total_width; + if (remaining_width > 0 && total_ratio > 0) { + for (int i = 0; i < col_count; i++) { + if (table->columns[i].expand) { + int dif = table->columns[i].max_width - table->columns[i].width; + if (dif > 0) { + int slice = table->columns[i].expand_ratio * remaining_width / total_ratio; + int incr = MIN(dif, slice); + table->columns.write[i].width += incr; + table->total_width += incr; + } + } + } + } + } -#define RETURN return nonblank_line_count - -#define NEW_LINE \ - { \ - if (p_mode != PROCESS_CACHE) { \ - line++; \ - backtrack = 0; \ - if (!line_is_blank) { \ - nonblank_line_count++; \ - } \ - line_is_blank = true; \ - if (line < l.offset_caches.size()) \ - line_ofs = l.offset_caches[line]; \ - wofs = margin; \ - if (align != ALIGN_FILL) \ - wofs += line_ofs; \ - } else { \ - int used = wofs - margin; \ - switch (align) { \ - case ALIGN_LEFT: \ - l.offset_caches.push_back(0); \ - break; \ - case ALIGN_CENTER: \ - l.offset_caches.push_back(((p_width - margin) - used) / 2); \ - break; \ - case ALIGN_RIGHT: \ - l.offset_caches.push_back(((p_width - margin) - used)); \ - break; \ - case ALIGN_FILL: \ - l.offset_caches.push_back(line_wrapped ? ((p_width - margin) - used) : 0); \ - break; \ - } \ - l.height_caches.push_back(line_height); \ - l.ascent_caches.push_back(line_ascent); \ - l.descent_caches.push_back(line_descent); \ - l.space_caches.push_back(spaces); \ - } \ - line_wrapped = false; \ - y += line_height + get_theme_constant(SceneStringNames::get_singleton()->line_separation); \ - line_height = 0; \ - line_ascent = 0; \ - line_descent = 0; \ - spaces = 0; \ - spaces_size = 0; \ - wofs = begin; \ - align_ofs = 0; \ - if (p_mode != PROCESS_CACHE) { \ - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; \ - line_ascent = line < l.ascent_caches.size() ? l.ascent_caches[line] : 1; \ - line_descent = line < l.descent_caches.size() ? l.descent_caches[line] : 1; \ - if (align != ALIGN_FILL) { \ - if (line < l.offset_caches.size()) { \ - wofs = l.offset_caches[line]; \ - } \ - } \ - } \ - if (p_mode == PROCESS_POINTER && r_click_item && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh && p_click_pos.x < p_ofs.x + wofs) { \ - if (r_outside) \ - *r_outside = true; \ - *r_click_item = it; \ - *r_click_char = rchar; \ - RETURN; \ - } \ - } - -#define ENSURE_WIDTH(m_width) \ - if (p_mode == PROCESS_CACHE) { \ - l.maximum_width = MAX(l.maximum_width, MIN(p_width, wofs + m_width)); \ - l.minimum_width = MAX(l.minimum_width, m_width); \ - } \ - if (wofs - backtrack + m_width > p_width) { \ - line_wrapped = true; \ - if (p_mode == PROCESS_CACHE) { \ - if (spaces > 0) \ - spaces -= 1; \ - } \ - const bool x_in_range = (p_click_pos.x > p_ofs.x + wofs) && (!p_frame->cell || p_click_pos.x < p_ofs.x + p_width); \ - if (p_mode == PROCESS_POINTER && r_click_item && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh && x_in_range) { \ - if (r_outside) \ - *r_outside = true; \ - *r_click_item = it; \ - *r_click_char = rchar; \ - RETURN; \ - } \ - NEW_LINE \ - } - -#define ADVANCE(m_width) \ - { \ - if (p_mode == PROCESS_POINTER && r_click_item && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh && p_click_pos.x >= p_ofs.x + wofs && p_click_pos.x < p_ofs.x + wofs + m_width) { \ - if (r_outside) \ - *r_outside = false; \ - *r_click_item = it; \ - *r_click_char = rchar; \ - RETURN; \ - } \ - wofs += m_width; \ - } - -#define CHECK_HEIGHT(m_height) \ - if (m_height > line_height) { \ - line_height = m_height; \ - } - -#define YRANGE_VISIBLE(m_top, m_height) \ - (m_height > 0 && ((m_top >= 0 && m_top < height) || ((m_top + m_height - 1) >= 0 && (m_top + m_height - 1) < height))) - - Color selection_fg; - Color selection_bg; - - if (p_mode == PROCESS_DRAW) { - selection_fg = get_theme_color("font_color_selected"); - selection_bg = get_theme_color("selection_color"); - } - - int rchar = 0; - int lh = 0; - bool line_is_blank = true; - bool line_wrapped = false; - int fh = 0; + // Update line width and get total height. + idx = 0; + table->total_height = 0; + table->rows.clear(); - while (it) { - switch (it->type) { - case ITEM_ALIGN: { - ItemAlign *align_it = static_cast<ItemAlign *>(it); + Vector2 offset; + float row_height = 0.0; - align = align_it->align; + for (Item *E : table->subitems) { + ERR_CONTINUE(E->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E); - } break; - case ITEM_INDENT: { - if (it != l.from) { - ItemIndent *indent_it = static_cast<ItemIndent *>(it); - - int indent = indent_it->level * tab_size * cfont->get_char_size(' ').width; - margin += indent; - begin += indent; - wofs += indent; + int column = idx % col_count; + + offset.x += frame->padding.position.x; + float yofs = frame->padding.position.y; + for (int i = 0; i < frame->lines.size(); i++) { + frame->lines.write[i].text_buf->set_width(table->columns[column].width); + table->columns.write[column].width = MAX(table->columns.write[column].width, ceil(frame->lines[i].text_buf->get_size().x)); + + if (i > 0) { + frame->lines.write[i].offset.y = frame->lines[i - 1].offset.y + frame->lines[i - 1].text_buf->get_size().y; + } else { + frame->lines.write[i].offset.y = 0; + } + frame->lines.write[i].offset += Vector2(offset.x, offset.y); + + float h = frame->lines[i].text_buf->get_size().y; + if (frame->min_size_over.y > 0) { + h = MAX(h, frame->min_size_over.y); + } + if (frame->max_size_over.y > 0) { + h = MIN(h, frame->max_size_over.y); + } + yofs += h; + } + yofs += frame->padding.size.y; + offset.x += table->columns[column].width + hseparation + frame->padding.size.x; + + row_height = MAX(yofs, row_height); + if (column == col_count - 1) { + offset.x = 0; + row_height += vseparation; + table->total_height += row_height; + offset.y += row_height; + table->rows.push_back(row_height); + row_height = 0; + } + idx++; } + l.text_buf->resize_object((uint64_t)it, Size2(table->total_width, table->total_height), table->inline_align); + } break; + default: + break; + } + } + + 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; + } else { + l.offset.y = 0; + } +} +void RichTextLabel::_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) { + ERR_FAIL_COND(p_frame == nullptr); + ERR_FAIL_COND(p_line < 0 || p_line >= p_frame->lines.size()); + + Line &l = p_frame->lines.write[p_line]; + + // Clear cache. + l.text_buf->clear(); + l.text_buf->set_flags(TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_TRIM_EDGE_SPACES); + l.char_offset = *r_char_offset; + l.char_count = 0; + + // Add indent. + l.offset.x = _find_margin(l.from, p_base_font, p_base_font_size); + l.text_buf->set_width(p_width - l.offset.x); + l.text_buf->set_align((HAlign)_find_align(l.from)); + l.text_buf->set_direction(_find_direction(l.from)); + + if (tab_size > 0) { // Align inline tabs. + Vector<float> tabs; + tabs.push_back(tab_size * p_base_font->get_char_size(' ', 0, p_base_font_size).width); + l.text_buf->tab_align(tabs); + } + + // Shape current paragraph. + String text; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + if (visible_characters >= 0 && l.char_offset + l.char_count > visible_characters) { + break; + } + switch (it->type) { + case ITEM_DROPCAP: { + // Add dropcap. + const ItemDropcap *dc = (ItemDropcap *)it; + if (dc != nullptr) { + l.text_buf->set_dropcap(dc->text, dc->font, dc->font_size, dc->dropcap_margins); + l.dc_color = dc->color; + l.dc_ol_size = dc->ol_size; + l.dc_ol_color = dc->ol_color; + } else { + l.text_buf->clear_dropcap(); + } + } break; + case ITEM_NEWLINE: { + Ref<Font> font = _find_font(it); + if (font.is_null()) { + font = p_base_font; + } + int font_size = _find_font_size(it); + if (font_size == -1) { + font_size = p_base_font_size; + } + l.text_buf->add_string("\n", font, font_size, Dictionary(), ""); + text += "\n"; + l.char_count += 1; } break; case ITEM_TEXT: { - ItemText *text = static_cast<ItemText *>(it); - + ItemText *t = (ItemText *)it; Ref<Font> font = _find_font(it); if (font.is_null()) { font = p_base_font; } + int font_size = _find_font_size(it); + if (font_size == -1) { + font_size = p_base_font_size; + } + Dictionary font_ftr = _find_font_features(it); + String lang = _find_language(it); + String tx = t->text; + if (visible_characters >= 0 && l.char_offset + l.char_count + tx.length() > visible_characters) { + tx = tx.substr(0, l.char_offset + l.char_count + tx.length() - visible_characters); + } - const char32_t *c = text->text.get_data(); - const char32_t *cf = c; - int ascent = font->get_ascent(); - int descent = font->get_descent(); - - Color color; - Color font_color_shadow; - bool underline = false; - bool strikethrough = false; - ItemFade *fade = nullptr; - int it_char_start = p_char_count; - - Vector<ItemFX *> fx_stack = Vector<ItemFX *>(); - _fetch_item_fx_stack(text, fx_stack); - bool custom_fx_ok = true; - - if (p_mode == PROCESS_DRAW) { - color = _find_color(text, p_base_color); - font_color_shadow = _find_color(text, p_font_color_shadow); - if (_find_underline(text) || (_find_meta(text, &meta) && underline_meta)) { - underline = true; - } else if (_find_strikethrough(text)) { - strikethrough = true; - } + l.text_buf->add_string(tx, font, font_size, font_ftr, lang); + text += tx; + l.char_count += tx.length(); + } break; + case ITEM_IMAGE: { + ItemImage *img = (ItemImage *)it; + l.text_buf->add_object((uint64_t)it, img->image->get_size(), img->inline_align, 1); + text += String::chr(0xfffc); + l.char_count += 1; + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + 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. + for (int i = 0; i < col_count; i++) { + table->columns.write[i].min_width = 0; + table->columns.write[i].max_width = 0; + table->columns.write[i].width = 0; + } + // Compute minimum width for each cell. + const int available_width = p_width - hseparation * (col_count - 1); - Item *fade_item = it; - while (fade_item) { - if (fade_item->type == ITEM_FADE) { - fade = static_cast<ItemFade *>(fade_item); - break; - } - fade_item = fade_item->parent; - } + int idx = 0; + for (Item *E : table->subitems) { + ERR_CONTINUE(E->type != ITEM_FRAME); // Children should all be frames. + ItemFrame *frame = static_cast<ItemFrame *>(E); - } else if (p_mode == PROCESS_CACHE) { - l.char_count += text->text.length(); + int column = idx % col_count; + for (int i = 0; i < frame->lines.size(); i++) { + int char_offset = l.char_offset + l.char_count; + _shape_line(frame, i, p_base_font, p_base_font_size, 1, &char_offset); + int cell_ch = (char_offset - (l.char_offset + l.char_count)); + l.char_count += cell_ch; + t_char_count += cell_ch; + + table->columns.write[column].min_width = MAX(table->columns[column].min_width, ceil(frame->lines[i].text_buf->get_size().x)); + table->columns.write[column].max_width = MAX(table->columns[column].max_width, ceil(frame->lines[i].text_buf->get_non_wraped_size().x)); + } + idx++; } - rchar = 0; - FontDrawer drawer(font, Color(1, 1, 1)); - while (*c) { - int end = 0; - int w = 0; - int fw = 0; + // Compute available width and total ratio (for expanders). + int total_ratio = 0; + int remaining_width = available_width; + table->total_width = hseparation; - lh = 0; + for (int i = 0; i < col_count; i++) { + remaining_width -= table->columns[i].min_width; + if (table->columns[i].max_width > table->columns[i].min_width) { + table->columns.write[i].expand = true; + } + if (table->columns[i].expand) { + total_ratio += table->columns[i].expand_ratio; + } + } - if (p_mode != PROCESS_CACHE) { - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; - line_ascent = line < l.ascent_caches.size() ? l.ascent_caches[line] : 1; - line_descent = line < l.descent_caches.size() ? l.descent_caches[line] : 1; + // Assign actual widths. + for (int i = 0; i < col_count; i++) { + table->columns.write[i].width = table->columns[i].min_width; + if (table->columns[i].expand && total_ratio > 0) { + table->columns.write[i].width += table->columns[i].expand_ratio * remaining_width / total_ratio; } - while (c[end] != 0 && !(end && c[end - 1] == ' ' && c[end] != ' ')) { - int cw = font->get_char_size(c[end], c[end + 1]).width; - if (c[end] == '\t') { - cw = tab_size * font->get_char_size(' ').width; - } + table->total_width += table->columns[i].width + hseparation; + } - if (end > 0 && fw + cw + begin > p_width) { - break; //don't allow lines longer than assigned width + // Resize to max_width if needed and distribute the remaining space. + bool table_need_fit = true; + while (table_need_fit) { + table_need_fit = false; + // Fit slim. + for (int i = 0; i < col_count; i++) { + if (!table->columns[i].expand) { + continue; + } + int dif = table->columns[i].width - table->columns[i].max_width; + if (dif > 0) { + table_need_fit = true; + table->columns.write[i].width = table->columns[i].max_width; + table->total_width -= dif; + total_ratio -= table->columns[i].expand_ratio; } - - fw += cw; - - end++; } - CHECK_HEIGHT(fh); - ENSURE_WIDTH(fw); - - line_ascent = MAX(line_ascent, ascent); - line_descent = MAX(line_descent, descent); - fh = line_ascent + line_descent; - - if (end && c[end - 1] == ' ') { - if (p_mode == PROCESS_CACHE) { - spaces_size += font->get_char_size(' ').width; - } else if (align == ALIGN_FILL) { - int ln = MIN(l.offset_caches.size() - 1, line); - if (l.space_caches[ln]) { - align_ofs = spaces * l.offset_caches[ln] / l.space_caches[ln]; + // Grow. + remaining_width = available_width - table->total_width; + if (remaining_width > 0 && total_ratio > 0) { + for (int i = 0; i < col_count; i++) { + if (table->columns[i].expand) { + int dif = table->columns[i].max_width - table->columns[i].width; + if (dif > 0) { + int slice = table->columns[i].expand_ratio * remaining_width / total_ratio; + int incr = MIN(dif, slice); + table->columns.write[i].width += incr; + table->total_width += incr; + } } } - spaces++; } + } - { - int ofs = 0 - backtrack; + // Update line width and get total height. + idx = 0; + table->total_height = 0; + table->rows.clear(); - for (int i = 0; i < end; i++) { - int pofs = wofs + ofs; + Vector2 offset; + float row_height = 0.0; - if (p_mode == PROCESS_POINTER && r_click_char && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh) { - int cw = font->get_char_size(c[i], c[i + 1]).x; + 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()); - if (c[i] == '\t') { - cw = tab_size * font->get_char_size(' ').width; - } + int column = idx % col_count; - if (p_click_pos.x - cw / 2 > p_ofs.x + align_ofs + pofs) { - rchar = int((&c[i]) - cf); - } + offset.x += frame->padding.position.x; + float yofs = frame->padding.position.y; + for (int i = 0; i < frame->lines.size(); i++) { + frame->lines.write[i].text_buf->set_width(table->columns[column].width); + table->columns.write[column].width = MAX(table->columns.write[column].width, ceil(frame->lines[i].text_buf->get_size().x)); - ofs += cw; - } else if (p_mode == PROCESS_DRAW) { - bool selected = false; - Color fx_color = Color(color); - Point2 fx_offset; - char32_t fx_char = c[i]; - - if (selection.active) { - int cofs = (&c[i]) - cf; - if ((text->index > selection.from->index || (text->index == selection.from->index && cofs >= selection.from_char)) && (text->index < selection.to->index || (text->index == selection.to->index && cofs <= selection.to_char))) { - selected = true; - } - } + if (i > 0) { + frame->lines.write[i].offset.y = frame->lines[i - 1].offset.y + frame->lines[i - 1].text_buf->get_size().y; + } else { + frame->lines.write[i].offset.y = 0; + } + frame->lines.write[i].offset += Vector2(offset.x, offset.y); - int cw = 0; - int c_item_offset = p_char_count - it_char_start; + float h = frame->lines[i].text_buf->get_size().y; + if (frame->min_size_over.y > 0) { + h = MAX(h, frame->min_size_over.y); + } + if (frame->max_size_over.y > 0) { + h = MIN(h, frame->max_size_over.y); + } + yofs += h; + } + yofs += frame->padding.size.y; + offset.x += table->columns[column].width + hseparation + frame->padding.size.x; - float faded_visibility = 1.0f; - if (fade) { - if (c_item_offset >= fade->starting_index) { - faded_visibility -= (float)(c_item_offset - fade->starting_index) / (float)fade->length; - faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; - } - fx_color.a = faded_visibility; - } + row_height = MAX(yofs, row_height); + // Add row height after last column of the row or last cell of the table. + if (column == col_count - 1 || E->next() == nullptr) { + offset.x = 0; + row_height += vseparation; + table->total_height += row_height; + offset.y += row_height; + table->rows.push_back(row_height); + row_height = 0; + } + idx++; + } - bool visible = visible_characters < 0 || ((p_char_count < visible_characters && YRANGE_VISIBLE(y + lh - line_descent - line_ascent, line_ascent + line_descent)) && - faded_visibility > 0.0f); + l.text_buf->add_object((uint64_t)it, Size2(table->total_width, table->total_height), table->inline_align, t_char_count); + text += String::chr(0xfffc).repeat(t_char_count); + } break; + default: + break; + } + } - const bool previously_visible = visible; + //Apply BiDi override. + l.text_buf->set_bidi_override(structured_text_parser(_find_stt(l.from), st_args, text)); - for (int j = 0; j < fx_stack.size(); j++) { - ItemFX *item_fx = fx_stack[j]; + *r_char_offset = l.char_offset + l.char_count; - if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { - ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); + 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; + } else { + l.offset.y = 0; + } +} - Ref<CharFXTransform> charfx = item_custom->char_fx_transform; - Ref<RichTextEffect> custom_effect = item_custom->custom_effect; +int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, const Color &p_base_color, int p_outline_size, const Color &p_outline_color, const Color &p_font_shadow_color, bool p_shadow_as_outline, const Point2 &p_shadow_ofs) { + Vector2 off; - if (!custom_effect.is_null()) { - charfx->elapsed_time = item_custom->elapsed_time; - charfx->relative_index = c_item_offset; - charfx->absolute_index = p_char_count; - charfx->visibility = visible; - charfx->offset = fx_offset; - charfx->color = fx_color; - charfx->character = fx_char; + ERR_FAIL_COND_V(p_frame == nullptr, 0); + ERR_FAIL_COND_V(p_line < 0 || p_line >= p_frame->lines.size(), 0); - bool effect_status = custom_effect->_process_effect_impl(charfx); - custom_fx_ok = effect_status; + Line &l = p_frame->lines.write[p_line]; - fx_offset += charfx->offset; - fx_color = charfx->color; - visible &= charfx->visibility; - fx_char = charfx->character; - } - } else if (item_fx->type == ITEM_SHAKE) { - ItemShake *item_shake = static_cast<ItemShake *>(item_fx); - - uint64_t char_current_rand = item_shake->offset_random(c_item_offset); - uint64_t char_previous_rand = item_shake->offset_previous_random(c_item_offset); - uint64_t max_rand = 2147483647; - double current_offset = Math::range_lerp(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); - double previous_offset = Math::range_lerp(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); - double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); - n_time = (n_time > 1.0) ? 1.0 : n_time; - fx_offset += Point2(Math::lerp(Math::sin(previous_offset), - Math::sin(current_offset), - n_time), - Math::lerp(Math::cos(previous_offset), - Math::cos(current_offset), - n_time)) * - (float)item_shake->strength / 10.0f; - } else if (item_fx->type == ITEM_WAVE) { - ItemWave *item_wave = static_cast<ItemWave *>(item_fx); - - double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + pofs) / 50)) * (item_wave->amplitude / 10.0f); - fx_offset += Point2(0, 1) * value; - } else if (item_fx->type == ITEM_TORNADO) { - ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); - - double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + pofs) / 50)) * (item_tornado->radius); - double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + pofs) / 50)) * (item_tornado->radius); - fx_offset += Point2(torn_x, torn_y); - } else if (item_fx->type == ITEM_RAINBOW) { - ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); - - fx_color = fx_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + pofs) / 50)), - item_rainbow->saturation, - item_rainbow->value, - fx_color.a); - } - } + Item *it_from = l.from; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + Variant meta; - if (visible) { - line_is_blank = false; - w += font->get_char_size(c[i], c[i + 1]).x; - } + if (it_from == nullptr) { + return 0; + } - if (c[i] == '\t') { - visible = false; - } + RID ci = get_canvas_item(); + bool rtl = (l.text_buf->get_direction() == TextServer::DIRECTION_RTL); + bool lrtl = is_layout_rtl(); - if (visible) { - if (selected) { - cw = font->get_char_size(fx_char, c[i + 1]).x; - draw_rect(Rect2(p_ofs.x + pofs, p_ofs.y + y, cw, lh), selection_bg); - } + Vector<int> list_index; + Vector<ItemList *> list_items; + _find_list(l.from, list_index, list_items); - if (p_font_color_shadow.a > 0) { - float x_ofs_shadow = align_ofs + pofs; - float y_ofs_shadow = y + lh - line_descent; - font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + shadow_ofs + fx_offset, fx_char, c[i + 1], p_font_color_shadow); + String prefix; + for (int i = 0; i < list_index.size(); i++) { + if (rtl) { + prefix = prefix + "."; + } else { + prefix = "." + prefix; + } + String segment; + if (list_items[i]->list_type == LIST_DOTS) { + static const char32_t _prefix[2] = { 0x25CF, 0 }; + prefix = _prefix; + break; + } else if (list_items[i]->list_type == LIST_NUMBERS) { + segment = TS->format_number(itos(list_index[i]), _find_language(l.from)); + } else if (list_items[i]->list_type == LIST_LETTERS) { + segment = _letters(list_index[i], list_items[i]->capitalize); + } else if (list_items[i]->list_type == LIST_ROMAN) { + segment = _roman(list_index[i], list_items[i]->capitalize); + } + if (rtl) { + prefix = prefix + segment; + } else { + prefix = segment + prefix; + } + } + if (prefix != "") { + Ref<Font> font = _find_font(l.from); + if (font.is_null()) { + 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(SNAME("normal_font_size")); + } + if (rtl) { + float offx = 0.0f; + if (!lrtl && p_frame == main) { // Skip Scrollbar. + offx -= scroll_w; + } + font->draw_string(ci, p_ofs + Vector2(p_width - l.offset.x + offx, l.text_buf->get_line_ascent(0)), " " + prefix, HALIGN_LEFT, l.offset.x, font_size, _find_color(l.from, p_base_color)); + } else { + float offx = 0.0f; + if (lrtl && p_frame == main) { // Skip Scrollbar. + offx += scroll_w; + } + font->draw_string(ci, p_ofs + Vector2(offx, l.text_buf->get_line_ascent(0)), prefix + " ", HALIGN_RIGHT, l.offset.x, font_size, _find_color(l.from, p_base_color)); + } + } - if (p_shadow_as_outline) { - font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + Vector2(-shadow_ofs.x, shadow_ofs.y) + fx_offset, fx_char, c[i + 1], p_font_color_shadow); - font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + Vector2(shadow_ofs.x, -shadow_ofs.y) + fx_offset, fx_char, c[i + 1], p_font_color_shadow); - font->draw_char(ci, Point2(x_ofs_shadow, y_ofs_shadow) + Vector2(-shadow_ofs.x, -shadow_ofs.y) + fx_offset, fx_char, c[i + 1], p_font_color_shadow); - } - } + // Draw dropcap. + int dc_lines = l.text_buf->get_dropcap_lines(); + float h_off = l.text_buf->get_dropcap_size().x; + if (l.dc_ol_size > 0) { + l.text_buf->draw_dropcap_outline(ci, p_ofs + ((rtl) ? Vector2() : Vector2(l.offset.x, 0)), l.dc_ol_size, l.dc_ol_color); + } + l.text_buf->draw_dropcap(ci, p_ofs + ((rtl) ? Vector2() : Vector2(l.offset.x, 0)), l.dc_color); + + int line_count = 0; + Size2 ctrl_size = get_size(); + // Draw text. + for (int line = 0; line < l.text_buf->get_line_count(); line++) { + RID rid = l.text_buf->get_line_rid(line); + if (p_ofs.y + off.y >= ctrl_size.height) { + break; + } + if (p_ofs.y + off.y + TS->shaped_text_get_size(rid).y <= 0) { + off.y += TS->shaped_text_get_size(rid).y; + continue; + } - if (selected) { - drawer.draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent), fx_char, c[i + 1], override_selected_font_color ? selection_fg : fx_color); - } else { - cw = drawer.draw_char(ci, p_ofs + Point2(align_ofs + pofs, y + lh - line_descent) + fx_offset, fx_char, c[i + 1], fx_color); - } - } else if (previously_visible && c[i] != '\t') { - backtrack += font->get_char_size(fx_char, c[i + 1]).x; - } + float width = l.text_buf->get_width(); + float length = TS->shaped_text_get_width(rid); + + // Draw line. + line_count++; + + if (rtl) { + off.x = p_width - l.offset.x - width; + if (!lrtl && p_frame == main) { // Skip Scrollbar. + off.x -= scroll_w; + } + } else { + off.x = l.offset.x; + if (lrtl && p_frame == main) { // Skip Scrollbar. + off.x += scroll_w; + } + } - p_char_count++; - if (c[i] == '\t') { - cw = tab_size * font->get_char_size(' ').width; - backtrack = MAX(0, backtrack - cw); + // Draw text. + switch (l.text_buf->get_align()) { + case HALIGN_FILL: + case HALIGN_LEFT: { + if (rtl) { + off.x += width - length; + } + } break; + case HALIGN_CENTER: { + off.x += Math::floor((width - length) / 2.0); + } break; + case HALIGN_RIGHT: { + if (!rtl) { + off.x += width - length; + } + } break; + } + + if (line <= dc_lines) { + if (rtl) { + off.x -= h_off; + } else { + off.x += h_off; + } + } + + //draw_rect(Rect2(p_ofs + off, TS->shaped_text_get_size(rid)), Color(1,0,0), false, 2); //DEBUG_RECTS + + off.y += TS->shaped_text_get_ascent(rid) + l.text_buf->get_spacing_top(); + // Draw inlined objects. + Array objects = TS->shaped_text_get_objects(rid); + for (int i = 0; i < objects.size(); i++) { + Item *it = (Item *)(uint64_t)objects[i]; + if (it != nullptr) { + Rect2 rect = TS->shaped_text_get_object_rect(rid, objects[i]); + //draw_rect(rect, Color(1,0,0), false, 2); //DEBUG_RECTS + switch (it->type) { + case ITEM_IMAGE: { + ItemImage *img = static_cast<ItemImage *>(it); + img->image->draw_rect(ci, Rect2(p_ofs + rect.position + off, rect.size), false, img->color); + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + 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 (Item *E : table->subitems) { + ItemFrame *frame = static_cast<ItemFrame *>(E); + + int col = idx % col_count; + int row = idx / col_count; + + if (frame->lines.size() != 0 && row < row_count) { + Vector2 coff = frame->lines[0].offset; + if (rtl) { + coff.x = rect.size.width - table->columns[col].width - coff.x; } + if (row % 2 == 0) { + draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position, Size2(table->columns[col].width + hseparation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), (frame->odd_row_bg != Color(0, 0, 0, 0) ? frame->odd_row_bg : odd_row_bg), true); + } else { + draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position, Size2(table->columns[col].width + hseparation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), (frame->even_row_bg != Color(0, 0, 0, 0) ? frame->even_row_bg : even_row_bg), true); + } + draw_rect(Rect2(p_ofs + rect.position + off + coff - frame->padding.position, Size2(table->columns[col].width + hseparation + frame->padding.position.x + frame->padding.size.x, table->rows[row])), (frame->border != Color(0, 0, 0, 0) ? frame->border : border), false); + } - ofs += cw; + for (int j = 0; j < frame->lines.size(); j++) { + _draw_line(frame, j, p_ofs + rect.position + off + Vector2(0, frame->lines[j].offset.y), rect.size.x, p_base_color, p_outline_size, p_outline_color, p_font_shadow_color, p_shadow_as_outline, p_shadow_ofs); } + idx++; } + } break; + default: + break; + } + } + } - if (underline) { - Color uc = color; - uc.a *= 0.5; - int uy = y + lh - line_descent + font->get_underline_position(); - float underline_width = font->get_underline_thickness(); -#ifdef TOOLS_ENABLED - underline_width *= EDSCALE; -#endif - RS::get_singleton()->canvas_item_add_line(ci, p_ofs + Point2(align_ofs + wofs, uy), p_ofs + Point2(align_ofs + wofs + w, uy), uc, underline_width); - } else if (strikethrough) { - Color uc = color; - uc.a *= 0.5; - int uy = y + lh - (line_ascent + line_descent) / 2; - float strikethrough_width = font->get_underline_thickness(); -#ifdef TOOLS_ENABLED - strikethrough_width *= EDSCALE; -#endif - RS::get_singleton()->canvas_item_add_line(ci, p_ofs + Point2(align_ofs + wofs, uy), p_ofs + Point2(align_ofs + wofs + w, uy), uc, strikethrough_width); - } - } + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(rid); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); + + Vector2 gloff = off; + // Draw oulines and shadow. + for (int i = 0; i < gl_size; i++) { + Item *it = _get_item_at_pos(it_from, it_to, glyphs[i].start); + int size = _find_outline_size(it, p_outline_size); + Color font_color = _find_outline_color(it, p_outline_color); + if (size <= 0) { + gloff.x += glyphs[i].advance; + continue; + } - ADVANCE(fw); - CHECK_HEIGHT(fh); //must be done somewhere - c = &c[end]; + // Get FX. + ItemFade *fade = nullptr; + Item *fade_item = it; + while (fade_item) { + if (fade_item->type == ITEM_FADE) { + fade = static_cast<ItemFade *>(fade_item); + break; } + fade_item = fade_item->parent; + } - } break; - case ITEM_IMAGE: { - lh = 0; - if (p_mode != PROCESS_CACHE) { - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; - } else { - l.char_count += 1; //images count as chars too - } + Vector<ItemFX *> fx_stack; + _fetch_item_fx_stack(it, fx_stack); + bool custom_fx_ok = true; - ItemImage *img = static_cast<ItemImage *>(it); + Point2 fx_offset = Vector2(glyphs[i].x_off, glyphs[i].y_off); + RID frid = glyphs[i].font_rid; + uint32_t gl = glyphs[i].index; - Ref<Font> font = _find_font(it); - if (font.is_null()) { - font = p_base_font; + //Apply fx. + float faded_visibility = 1.0f; + if (fade) { + if (glyphs[i].start >= fade->starting_index) { + faded_visibility -= (float)(glyphs[i].start - fade->starting_index) / (float)fade->length; + faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; } + font_color.a = faded_visibility; + } - if (p_mode == PROCESS_POINTER && r_click_char) { - *r_click_char = 0; + bool visible = (font_color.a != 0); + + for (int j = 0; j < fx_stack.size(); j++) { + ItemFX *item_fx = fx_stack[j]; + if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { + ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); + + Ref<CharFXTransform> charfx = item_custom->char_fx_transform; + Ref<RichTextEffect> custom_effect = item_custom->custom_effect; + + if (!custom_effect.is_null()) { + charfx->elapsed_time = item_custom->elapsed_time; + charfx->range = Vector2i(l.char_offset + glyphs[i].start, l.char_offset + glyphs[i].end); + charfx->visibility = visible; + charfx->outline = true; + charfx->font = frid; + charfx->glyph_index = gl; + charfx->offset = fx_offset; + charfx->color = font_color; + + bool effect_status = custom_effect->_process_effect_impl(charfx); + custom_fx_ok = effect_status; + + fx_offset += charfx->offset; + font_color = charfx->color; + frid = charfx->font; + gl = charfx->glyph_index; + visible &= charfx->visibility; + } + } else if (item_fx->type == ITEM_SHAKE) { + ItemShake *item_shake = static_cast<ItemShake *>(item_fx); + + uint64_t char_current_rand = item_shake->offset_random(glyphs[i].start); + uint64_t char_previous_rand = item_shake->offset_previous_random(glyphs[i].start); + uint64_t max_rand = 2147483647; + double current_offset = Math::range_lerp(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double previous_offset = Math::range_lerp(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); + n_time = (n_time > 1.0) ? 1.0 : n_time; + fx_offset += Point2(Math::lerp(Math::sin(previous_offset), Math::sin(current_offset), n_time), Math::lerp(Math::cos(previous_offset), Math::cos(current_offset), n_time)) * (float)item_shake->strength / 10.0f; + } else if (item_fx->type == ITEM_WAVE) { + ItemWave *item_wave = static_cast<ItemWave *>(item_fx); + + double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_wave->amplitude / 10.0f); + fx_offset += Point2(0, 1) * value; + } else if (item_fx->type == ITEM_TORNADO) { + ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); + + double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + gloff.x) / 50)) * (item_tornado->radius); + double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + gloff.x) / 50)) * (item_tornado->radius); + fx_offset += Point2(torn_x, torn_y); + } else if (item_fx->type == ITEM_RAINBOW) { + ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); + + font_color = font_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + gloff.x) / 50)), item_rainbow->saturation, item_rainbow->value, font_color.a); } + } - ENSURE_WIDTH(img->size.width); + Point2 shadow_ofs(get_theme_constant(SNAME("shadow_offset_x")), get_theme_constant(SNAME("shadow_offset_y"))); - bool visible = visible_characters < 0 || (p_char_count < visible_characters && YRANGE_VISIBLE(y + lh - font->get_descent() - img->size.height, img->size.height)); + // Draw glyph outlines. + for (int j = 0; j < glyphs[i].repeat; j++) { if (visible) { - line_is_blank = false; + if (frid != RID()) { + if (p_shadow_as_outline) { + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff + Vector2(-shadow_ofs.x, shadow_ofs.y), gl, p_font_shadow_color); + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff + Vector2(shadow_ofs.x, -shadow_ofs.y), gl, p_font_shadow_color); + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff + Vector2(-shadow_ofs.x, -shadow_ofs.y), gl, p_font_shadow_color); + } + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff, gl, font_color); + } } + gloff.x += glyphs[i].advance; + } + } - if (p_mode == PROCESS_DRAW && visible) { - img->image->draw_rect(ci, Rect2(p_ofs + Point2(align_ofs + wofs, y + lh - font->get_descent() - img->size.height), img->size), false, img->color); - } - p_char_count++; + 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); - ADVANCE(img->size.width); - CHECK_HEIGHT((img->size.height + font->get_descent())); + // Draw main text. + Color selection_fg = get_theme_color(SNAME("font_selected_color")); + Color selection_bg = get_theme_color(SNAME("selection_color")); - } break; - case ITEM_NEWLINE: { - lh = 0; - - if (p_mode != PROCESS_CACHE) { - lh = line < l.height_caches.size() ? l.height_caches[line] : 1; - line_is_blank = true; - } + int sel_start = -1; + int sel_end = -1; - } break; - case ITEM_TABLE: { - lh = 0; - ItemTable *table = static_cast<ItemTable *>(it); - int hseparation = get_theme_constant("table_hseparation"); - int vseparation = get_theme_constant("table_vseparation"); - Color ccolor = _find_color(table, p_base_color); - Vector2 draw_ofs = Point2(wofs, y); - Color font_color_shadow = get_theme_color("font_color_shadow"); - bool use_outline = get_theme_constant("shadow_as_outline"); - Point2 shadow_ofs2(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); - - if (p_mode == PROCESS_CACHE) { - int idx = 0; - //set minimums to zero - for (int i = 0; i < table->columns.size(); i++) { - table->columns.write[i].min_width = 0; - table->columns.write[i].max_width = 0; - table->columns.write[i].width = 0; - } - //compute minimum width for each cell - const int available_width = p_width - hseparation * (table->columns.size() - 1) - wofs; + if (selection.active && (selection.from_frame->lines[selection.from_line].char_offset + selection.from_char) <= (l.char_offset + TS->shaped_text_get_range(rid).y) && (selection.to_frame->lines[selection.to_line].char_offset + selection.to_char) >= (l.char_offset + TS->shaped_text_get_range(rid).x)) { + sel_start = MAX(TS->shaped_text_get_range(rid).x, (selection.from_frame->lines[selection.from_line].char_offset + selection.from_char) - l.char_offset); + sel_end = MIN(TS->shaped_text_get_range(rid).y, (selection.to_frame->lines[selection.to_line].char_offset + selection.to_char) - l.char_offset); - 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()); + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, sel_start, sel_end); + for (int i = 0; i < sel.size(); i++) { + Rect2 rect = Rect2(sel[i].x + p_ofs.x + off.x, p_ofs.y + off.y - TS->shaped_text_get_ascent(rid), sel[i].y - sel[i].x, TS->shaped_text_get_size(rid).y); + RenderingServer::get_singleton()->canvas_item_add_rect(ci, rect, selection_bg); + } + } - int column = idx % table->columns.size(); + for (int i = 0; i < gl_size; i++) { + bool selected = selection.active && (sel_start != -1) && (glyphs[i].start >= sel_start) && (glyphs[i].end <= sel_end); + Item *it = _get_item_at_pos(it_from, it_to, glyphs[i].start); + Color font_color = _find_color(it, p_base_color); + if (_find_underline(it) || (_find_meta(it, &meta) && underline_meta)) { + Color uc = font_color; + uc.a *= 0.5; + float y_off = TS->shaped_text_get_underline_position(rid); + float underline_width = TS->shaped_text_get_underline_thickness(rid); +#ifdef TOOLS_ENABLED + underline_width *= EDSCALE; +#endif + draw_line(p_ofs + Vector2(off.x, off.y + y_off), p_ofs + Vector2(off.x + glyphs[i].advance * glyphs[i].repeat, off.y + y_off), uc, underline_width); + } else if (_find_strikethrough(it)) { + Color uc = font_color; + uc.a *= 0.5; + float y_off = -TS->shaped_text_get_ascent(rid) + TS->shaped_text_get_size(rid).y / 2; + float underline_width = TS->shaped_text_get_underline_thickness(rid); +#ifdef TOOLS_ENABLED + underline_width *= EDSCALE; +#endif + draw_line(p_ofs + Vector2(off.x, off.y + y_off), p_ofs + Vector2(off.x + glyphs[i].advance * glyphs[i].repeat, off.y + y_off), uc, underline_width); + } - int ly = 0; + // Get FX. + ItemFade *fade = nullptr; + Item *fade_item = it; + while (fade_item) { + if (fade_item->type == ITEM_FADE) { + fade = static_cast<ItemFade *>(fade_item); + break; + } + fade_item = fade_item->parent; + } - for (int i = 0; i < frame->lines.size(); i++) { - _process_line(frame, Point2(), ly, available_width, i, PROCESS_CACHE, cfont, Color(), font_color_shadow, use_outline, shadow_ofs2); - table->columns.write[column].min_width = MAX(table->columns[column].min_width, frame->lines[i].minimum_width); - table->columns.write[column].max_width = MAX(table->columns[column].max_width, frame->lines[i].maximum_width); - } - idx++; - } + Vector<ItemFX *> fx_stack; + _fetch_item_fx_stack(it, fx_stack); + bool custom_fx_ok = true; - //compute available width and total ratio (for expanders) + Point2 fx_offset = Vector2(glyphs[i].x_off, glyphs[i].y_off); + RID frid = glyphs[i].font_rid; + uint32_t gl = glyphs[i].index; - int total_ratio = 0; - int remaining_width = available_width; - table->total_width = hseparation; + //Apply fx. + float faded_visibility = 1.0f; + if (fade) { + if (glyphs[i].start >= fade->starting_index) { + faded_visibility -= (float)(glyphs[i].start - fade->starting_index) / (float)fade->length; + faded_visibility = faded_visibility < 0.0f ? 0.0f : faded_visibility; + } + font_color.a = faded_visibility; + } - for (int i = 0; i < table->columns.size(); i++) { - remaining_width -= table->columns[i].min_width; - if (table->columns[i].max_width > table->columns[i].min_width) { - table->columns.write[i].expand = true; - } - if (table->columns[i].expand) { - total_ratio += table->columns[i].expand_ratio; - } + bool visible = (font_color.a != 0); + + for (int j = 0; j < fx_stack.size(); j++) { + ItemFX *item_fx = fx_stack[j]; + if (item_fx->type == ITEM_CUSTOMFX && custom_fx_ok) { + ItemCustomFX *item_custom = static_cast<ItemCustomFX *>(item_fx); + + Ref<CharFXTransform> charfx = item_custom->char_fx_transform; + Ref<RichTextEffect> custom_effect = item_custom->custom_effect; + + if (!custom_effect.is_null()) { + charfx->elapsed_time = item_custom->elapsed_time; + charfx->range = Vector2i(l.char_offset + glyphs[i].start, l.char_offset + glyphs[i].end); + charfx->visibility = visible; + charfx->outline = false; + charfx->font = frid; + charfx->glyph_index = gl; + charfx->offset = fx_offset; + charfx->color = font_color; + + bool effect_status = custom_effect->_process_effect_impl(charfx); + custom_fx_ok = effect_status; + + fx_offset += charfx->offset; + font_color = charfx->color; + frid = charfx->font; + gl = charfx->glyph_index; + visible &= charfx->visibility; } + } else if (item_fx->type == ITEM_SHAKE) { + ItemShake *item_shake = static_cast<ItemShake *>(item_fx); + + uint64_t char_current_rand = item_shake->offset_random(glyphs[i].start); + uint64_t char_previous_rand = item_shake->offset_previous_random(glyphs[i].start); + uint64_t max_rand = 2147483647; + double current_offset = Math::range_lerp(char_current_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double previous_offset = Math::range_lerp(char_previous_rand % max_rand, 0, max_rand, 0.0f, 2.f * (float)Math_PI); + double n_time = (double)(item_shake->elapsed_time / (0.5f / item_shake->rate)); + n_time = (n_time > 1.0) ? 1.0 : n_time; + fx_offset += Point2(Math::lerp(Math::sin(previous_offset), Math::sin(current_offset), n_time), Math::lerp(Math::cos(previous_offset), Math::cos(current_offset), n_time)) * (float)item_shake->strength / 10.0f; + } else if (item_fx->type == ITEM_WAVE) { + ItemWave *item_wave = static_cast<ItemWave *>(item_fx); + + double value = Math::sin(item_wave->frequency * item_wave->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_wave->amplitude / 10.0f); + fx_offset += Point2(0, 1) * value; + } else if (item_fx->type == ITEM_TORNADO) { + ItemTornado *item_tornado = static_cast<ItemTornado *>(item_fx); + + double torn_x = Math::sin(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_tornado->radius); + double torn_y = Math::cos(item_tornado->frequency * item_tornado->elapsed_time + ((p_ofs.x + off.x) / 50)) * (item_tornado->radius); + fx_offset += Point2(torn_x, torn_y); + } else if (item_fx->type == ITEM_RAINBOW) { + ItemRainbow *item_rainbow = static_cast<ItemRainbow *>(item_fx); + + font_color = font_color.from_hsv(item_rainbow->frequency * (item_rainbow->elapsed_time + ((p_ofs.x + off.x) / 50)), item_rainbow->saturation, item_rainbow->value, font_color.a); + } + } - //assign actual widths - for (int i = 0; i < table->columns.size(); i++) { - table->columns.write[i].width = table->columns[i].min_width; - if (table->columns[i].expand && total_ratio > 0) { - table->columns.write[i].width += table->columns[i].expand_ratio * remaining_width / total_ratio; - } - table->total_width += table->columns[i].width + hseparation; - } + if (selected) { + font_color = override_selected_font_color ? selection_fg : font_color; + } - //resize to max_width if needed and distribute the remaining space - bool table_need_fit = true; - while (table_need_fit) { - table_need_fit = false; - //fit slim - for (int i = 0; i < table->columns.size(); i++) { - if (!table->columns[i].expand) { - continue; - } - int dif = table->columns[i].width - table->columns[i].max_width; - if (dif > 0) { - table_need_fit = true; - table->columns.write[i].width = table->columns[i].max_width; - table->total_width -= dif; - total_ratio -= table->columns[i].expand_ratio; - } - } - //grow - remaining_width = available_width - table->total_width; - if (remaining_width > 0 && total_ratio > 0) { - for (int i = 0; i < table->columns.size(); i++) { - if (table->columns[i].expand) { - int dif = table->columns[i].max_width - table->columns[i].width; - if (dif > 0) { - int slice = table->columns[i].expand_ratio * remaining_width / total_ratio; - int incr = MIN(dif, slice); - table->columns.write[i].width += incr; - table->total_width += incr; - } - } - } - } + // Draw glyphs. + for (int j = 0; j < glyphs[i].repeat; j++) { + if (visible) { + if (frid != RID()) { + TS->font_draw_glyph(frid, ci, glyphs[i].font_size, p_ofs + fx_offset + off, gl, selected ? selection_fg : font_color); + } else if ((glyphs[i].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + TS->draw_hex_code_box(ci, glyphs[i].font_size, p_ofs + fx_offset + off, gl, selected ? selection_fg : font_color); } + } + 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); - //compute caches properly again with the right width - 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()); + off.y += TS->shaped_text_get_descent(rid) + l.text_buf->get_spacing_bottom(); + } - int column = idx % table->columns.size(); + return line_count; +} - for (int i = 0; i < frame->lines.size(); i++) { - int ly = 0; - _process_line(frame, Point2(), ly, table->columns[column].width, i, PROCESS_CACHE, cfont, Color(), font_color_shadow, use_outline, shadow_ofs2); - frame->lines.write[i].height_cache = ly; //actual height - frame->lines.write[i].height_accum_cache = ly; //actual height - } - idx++; - } - } +void RichTextLabel::_find_click(ItemFrame *p_frame, const Point2i &p_click, ItemFrame **r_click_frame, int *r_click_line, Item **r_click_item, int *r_click_char, bool *r_outside) { + if (r_click_item) { + *r_click_item = nullptr; + } + if (r_click_char != nullptr) { + *r_click_char = 0; + } + if (r_outside != nullptr) { + *r_outside = true; + } - Point2 offset(align_ofs + hseparation, vseparation); + Size2 size = get_size(); + Rect2 text_rect = _get_text_rect(); - int row_height = 0; - //draw using computed caches - 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()); + int vofs = vscroll->get_value(); - int column = idx % table->columns.size(); + // Search for the first line. + int from_line = 0; - int ly = 0; - int yofs = 0; + //TODO, change to binary search ? + while (from_line < main->lines.size()) { + if (main->lines[from_line].offset.y + main->lines[from_line].text_buf->get_size().y >= vofs) { + break; + } + from_line++; + } - int lines_h = frame->lines[frame->lines.size() - 1].height_accum_cache - (frame->lines[0].height_accum_cache - frame->lines[0].height_cache); - int lines_ofs = p_ofs.y + offset.y + draw_ofs.y; + if (from_line >= main->lines.size()) { + return; + } - bool visible = lines_ofs < get_size().height && lines_ofs + lines_h >= 0; - if (visible) { - line_is_blank = false; - } + 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 + 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; + } + return; + } + from_line++; + } +} - for (int i = 0; i < frame->lines.size(); i++) { - if (visible) { - if (p_mode == PROCESS_DRAW) { - nonblank_line_count += _process_line(frame, p_ofs + offset + draw_ofs + Vector2(0, yofs), ly, table->columns[column].width, i, PROCESS_DRAW, cfont, ccolor, font_color_shadow, use_outline, shadow_ofs2); - } else if (p_mode == PROCESS_POINTER) { - _process_line(frame, p_ofs + offset + draw_ofs + Vector2(0, yofs), ly, table->columns[column].width, i, PROCESS_POINTER, cfont, ccolor, font_color_shadow, use_outline, shadow_ofs2, p_click_pos, r_click_item, r_click_char, r_outside); - if (r_click_item && *r_click_item) { - RETURN; // exit early - } - } - } +float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, const Point2i &p_click, ItemFrame **r_click_frame, int *r_click_line, Item **r_click_item, int *r_click_char) { + Vector2 off; - yofs += frame->lines[i].height_cache; - if (p_mode == PROCESS_CACHE) { - frame->lines.write[i].height_accum_cache = offset.y + draw_ofs.y + frame->lines[i].height_cache; - } - } + int char_pos = -1; + Line &l = p_frame->lines.write[p_line]; + bool rtl = (l.text_buf->get_direction() == TextServer::DIRECTION_RTL); + bool lrtl = is_layout_rtl(); + bool table_hit = false; - row_height = MAX(yofs, row_height); - offset.x += table->columns[column].width + hseparation; + for (int line = 0; line < l.text_buf->get_line_count(); line++) { + RID rid = l.text_buf->get_line_rid(line); - if (column == table->columns.size() - 1) { - offset.y += row_height + vseparation; - offset.x = hseparation; - row_height = 0; - } - idx++; - } + float width = l.text_buf->get_width(); + float length = TS->shaped_text_get_width(rid); + + if (rtl) { + off.x = p_width - l.offset.x - width; + if (!lrtl && p_frame == main) { // Skip Scrollbar. + off.x -= scroll_w; + } + } else { + off.x = l.offset.x; + if (lrtl && p_frame == main) { // Skip Scrollbar. + off.x += scroll_w; + } + } - int total_height = offset.y; - if (row_height) { - total_height = row_height + vseparation; + switch (l.text_buf->get_align()) { + case HALIGN_FILL: + case HALIGN_LEFT: { + if (rtl) { + off.x += width - length; } + } break; + case HALIGN_CENTER: { + off.x += Math::floor((width - length) / 2.0); + } break; + case HALIGN_RIGHT: { + if (!rtl) { + off.x += width - length; + } + } break; + } - ADVANCE(table->total_width); - CHECK_HEIGHT(total_height); + off.y += TS->shaped_text_get_ascent(rid) + l.text_buf->get_spacing_top(); - } break; + Array objects = TS->shaped_text_get_objects(rid); + for (int i = 0; i < objects.size(); i++) { + Item *it = (Item *)(uint64_t)objects[i]; + if (it != nullptr) { + Rect2 rect = TS->shaped_text_get_object_rect(rid, objects[i]); + if (rect.has_point(p_click - p_ofs - off)) { + switch (it->type) { + case ITEM_TABLE: { + int hseparation = get_theme_constant(SNAME("table_hseparation")); + int vseparation = get_theme_constant(SNAME("table_vseparation")); - default: { - } - } + ItemTable *table = static_cast<ItemTable *>(it); - Item *itp = it; + table_hit = true; - it = _get_next_item(it); + int idx = 0; + int col_count = table->columns.size(); + int row_count = table->rows.size(); - if (it && (p_line + 1 < p_frame->lines.size()) && p_frame->lines[p_line + 1].from == it) { - if (p_mode == PROCESS_POINTER && r_click_item && p_click_pos.y >= p_ofs.y + y && p_click_pos.y <= p_ofs.y + y + lh) { - //went to next line, but pointer was on the previous one - if (r_outside) { - *r_outside = true; + for (Item *E : table->subitems) { + ItemFrame *frame = static_cast<ItemFrame *>(E); + + int col = idx % col_count; + int row = idx / col_count; + + if (frame->lines.size() != 0 && row < row_count) { + Vector2 coff = frame->lines[0].offset; + if (rtl) { + coff.x = rect.size.width - table->columns[col].width - coff.x; + } + Rect2 crect = Rect2(p_ofs + off + rect.position + coff - frame->padding.position, Size2(table->columns[col].width + hseparation, table->rows[row] + vseparation) + frame->padding.position + frame->padding.size); + if (col == col_count - 1) { + if (rtl) { + crect.size.x = crect.position.x + crect.size.x; + crect.position.x = 0; + } else { + crect.size.x = get_size().x; + } + } + if (crect.has_point(p_click)) { + for (int j = 0; j < frame->lines.size(); j++) { + _find_click_in_line(frame, j, p_ofs + off + rect.position + Vector2(0, frame->lines[j].offset.y), rect.size.x, p_click, r_click_frame, r_click_line, r_click_item, r_click_char); + if (((r_click_item != nullptr) && ((*r_click_item) != nullptr)) || ((r_click_frame != nullptr) && ((*r_click_frame) != nullptr))) { + return off.y; + } + } + } + } + idx++; + } + } break; + default: + break; + } } - *r_click_item = itp; - *r_click_char = rchar; - RETURN; } + } + Rect2 rect = Rect2(p_ofs + off - Vector2(0, TS->shaped_text_get_ascent(rid)), Size2(get_size().x, TS->shaped_text_get_size(rid).y)); - break; + if (rect.has_point(p_click) && !table_hit) { + char_pos = TS->shaped_text_hit_test_position(rid, p_click.x - rect.position.x); } + off.y += TS->shaped_text_get_descent(rid) + l.text_buf->get_spacing_bottom(); } - NEW_LINE; - RETURN; + if (char_pos >= 0) { + // Find item. + if (r_click_item != nullptr) { + Item *it = p_frame->lines[p_line].from; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + if (char_pos == p_frame->lines[p_line].char_count) { + // Selection after the end of line, select last item. + if (it_to != nullptr) { + *r_click_item = _get_prev_item(it_to); + } else { + for (Item *i = it; i && i != it_to; i = _get_next_item(i)) { + *r_click_item = i; + } + } + } else { + // Selection in the line. + *r_click_item = _get_item_at_pos(it, it_to, char_pos); + } + } + + if (r_click_frame != nullptr) { + *r_click_frame = p_frame; + } + + if (r_click_line != nullptr) { + *r_click_line = p_line; + } + + if (r_click_char != nullptr) { + *r_click_char = char_pos; + } + } -#undef RETURN -#undef NEW_LINE -#undef ENSURE_WIDTH -#undef ADVANCE -#undef CHECK_HEIGHT + return off.y; } void RichTextLabel::_scroll_changed(double) { @@ -903,19 +1311,19 @@ void RichTextLabel::_update_scroll() { scroll_visible = true; scroll_w = vscroll->get_combined_minimum_size().width; vscroll->show(); - vscroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_END, -scroll_w); + vscroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -scroll_w); } else { scroll_visible = false; scroll_w = 0; vscroll->hide(); } - main->first_invalid_line = 0; //invalidate ALL + main->first_resized_line = 0; //invalidate ALL _validate_line_caches(main); } } -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; @@ -954,16 +1362,17 @@ 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(); } } break; case NOTIFICATION_RESIZED: { - main->first_invalid_line = 0; //invalidate ALL + main->first_resized_line = 0; //invalidate ALL update(); } break; + case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_ENTER_TREE: { if (bbcode != "") { set_bbcode(bbcode); @@ -971,11 +1380,11 @@ void RichTextLabel::_notification(int p_what) { main->first_invalid_line = 0; //invalidate ALL update(); - } break; - case NOTIFICATION_THEME_CHANGED: { + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: { + main->first_invalid_line = 0; //invalidate ALL update(); - } break; case NOTIFICATION_DRAW: { _validate_line_caches(main); @@ -986,206 +1395,192 @@ 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); } - int ofs = vscroll->get_value(); - - //todo, change to binary search + float vofs = vscroll->get_value(); + // Search for the first line. int from_line = 0; - int total_chars = 0; + + //TODO, change to binary search ? while (from_line < main->lines.size()) { - if (main->lines[from_line].height_accum_cache + _get_text_rect().get_position().y >= ofs) { + if (main->lines[from_line].offset.y + main->lines[from_line].text_buf->get_size().y >= vofs) { break; } - total_chars += main->lines[from_line].char_count; from_line++; } if (from_line >= main->lines.size()) { break; //nothing to draw } - int y = (main->lines[from_line].height_accum_cache - main->lines[from_line].height_cache) - ofs; - Ref<Font> base_font = get_theme_font("normal_font"); - Color base_color = get_theme_color("default_color"); - Color font_color_shadow = get_theme_color("font_color_shadow"); - 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; - while (y < size.height && from_line < main->lines.size()) { - visible_line_count++; - _process_line(main, text_rect.get_position(), y, text_rect.get_size().width - scroll_w, from_line, PROCESS_DRAW, base_font, base_color, font_color_shadow, use_outline, shadow_ofs, Point2i(), nullptr, nullptr, nullptr, total_chars); - total_chars += main->lines[from_line].char_count; + // New cache draw. + 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()) { + 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 + get_theme_constant(SNAME("line_separation")); from_line++; } } break; case NOTIFICATION_INTERNAL_PROCESS: { - float dt = get_process_delta_time(); - - _update_fx(main, dt); - update(); - } - } -} - -void RichTextLabel::_find_click(ItemFrame *p_frame, const Point2i &p_click, Item **r_click_item, int *r_click_char, bool *r_outside) { - if (r_click_item) { - *r_click_item = nullptr; - } - - Rect2 text_rect = _get_text_rect(); - int ofs = vscroll->get_value(); - Color font_color_shadow = get_theme_color("font_color_shadow"); - bool use_outline = get_theme_constant("shadow_as_outline"); - Point2 shadow_ofs(get_theme_constant("shadow_offset_x"), get_theme_constant("shadow_offset_y")); - - //todo, change to binary search - int from_line = 0; - - while (from_line < p_frame->lines.size()) { - if (p_frame->lines[from_line].height_accum_cache >= ofs) { - break; - } - from_line++; - } - - if (from_line >= p_frame->lines.size()) { - return; - } - - int y = (p_frame->lines[from_line].height_accum_cache - p_frame->lines[from_line].height_cache) - ofs; - Ref<Font> base_font = get_theme_font("normal_font"); - Color base_color = get_theme_color("default_color"); - - while (y < text_rect.get_size().height && from_line < p_frame->lines.size()) { - _process_line(p_frame, text_rect.get_position(), y, text_rect.get_size().width - scroll_w, from_line, PROCESS_POINTER, base_font, base_color, font_color_shadow, use_outline, shadow_ofs, p_click, r_click_item, r_click_char, r_outside); - if (r_click_item && *r_click_item) { - return; + if (is_visible_in_tree()) { + double dt = get_process_delta_time(); + _update_fx(main, dt); + update(); + } } - from_line++; } } 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) { + if (selection.click_item) { return CURSOR_IBEAM; } 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 get_default_cursor_shape(); //invalid } - int line = 0; Item *item = nullptr; - bool outside; - ((RichTextLabel *)(this))->_find_click(main, p_pos, &item, &line, &outside); + bool outside = true; + ((RichTextLabel *)(this))->_find_click(main, p_pos, nullptr, nullptr, &item, nullptr, &outside); if (item && !outside && ((RichTextLabel *)(this))->_find_meta(item, nullptr)) { 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; if (b.is_valid()) { if (main->first_invalid_line < main->lines.size()) { return; } + if (main->first_resized_line < main->lines.size()) { + return; + } - if (b->get_button_index() == BUTTON_LEFT) { - if (b->is_pressed() && !b->is_doubleclick()) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT) { + if (b->is_pressed() && !b->is_double_click()) { scroll_updated = false; - int line = 0; - Item *item = nullptr; - + ItemFrame *c_frame = nullptr; + int c_line = 0; + Item *c_item = nullptr; + int c_index = 0; bool outside; - _find_click(main, b->get_position(), &item, &line, &outside); - if (item) { + _find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside); + if (c_item != nullptr) { if (selection.enabled) { - selection.click = item; - selection.click_char = line; + selection.click_frame = c_frame; + selection.click_item = c_item; + selection.click_line = c_line; + selection.click_char = c_index; // Erase previous selection. if (selection.active) { - selection.from = nullptr; - selection.from_char = '\0'; - selection.to = nullptr; - selection.to_char = '\0'; + selection.from_frame = nullptr; + selection.from_line = 0; + selection.from_item = nullptr; + selection.from_char = 0; + selection.to_frame = nullptr; + selection.to_line = 0; + selection.to_item = nullptr; + selection.to_char = 0; selection.active = false; update(); } } } - } else if (b->is_pressed() && b->is_doubleclick() && selection.enabled) { - //doubleclick: select word - int line = 0; - Item *item = nullptr; + } else if (b->is_pressed() && b->is_double_click() && selection.enabled) { + //double_click: select word + + ItemFrame *c_frame = nullptr; + int c_line = 0; + Item *c_item = nullptr; + int c_index = 0; bool outside; - _find_click(main, b->get_position(), &item, &line, &outside); + _find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside); - while (item && item->type != ITEM_TEXT) { - item = _get_next_item(item, true); - } + if (c_frame) { + const Line &l = c_frame->lines[c_line]; + Vector<Vector2i> words = TS->shaped_text_get_word_breaks(l.text_buf->get_rid()); + for (int i = 0; i < words.size(); i++) { + if (c_index >= words[i].x && c_index < words[i].y) { + selection.from_frame = c_frame; + selection.from_line = c_line; + selection.from_item = c_item; + selection.from_char = words[i].x; + + selection.to_frame = c_frame; + selection.to_line = c_line; + selection.to_item = c_item; + selection.to_char = words[i].y; - if (item && item->type == ITEM_TEXT) { - String itext = static_cast<ItemText *>(item)->text; - - int beg, end; - if (select_word(itext, line, beg, end)) { - selection.from = item; - selection.to = item; - selection.from_char = beg; - selection.to_char = end - 1; - selection.active = true; - update(); + selection.active = true; + update(); + break; + } } } } else if (!b->is_pressed()) { - selection.click = nullptr; + selection.click_item = nullptr; - if (!b->is_doubleclick() && !scroll_updated) { - int line = 0; - Item *item = nullptr; + if (!b->is_double_click() && !scroll_updated) { + Item *c_item = nullptr; - bool outside; - _find_click(main, b->get_position(), &item, &line, &outside); + bool outside = true; + _find_click(main, b->get_position(), nullptr, nullptr, &c_item, nullptr, &outside); - if (item) { + if (c_item) { Variant meta; - if (!outside && _find_meta(item, &meta)) { + if (!outside && _find_meta(c_item, &meta)) { //meta clicked - - emit_signal("meta_clicked", meta); + emit_signal(SNAME("meta_clicked"), meta); } } } } } - if (b->get_button_index() == BUTTON_WHEEL_UP) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP) { if (scroll_active) { vscroll->set_value(vscroll->get_value() - vscroll->get_page() * b->get_factor() * 0.5 / 8); } } - if (b->get_button_index() == BUTTON_WHEEL_DOWN) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN) { if (scroll_active) { vscroll->set_value(vscroll->get_value() + vscroll->get_page() * b->get_factor() * 0.5 / 8); } @@ -1204,53 +1599,36 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventKey> k = p_event; if (k.is_valid()) { - if (k->is_pressed() && !k->get_alt() && !k->get_shift()) { + if (k->is_pressed()) { bool handled = false; - switch (k->get_keycode()) { - case KEY_PAGEUP: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() - vscroll->get_page()); - handled = true; - } - } break; - case KEY_PAGEDOWN: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() + vscroll->get_page()); - handled = true; - } - } break; - case KEY_UP: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() - get_theme_font("normal_font")->get_height()); - handled = true; - } - } break; - case KEY_DOWN: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_value() + get_theme_font("normal_font")->get_height()); - handled = true; - } - } break; - case KEY_HOME: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(0); - handled = true; - } - } break; - case KEY_END: { - if (vscroll->is_visible_in_tree()) { - vscroll->set_value(vscroll->get_max()); - handled = true; - } - } break; - case KEY_INSERT: - case KEY_C: { - if (k->get_command()) { - selection_copy(); - handled = true; - } - } break; + if (k->is_action("ui_page_up") && vscroll->is_visible_in_tree()) { + vscroll->set_value(vscroll->get_value() - vscroll->get_page()); + handled = true; + } + if (k->is_action("ui_page_down") && vscroll->is_visible_in_tree()) { + vscroll->set_value(vscroll->get_value() + vscroll->get_page()); + handled = true; + } + if (k->is_action("ui_up") && vscroll->is_visible_in_tree()) { + 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(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()) { + vscroll->set_value(0); + handled = true; + } + if (k->is_action("ui_end") && vscroll->is_visible_in_tree()) { + vscroll->set_value(vscroll->get_max()); + handled = true; + } + if (k->is_action("ui_copy")) { + selection_copy(); + handled = true; } if (handled) { @@ -1265,27 +1643,32 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { if (main->first_invalid_line < main->lines.size()) { return; } + if (main->first_resized_line < main->lines.size()) { + return; + } - int line = 0; - Item *item = nullptr; + ItemFrame *c_frame = nullptr; + int c_line = 0; + Item *c_item = nullptr; + int c_index = 0; bool outside; - _find_click(main, m->get_position(), &item, &line, &outside); - if (selection.click) { - if (!item) { - return; // do not update - } - - selection.from = selection.click; + _find_click(main, m->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside); + if (selection.click_item && c_item) { + selection.from_frame = selection.click_frame; + selection.from_line = selection.click_line; + selection.from_item = selection.click_item; selection.from_char = selection.click_char; - selection.to = item; - selection.to_char = line; + selection.to_frame = c_frame; + selection.to_line = c_line; + selection.to_item = c_item; + selection.to_char = c_index; bool swap = false; - if (selection.from->index > selection.to->index) { + if (selection.from_item->index > selection.to_item->index) { swap = true; - } else if (selection.from->index == selection.to->index) { + } else if (selection.from_item->index == selection.to_item->index) { if (selection.from_char > selection.to_char) { swap = true; } else if (selection.from_char == selection.to_char) { @@ -1295,7 +1678,9 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { } if (swap) { - SWAP(selection.from, selection.to); + SWAP(selection.from_frame, selection.to_frame); + SWAP(selection.from_line, selection.to_line); + SWAP(selection.from_item, selection.to_item); SWAP(selection.from_char, selection.to_char); } @@ -1305,23 +1690,48 @@ void RichTextLabel::_gui_input(Ref<InputEvent> p_event) { Variant meta; ItemMeta *item_meta; - if (item && !outside && _find_meta(item, &meta, &item_meta)) { + 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; } } } +void RichTextLabel::_find_frame(Item *p_item, ItemFrame **r_frame, int *r_line) { + if (r_frame != nullptr) { + *r_frame = nullptr; + } + if (r_line != nullptr) { + *r_line = 0; + } + + Item *item = p_item; + + while (item) { + if (item->parent != nullptr && item->parent->type == ITEM_FRAME) { + if (r_frame != nullptr) { + *r_frame = (ItemFrame *)item->parent; + } + if (r_line != nullptr) { + *r_line = item->line; + } + return; + } + + item = item->parent; + } +} + Ref<Font> RichTextLabel::_find_font(Item *p_item) { Item *fontitem = p_item; @@ -1337,10 +1747,116 @@ Ref<Font> RichTextLabel::_find_font(Item *p_item) { return Ref<Font>(); } -int RichTextLabel::_find_margin(Item *p_item, const Ref<Font> &p_base_font) { +int RichTextLabel::_find_font_size(Item *p_item) { + Item *sizeitem = p_item; + + while (sizeitem) { + if (sizeitem->type == ITEM_FONT_SIZE) { + ItemFontSize *fi = static_cast<ItemFontSize *>(sizeitem); + return fi->font_size; + } + + sizeitem = sizeitem->parent; + } + + return -1; +} + +int RichTextLabel::_find_outline_size(Item *p_item, int p_default) { + Item *sizeitem = p_item; + + while (sizeitem) { + if (sizeitem->type == ITEM_OUTLINE_SIZE) { + ItemOutlineSize *fi = static_cast<ItemOutlineSize *>(sizeitem); + return fi->outline_size; + } + + sizeitem = sizeitem->parent; + } + + return p_default; +} + +Dictionary RichTextLabel::_find_font_features(Item *p_item) { + Item *ffitem = p_item; + + while (ffitem) { + if (ffitem->type == ITEM_FONT_FEATURES) { + ItemFontFeatures *fi = static_cast<ItemFontFeatures *>(ffitem); + return fi->opentype_features; + } + + ffitem = ffitem->parent; + } + + return Dictionary(); +} + +RichTextLabel::ItemDropcap *RichTextLabel::_find_dc_item(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_DROPCAP) { + return static_cast<ItemDropcap *>(item); + } + item = item->parent; + } + + return nullptr; +} + +RichTextLabel::ItemList *RichTextLabel::_find_list_item(Item *p_item) { Item *item = p_item; - int margin = 0; + while (item) { + if (item->type == ITEM_LIST) { + return static_cast<ItemList *>(item); + } + item = item->parent; + } + + return nullptr; +} + +int RichTextLabel::_find_list(Item *p_item, Vector<int> &r_index, Vector<ItemList *> &r_list) { + Item *item = p_item; + Item *prev_item = p_item; + + int level = 0; + + while (item) { + if (item->type == ITEM_LIST) { + ItemList *list = static_cast<ItemList *>(item); + + ItemFrame *frame = nullptr; + int line = -1; + _find_frame(list, &frame, &line); + + int index = 1; + if (frame != nullptr) { + for (int i = list->line + 1; i <= prev_item->line; i++) { + if (_find_list_item(frame->lines[i].from) == list) { + index++; + } + } + } + + r_index.push_back(index); + r_list.push_back(list); + + prev_item = item; + } + level++; + item = item->parent; + } + + return level; +} + +int RichTextLabel::_find_margin(Item *p_item, const Ref<Font> &p_base_font, int p_base_font_size) { + Item *item = p_item; + + float margin = 0.0; while (item) { if (item->type == ITEM_INDENT) { @@ -1348,16 +1864,22 @@ int RichTextLabel::_find_margin(Item *p_item, const Ref<Font> &p_base_font) { if (font.is_null()) { font = p_base_font; } - - ItemIndent *indent = static_cast<ItemIndent *>(item); - - margin += indent->level * tab_size * font->get_char_size(' ').width; + int font_size = _find_font_size(item); + if (font_size == -1) { + font_size = p_base_font_size; + } + margin += tab_size * font->get_char_size(' ', 0, font_size).width; } else if (item->type == ITEM_LIST) { Ref<Font> font = _find_font(item); if (font.is_null()) { font = p_base_font; } + int font_size = _find_font_size(item); + if (font_size == -1) { + font_size = p_base_font_size; + } + margin += tab_size * font->get_char_size(' ', 0, font_size).width; } item = item->parent; @@ -1370,9 +1892,9 @@ RichTextLabel::Align RichTextLabel::_find_align(Item *p_item) { Item *item = p_item; while (item) { - if (item->type == ITEM_ALIGN) { - ItemAlign *align = static_cast<ItemAlign *>(item); - return align->align; + if (item->type == ITEM_PARAGRAPH) { + ItemParagraph *p = static_cast<ItemParagraph *>(item); + return p->align; } item = item->parent; @@ -1381,6 +1903,57 @@ RichTextLabel::Align RichTextLabel::_find_align(Item *p_item) { return default_align; } +TextServer::Direction RichTextLabel::_find_direction(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_PARAGRAPH) { + ItemParagraph *p = static_cast<ItemParagraph *>(item); + if (p->direction != Control::TEXT_DIRECTION_INHERITED) { + return (TextServer::Direction)p->direction; + } + } + + item = item->parent; + } + + if (text_direction == Control::TEXT_DIRECTION_INHERITED) { + return is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR; + } else { + return (TextServer::Direction)text_direction; + } +} + +Control::StructuredTextParser RichTextLabel::_find_stt(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_PARAGRAPH) { + ItemParagraph *p = static_cast<ItemParagraph *>(item); + return p->st_parser; + } + + item = item->parent; + } + + return st_parser; +} + +String RichTextLabel::_find_language(Item *p_item) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_PARAGRAPH) { + ItemParagraph *p = static_cast<ItemParagraph *>(item); + return p->language; + } + + item = item->parent; + } + + return language; +} + Color RichTextLabel::_find_color(Item *p_item, const Color &p_default_color) { Item *item = p_item; @@ -1396,25 +1969,26 @@ Color RichTextLabel::_find_color(Item *p_item, const Color &p_default_color) { return p_default_color; } -bool RichTextLabel::_find_underline(Item *p_item) { +Color RichTextLabel::_find_outline_color(Item *p_item, const Color &p_default_color) { Item *item = p_item; while (item) { - if (item->type == ITEM_UNDERLINE) { - return true; + if (item->type == ITEM_OUTLINE_COLOR) { + ItemOutlineColor *color = static_cast<ItemOutlineColor *>(item); + return color->color; } item = item->parent; } - return false; + return p_default_color; } -bool RichTextLabel::_find_strikethrough(Item *p_item) { +bool RichTextLabel::_find_underline(Item *p_item) { Item *item = p_item; while (item) { - if (item->type == ITEM_STRIKETHROUGH) { + if (item->type == ITEM_UNDERLINE) { return true; } @@ -1424,17 +1998,17 @@ bool RichTextLabel::_find_strikethrough(Item *p_item) { return false; } -bool RichTextLabel::_find_by_type(Item *p_item, ItemType p_type) { - ERR_FAIL_INDEX_V((int)p_type, 19, false); - +bool RichTextLabel::_find_strikethrough(Item *p_item) { Item *item = p_item; while (item) { - if (item->type == p_type) { + if (item->type == ITEM_STRIKETHROUGH) { return true; } + item = item->parent; } + return false; } @@ -1449,46 +2023,6 @@ void RichTextLabel::_fetch_item_fx_stack(Item *p_item, Vector<ItemFX *> &r_stack } } -Color RichTextLabel::_get_color_from_string(const String &p_color_str, const Color &p_default_color) { - if (p_color_str.begins_with("#")) { - return Color::html(p_color_str); - } else if (p_color_str == "aqua") { - return Color(0, 1, 1); - } else if (p_color_str == "black") { - return Color(0, 0, 0); - } else if (p_color_str == "blue") { - return Color(0, 0, 1); - } else if (p_color_str == "fuchsia") { - return Color(1, 0, 1); - } else if (p_color_str == "gray" || p_color_str == "grey") { - return Color(0.5, 0.5, 0.5); - } else if (p_color_str == "green") { - return Color(0, 0.5, 0); - } else if (p_color_str == "lime") { - return Color(0, 1, 0); - } else if (p_color_str == "maroon") { - return Color(0.5, 0, 0); - } else if (p_color_str == "navy") { - return Color(0, 0, 0.5); - } else if (p_color_str == "olive") { - return Color(0.5, 0.5, 0); - } else if (p_color_str == "purple") { - return Color(0.5, 0, 0.5); - } else if (p_color_str == "red") { - return Color(1, 0, 0); - } else if (p_color_str == "silver") { - return Color(0.75, 0.75, 0.75); - } else if (p_color_str == "teal") { - return Color(0, 0.5, 0.5); - } else if (p_color_str == "white") { - return Color(1, 1, 1); - } else if (p_color_str == "yellow") { - return Color(1, 1, 0); - } else { - return p_default_color; - } -} - bool RichTextLabel::_find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item) { Item *item = p_item; @@ -1510,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; @@ -1530,46 +2094,74 @@ bool RichTextLabel::_find_layout_subitem(Item *from, Item *to) { void RichTextLabel::_validate_line_caches(ItemFrame *p_frame) { if (p_frame->first_invalid_line == p_frame->lines.size()) { + if (p_frame->first_resized_line == p_frame->lines.size()) { + return; + } + + // Resize lines without reshaping. + Size2 size = get_size(); + if (fixed_width != -1) { + size.width = fixed_width; + } + Rect2 text_rect = _get_text_rect(); + + 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); + } + + int total_height = 0; + if (p_frame->lines.size()) { + total_height = p_frame->lines[p_frame->lines.size() - 1].offset.y + p_frame->lines[p_frame->lines.size() - 1].text_buf->get_size().y; + } + + p_frame->first_resized_line = p_frame->lines.size(); + + updating_scroll = true; + vscroll->set_max(total_height); + vscroll->set_page(text_rect.size.height); + if (scroll_follow && scroll_following) { + vscroll->set_value(total_height - size.height); + } + updating_scroll = false; + + if (fit_content_height) { + minimum_size_changed(); + } return; } - //validate invalid lines + // Shape invalid lines. Size2 size = get_size(); if (fixed_width != -1) { size.width = fixed_width; } Rect2 text_rect = _get_text_rect(); - Color font_color_shadow = get_theme_color("font_color_shadow"); - 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("normal_font"); + 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++) { - int y = 0; - _process_line(p_frame, text_rect.get_position(), y, text_rect.get_size().width - scroll_w, i, PROCESS_CACHE, base_font, Color(), font_color_shadow, use_outline, shadow_ofs); - p_frame->lines.write[i].height_cache = y; - p_frame->lines.write[i].height_accum_cache = y; - - if (i > 0) { - p_frame->lines.write[i].height_accum_cache += p_frame->lines[i - 1].height_accum_cache; - } + _shape_line(p_frame, i, base_font, base_font_size, text_rect.get_size().width - scroll_w, &total_chars); } int total_height = 0; if (p_frame->lines.size()) { - total_height = p_frame->lines[p_frame->lines.size() - 1].height_accum_cache + get_theme_stylebox("normal")->get_minimum_size().height; + total_height = p_frame->lines[p_frame->lines.size() - 1].offset.y + p_frame->lines[p_frame->lines.size() - 1].text_buf->get_size().y; } - main->first_invalid_line = p_frame->lines.size(); + p_frame->first_invalid_line = p_frame->lines.size(); + p_frame->first_resized_line = p_frame->lines.size(); updating_scroll = true; vscroll->set_max(total_height); - vscroll->set_page(size.height); + vscroll->set_page(text_rect.size.height); if (scroll_follow && scroll_following) { vscroll->set_value(total_height - size.height); } - updating_scroll = false; if (fit_content_height) { @@ -1641,6 +2233,13 @@ void RichTextLabel::_add_item(Item *p_item, bool p_enter, bool p_ensure_newline) p_item->parent = current; p_item->E = current->subitems.push_back(p_item); p_item->index = current_idx++; + p_item->char_ofs = current_char_ofs; + if (p_item->type == ITEM_TEXT) { + ItemText *t = (ItemText *)p_item; + current_char_ofs += t->text.length(); + } else if (p_item->type == ITEM_IMAGE) { + current_char_ofs++; + } if (p_enter) { current = p_item; @@ -1671,31 +2270,38 @@ void RichTextLabel::_remove_item(Item *p_item, const int p_line, const int p_sub int size = p_item->subitems.size(); if (size == 0) { p_item->parent->subitems.erase(p_item); + // If a newline was erased, all lines AFTER the newline need to be decremented. if (p_item->type == ITEM_NEWLINE) { current_frame->lines.remove(p_line); - for (int i = p_subitem_line; i < current->subitems.size(); i++) { - if (current->subitems[i]->line > 0) { + for (int i = 0; i < current->subitems.size(); i++) { + if (current->subitems[i]->line > p_subitem_line) { current->subitems[i]->line--; } } } } else { + // First, remove all child items for the provided item. for (int i = 0; i < size; i++) { _remove_item(p_item->subitems.front()->get(), p_line, p_subitem_line); } + // Then remove the provided item itself. + p_item->parent->subitems.erase(p_item); } } -void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color) { +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; } ERR_FAIL_COND(p_image.is_null()); + ERR_FAIL_COND(p_image->get_width() == 0); + ERR_FAIL_COND(p_image->get_height() == 0); ItemImage *item = memnew(ItemImage); item->image = p_image; item->color = p_color; + item->inline_align = p_align; if (p_width > 0) { // custom width @@ -1739,21 +2345,23 @@ bool RichTextLabel::remove_line(const int p_line) { return false; } - int i = 0; - while (i < current->subitems.size() && current->subitems[i]->line < p_line) { - i++; + // Remove all subitems with the same line as that provided. + Vector<int> subitem_indices_to_remove; + for (int i = 0; i < current->subitems.size(); i++) { + if (current->subitems[i]->line == p_line) { + subitem_indices_to_remove.push_back(i); + } } - bool was_newline = false; - while (i < current->subitems.size()) { - was_newline = current->subitems[i]->type == ITEM_NEWLINE; - _remove_item(current->subitems[i], current->subitems[i]->line, p_line); - if (was_newline) { - break; - } + bool had_newline = false; + // Reverse for loop to remove items from the end first. + for (int i = subitem_indices_to_remove.size() - 1; i >= 0; i--) { + int subitem_idx = subitem_indices_to_remove[i]; + had_newline = had_newline || current->subitems[subitem_idx]->type == ITEM_NEWLINE; + _remove_item(current->subitems[subitem_idx], current->subitems[subitem_idx]->line, p_line); } - if (!was_newline) { + if (!had_newline) { current_frame->lines.remove(p_line); if (current_frame->lines.size() == 0) { current_frame->lines.resize(1); @@ -1764,11 +2372,30 @@ bool RichTextLabel::remove_line(const int p_line) { main->lines.write[0].from = main; } - main->first_invalid_line = 0; + main->first_invalid_line = 0; // p_line ??? + update(); return true; } +void RichTextLabel::push_dropcap(const String &p_string, const Ref<Font> &p_font, int p_size, const Rect2 &p_dropcap_margins, const Color &p_color, int p_ol_size, const Color &p_ol_color) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ERR_FAIL_COND(p_string.is_empty()); + ERR_FAIL_COND(p_font.is_null()); + ERR_FAIL_COND(p_size <= 0); + + ItemDropcap *item = memnew(ItemDropcap); + + item->text = p_string; + item->font = p_font; + item->font_size = p_size; + item->color = p_color; + item->ol_size = p_ol_size; + item->ol_color = p_ol_color; + item->dropcap_margins = p_dropcap_margins; + _add_item(item, false); +} + void RichTextLabel::push_font(const Ref<Font> &p_font) { ERR_FAIL_COND(current->type == ITEM_TABLE); ERR_FAIL_COND(p_font.is_null()); @@ -1779,40 +2406,64 @@ 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); } +void RichTextLabel::push_font_size(int p_font_size) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemFontSize *item = memnew(ItemFontSize); + + item->font_size = p_font_size; + _add_item(item, true); +} + +void RichTextLabel::push_font_features(const Dictionary &p_features) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemFontFeatures *item = memnew(ItemFontFeatures); + + item->opentype_features = p_features; + _add_item(item, true); +} + +void RichTextLabel::push_outline_size(int p_font_size) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemOutlineSize *item = memnew(ItemOutlineSize); + + item->outline_size = p_font_size; + _add_item(item, true); +} + void RichTextLabel::push_color(const Color &p_color) { ERR_FAIL_COND(current->type == ITEM_TABLE); ItemColor *item = memnew(ItemColor); @@ -1821,6 +2472,14 @@ void RichTextLabel::push_color(const Color &p_color) { _add_item(item, true); } +void RichTextLabel::push_outline_color(const Color &p_color) { + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemOutlineColor *item = memnew(ItemOutlineColor); + + item->color = p_color; + _add_item(item, true); +} + void RichTextLabel::push_underline() { ERR_FAIL_COND(current->type == ITEM_TABLE); ItemUnderline *item = memnew(ItemUnderline); @@ -1835,11 +2494,14 @@ void RichTextLabel::push_strikethrough() { _add_item(item, true); } -void RichTextLabel::push_align(Align p_align) { +void RichTextLabel::push_paragraph(Align p_align, Control::TextDirection p_direction, const String &p_language, Control::StructuredTextParser p_st_parser) { ERR_FAIL_COND(current->type == ITEM_TABLE); - ItemAlign *item = memnew(ItemAlign); + ItemParagraph *item = memnew(ItemParagraph); item->align = p_align; + item->direction = p_direction; + item->language = p_language; + item->st_parser = p_st_parser; _add_item(item, true, true); } @@ -1852,13 +2514,15 @@ void RichTextLabel::push_indent(int p_level) { _add_item(item, true, true); } -void RichTextLabel::push_list(ListType p_list) { +void RichTextLabel::push_list(int p_level, ListType p_list, bool p_capitalize) { ERR_FAIL_COND(current->type == ITEM_TABLE); - ERR_FAIL_INDEX(p_list, 3); + ERR_FAIL_COND(p_level < 0); ItemList *item = memnew(ItemList); item->list_type = p_list; + item->level = p_level; + item->capitalize = p_capitalize; _add_item(item, true, true); } @@ -1870,17 +2534,18 @@ void RichTextLabel::push_meta(const Variant &p_meta) { _add_item(item, true); } -void RichTextLabel::push_table(int p_columns) { +void RichTextLabel::push_table(int p_columns, InlineAlign p_align) { ERR_FAIL_COND(p_columns < 1); ItemTable *item = memnew(ItemTable); item->columns.resize(p_columns); item->total_width = 0; + item->inline_align = p_align; for (int i = 0; i < item->columns.size(); i++) { item->columns.write[i].expand = false; item->columns.write[i].expand_ratio = 1; } - _add_item(item, true, true); + _add_item(item, true, false); } void RichTextLabel::push_fade(int p_start_index, int p_length) { @@ -1919,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; @@ -1934,6 +2615,36 @@ void RichTextLabel::set_table_column_expand(int p_column, bool p_expand, int p_r table->columns.write[p_column].expand_ratio = p_ratio; } +void RichTextLabel::set_cell_row_background_color(const Color &p_odd_row_bg, const Color &p_even_row_bg) { + ERR_FAIL_COND(current->type != ITEM_FRAME); + ItemFrame *cell = static_cast<ItemFrame *>(current); + ERR_FAIL_COND(!cell->cell); + cell->odd_row_bg = p_odd_row_bg; + cell->even_row_bg = p_even_row_bg; +} + +void RichTextLabel::set_cell_border_color(const Color &p_color) { + ERR_FAIL_COND(current->type != ITEM_FRAME); + ItemFrame *cell = static_cast<ItemFrame *>(current); + ERR_FAIL_COND(!cell->cell); + cell->border = p_color; +} + +void RichTextLabel::set_cell_size_override(const Size2 &p_min_size, const Size2 &p_max_size) { + ERR_FAIL_COND(current->type != ITEM_FRAME); + ItemFrame *cell = static_cast<ItemFrame *>(current); + ERR_FAIL_COND(!cell->cell); + cell->min_size_over = p_min_size; + cell->max_size_over = p_max_size; +} + +void RichTextLabel::set_cell_padding(const Rect2 &p_padding) { + ERR_FAIL_COND(current->type != ITEM_FRAME); + ItemFrame *cell = static_cast<ItemFrame *>(current); + ERR_FAIL_COND(!cell->cell); + cell->padding = p_padding; +} + void RichTextLabel::push_cell() { ERR_FAIL_COND(current->type != ITEM_TABLE); @@ -1942,10 +2653,9 @@ void RichTextLabel::push_cell() { _add_item(item, true); current_frame = item; item->cell = true; - item->parent_line = item->parent_frame->lines.size() - 1; item->lines.resize(1); item->lines.write[0].from = nullptr; - item->first_invalid_line = 0; + item->first_invalid_line = 0; // parent frame last line ??? } int RichTextLabel::get_current_table_column() const { @@ -1972,9 +2682,13 @@ void RichTextLabel::clear() { main->lines.resize(1); main->first_invalid_line = 0; update(); - selection.click = nullptr; + + selection.click_frame = nullptr; + selection.click_item = nullptr; selection.active = false; + current_idx = 1; + current_char_ofs = 0; if (scroll_follow) { scroll_following = true; } @@ -1986,7 +2700,7 @@ void RichTextLabel::clear() { void RichTextLabel::set_tab_size(int p_spaces) { tab_size = p_spaces; - main->first_invalid_line = 0; + main->first_resized_line = 0; update(); } @@ -2060,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; @@ -2105,7 +2819,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { String bbcode_name; typedef Map<String, String> OptionMap; OptionMap bbcode_options; - if (!split_tag_block.empty()) { + if (!split_tag_block.is_empty()) { bbcode_name = split_tag_block[0]; for (int i = 1; i < split_tag_block.size(); i++) { const String &expr = split_tag_block[i]; @@ -2135,7 +2849,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { if (tag_stack.front()->get() == "i") { in_italics = false; } - if (tag_stack.front()->get() == "indent") { + if ((tag_stack.front()->get() == "indent") || (tag_stack.front()->get() == "ol") || (tag_stack.front()->get() == "ul")) { indent_level--; } @@ -2147,7 +2861,7 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { tag_stack.pop_front(); pos = brk_end + 1; - if (tag != "/img") { + if (tag != "/img" && tag != "/dropcap") { pop(); } @@ -2177,12 +2891,41 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag.begins_with("table=")) { - int columns = tag.substr(6, tag.length()).to_int(); + Vector<String> subtag = tag.substr(6, tag.length()).split(","); + int columns = subtag[0].to_int(); if (columns < 1) { columns = 1; } - push_table(columns); + int align = INLINE_ALIGN_TOP; + if (subtag.size() > 2) { + if (subtag[1] == "top" || subtag[1] == "t") { + align = INLINE_ALIGN_TOP_TO; + } else if (subtag[1] == "center" || subtag[1] == "c") { + align = INLINE_ALIGN_CENTER_TO; + } else if (subtag[1] == "bottom" || subtag[1] == "b") { + 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, (InlineAlign)align); pos = brk_end + 1; tag_stack.push_front("table"); } else if (tag == "cell") { @@ -2197,6 +2940,43 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { set_table_column_expand(get_current_table_column(), true, ratio); push_cell(); + + pos = brk_end + 1; + tag_stack.push_front("cell"); + } else if (tag.begins_with("cell ")) { + Vector<String> subtag = tag.substr(5, tag.length()).split(" "); + + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "expand") { + int ratio = subtag_a[1].to_int(); + if (ratio < 1) { + ratio = 1; + } + set_table_column_expand(get_current_table_column(), true, ratio); + } + } + } + push_cell(); + const Color fallback_color = Color(0, 0, 0, 0); + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "border") { + Color color = Color::from_string(subtag_a[1], fallback_color); + set_cell_border_color(color); + } else if (subtag_a[0] == "bg") { + Vector<String> subtag_b = subtag_a[1].split(","); + if (subtag_b.size() == 2) { + Color color1 = Color::from_string(subtag_b[0], fallback_color); + Color color2 = Color::from_string(subtag_b[1], fallback_color); + set_cell_row_background_color(color1, color2); + } + } + } + } + pos = brk_end + 1; tag_stack.push_front("cell"); } else if (tag == "u") { @@ -2209,32 +2989,156 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { push_strikethrough(); pos = brk_end + 1; tag_stack.push_front(tag); + } else if (tag == "lrm") { + add_text(String::chr(0x200E)); + pos = brk_end + 1; + } else if (tag == "rlm") { + add_text(String::chr(0x200F)); + pos = brk_end + 1; + } else if (tag == "lre") { + add_text(String::chr(0x202A)); + pos = brk_end + 1; + } else if (tag == "rle") { + add_text(String::chr(0x202B)); + pos = brk_end + 1; + } else if (tag == "lro") { + add_text(String::chr(0x202D)); + pos = brk_end + 1; + } else if (tag == "rlo") { + add_text(String::chr(0x202E)); + pos = brk_end + 1; + } else if (tag == "pdf") { + add_text(String::chr(0x202C)); + pos = brk_end + 1; + } else if (tag == "alm") { + add_text(String::chr(0x061c)); + pos = brk_end + 1; + } else if (tag == "lri") { + add_text(String::chr(0x2066)); + pos = brk_end + 1; + } else if (tag == "rli") { + add_text(String::chr(0x2027)); + pos = brk_end + 1; + } else if (tag == "fsi") { + add_text(String::chr(0x2068)); + pos = brk_end + 1; + } else if (tag == "pdi") { + add_text(String::chr(0x2069)); + pos = brk_end + 1; + } else if (tag == "zwj") { + add_text(String::chr(0x200D)); + pos = brk_end + 1; + } else if (tag == "zwnj") { + add_text(String::chr(0x200C)); + pos = brk_end + 1; + } else if (tag == "wj") { + add_text(String::chr(0x2060)); + pos = brk_end + 1; + } else if (tag == "shy") { + add_text(String::chr(0x00AD)); + pos = brk_end + 1; } else if (tag == "center") { - push_align(ALIGN_CENTER); + push_paragraph(ALIGN_CENTER); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "fill") { - push_align(ALIGN_FILL); + push_paragraph(ALIGN_FILL); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "right") { - push_align(ALIGN_RIGHT); + push_paragraph(ALIGN_RIGHT); pos = brk_end + 1; tag_stack.push_front(tag); } else if (tag == "ul") { - push_list(LIST_DOTS); + indent_level++; + push_list(indent_level, LIST_DOTS, false); pos = brk_end + 1; tag_stack.push_front(tag); - } else if (tag == "ol") { - push_list(LIST_NUMBERS); + } else if ((tag == "ol") || (tag == "ol type=1")) { + indent_level++; + push_list(indent_level, LIST_NUMBERS, false); pos = brk_end + 1; tag_stack.push_front(tag); + } else if (tag == "ol type=a") { + indent_level++; + push_list(indent_level, LIST_LETTERS, false); + pos = brk_end + 1; + tag_stack.push_front("ol"); + } else if (tag == "ol type=A") { + indent_level++; + push_list(indent_level, LIST_LETTERS, true); + pos = brk_end + 1; + tag_stack.push_front("ol"); + } else if (tag == "ol type=i") { + indent_level++; + push_list(indent_level, LIST_ROMAN, false); + pos = brk_end + 1; + tag_stack.push_front("ol"); + } else if (tag == "ol type=I") { + indent_level++; + push_list(indent_level, LIST_ROMAN, true); + pos = brk_end + 1; + tag_stack.push_front("ol"); } else if (tag == "indent") { indent_level++; push_indent(indent_level); pos = brk_end + 1; tag_stack.push_front(tag); - + } else if (tag == "p") { + push_paragraph(ALIGN_LEFT); + pos = brk_end + 1; + tag_stack.push_front("p"); + } else if (tag.begins_with("p ")) { + Vector<String> subtag = tag.substr(2, tag.length()).split(" "); + Align align = ALIGN_LEFT; + Control::TextDirection dir = Control::TEXT_DIRECTION_INHERITED; + String lang; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "align") { + if (subtag_a[1] == "l" || subtag_a[1] == "left") { + align = ALIGN_LEFT; + } else if (subtag_a[1] == "c" || subtag_a[1] == "center") { + align = ALIGN_CENTER; + } else if (subtag_a[1] == "r" || subtag_a[1] == "right") { + align = ALIGN_RIGHT; + } else if (subtag_a[1] == "f" || subtag_a[1] == "fill") { + align = ALIGN_FILL; + } + } else if (subtag_a[0] == "dir" || subtag_a[0] == "direction") { + if (subtag_a[1] == "a" || subtag_a[1] == "auto") { + dir = Control::TEXT_DIRECTION_AUTO; + } else if (subtag_a[1] == "l" || subtag_a[1] == "ltr") { + dir = Control::TEXT_DIRECTION_LTR; + } else if (subtag_a[1] == "r" || subtag_a[1] == "rtl") { + dir = Control::TEXT_DIRECTION_RTL; + } + } else if (subtag_a[0] == "lang" || subtag_a[0] == "language") { + lang = subtag_a[1]; + } else if (subtag_a[0] == "st" || subtag_a[0] == "bidi_override") { + if (subtag_a[1] == "d" || subtag_a[1] == "default") { + st_parser = STRUCTURED_TEXT_DEFAULT; + } else if (subtag_a[1] == "u" || subtag_a[1] == "uri") { + st_parser = STRUCTURED_TEXT_URI; + } else if (subtag_a[1] == "f" || subtag_a[1] == "file") { + st_parser = STRUCTURED_TEXT_FILE; + } else if (subtag_a[1] == "e" || subtag_a[1] == "email") { + st_parser = STRUCTURED_TEXT_EMAIL; + } else if (subtag_a[1] == "l" || subtag_a[1] == "list") { + st_parser = STRUCTURED_TEXT_LIST; + } else if (subtag_a[1] == "n" || subtag_a[1] == "none") { + st_parser = STRUCTURED_TEXT_NONE; + } else if (subtag_a[1] == "c" || subtag_a[1] == "custom") { + st_parser = STRUCTURED_TEXT_CUSTOM; + } + } + } + } + push_paragraph(align, dir, lang, st_parser); + pos = brk_end + 1; + tag_stack.push_front("p"); } else if (tag == "url") { int end = p_bbcode.find("[", brk_end); if (end == -1) { @@ -2251,7 +3155,86 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { push_meta(url); pos = brk_end + 1; tag_stack.push_front("url"); - } else if (bbcode_name == "img") { + } else if (tag.begins_with("dropcap")) { + Vector<String> subtag = tag.substr(5, tag.length()).split(" "); + 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++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "font" || subtag_a[0] == "f") { + String fnt = subtag_a[1]; + Ref<Font> font = ResourceLoader::load(fnt, "Font"); + if (font.is_valid()) { + f = font; + } + } else if (subtag_a[0] == "font_size") { + fs = subtag_a[1].to_int(); + } else if (subtag_a[0] == "margins") { + Vector<String> subtag_b = subtag_a[1].split(","); + if (subtag_b.size() == 4) { + dropcap_margins.position.x = subtag_b[0].to_float(); + dropcap_margins.position.y = subtag_b[1].to_float(); + dropcap_margins.size.x = subtag_b[2].to_float(); + dropcap_margins.size.y = subtag_b[3].to_float(); + } + } else if (subtag_a[0] == "outline_size") { + outline_size = subtag_a[1].to_int(); + } else if (subtag_a[0] == "color") { + color = Color::from_string(subtag_a[1], color); + } else if (subtag_a[0] == "outline_color") { + outline_color = Color::from_string(subtag_a[1], outline_color); + } + } + } + int end = p_bbcode.find("[", brk_end); + if (end == -1) { + end = p_bbcode.length(); + } + + String txt = p_bbcode.substr(brk_end + 1, end - brk_end - 1); + + push_dropcap(txt, f, fs, dropcap_margins, color, outline_size, outline_color); + + pos = end; + tag_stack.push_front(bbcode_name); + } else if (tag.begins_with("img")) { + int align = INLINE_ALIGN_CENTER; + if (tag.begins_with("img=")) { + 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; + } + } + } + int end = p_bbcode.find("[", brk_end); if (end == -1) { end = p_bbcode.length(); @@ -2264,12 +3247,12 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { Color color = Color(1.0, 1.0, 1.0); OptionMap::Element *color_option = bbcode_options.find("color"); if (color_option) { - color = _get_color_from_string(color_option->value(), color); + color = Color::from_string(color_option->value(), color); } int width = 0; int height = 0; - if (!bbcode_value.empty()) { + if (!bbcode_value.is_empty()) { int sep = bbcode_value.find("x"); if (sep == -1) { width = bbcode_value.to_int(); @@ -2289,18 +3272,25 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { } } - add_image(texture, width, height, color); + add_image(texture, width, height, color, (InlineAlign)align); } pos = end; tag_stack.push_front(bbcode_name); } else if (tag.begins_with("color=")) { String color_str = tag.substr(6, tag.length()); - Color color = _get_color_from_string(color_str, base_color); + Color color = Color::from_string(color_str, base_color); push_color(color); pos = brk_end + 1; tag_stack.push_front("color"); + } else if (tag.begins_with("outline_color=")) { + String color_str = tag.substr(14, tag.length()); + Color color = Color::from_string(color_str, base_color); + push_outline_color(color); + pos = brk_end + 1; + tag_stack.push_front("outline_color"); + } else if (tag.begins_with("font=")) { String fnt = tag.substr(5, tag.length()); @@ -2313,6 +3303,54 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { pos = brk_end + 1; tag_stack.push_front("font"); + } else if (tag.begins_with("font_size=")) { + int fnt_size = tag.substr(10, tag.length()).to_int(); + push_font_size(fnt_size); + pos = brk_end + 1; + tag_stack.push_front("font_size"); + } else if (tag.begins_with("opentype_features=")) { + String fnt_ftr = tag.substr(18, tag.length()); + Vector<String> subtag = fnt_ftr.split(","); + Dictionary ftrs; + for (int i = 0; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("="); + if (subtag_a.size() == 2) { + ftrs[TS->name_to_tag(subtag_a[0])] = subtag_a[1].to_int(); + } else if (subtag_a.size() == 1) { + ftrs[TS->name_to_tag(subtag_a[0])] = 1; + } + } + push_font_features(ftrs); + pos = brk_end + 1; + tag_stack.push_front("opentype_features"); + } else if (tag.begins_with("font ")) { + Vector<String> subtag = tag.substr(2, tag.length()).split(" "); + + for (int i = 1; i < subtag.size(); i++) { + Vector<String> subtag_a = subtag[i].split("=", true, 2); + if (subtag_a.size() == 2) { + if (subtag_a[0] == "name" || subtag_a[0] == "n") { + String fnt = subtag_a[1]; + Ref<Font> font = ResourceLoader::load(fnt, "Font"); + if (font.is_valid()) { + push_font(font); + } else { + push_font(normal_font); + } + } else if (subtag_a[0] == "size" || subtag_a[0] == "s") { + int fnt_size = subtag_a[1].to_int(); + push_font_size(fnt_size); + } + } + } + + pos = brk_end + 1; + tag_stack.push_front("font"); + } else if (tag.begins_with("outline_size=")) { + int fnt_size = tag.substr(13, tag.length()).to_int(); + push_outline_size(fnt_size); + pos = brk_end + 1; + tag_stack.push_front("outline_size"); } else if (bbcode_name == "fade") { int start_index = 0; @@ -2404,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) { @@ -2429,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()) { @@ -2442,14 +3497,46 @@ Error RichTextLabel::append_bbcode(const String &p_bbcode) { return OK; } +void RichTextLabel::scroll_to_paragraph(int p_paragraph) { + ERR_FAIL_INDEX(p_paragraph, main->lines.size()); + _validate_line_caches(main); + vscroll->set_value(main->lines[p_paragraph].offset.y); +} + +int RichTextLabel::get_paragraph_count() const { + return current_frame->lines.size(); +} + +int RichTextLabel::get_visible_paragraph_count() const { + if (!is_visible()) { + return 0; + } + return visible_paragraph_count; +} + void RichTextLabel::scroll_to_line(int p_line) { - ERR_FAIL_INDEX(p_line, main->lines.size()); _validate_line_caches(main); - vscroll->set_value(main->lines[p_line].height_accum_cache - main->lines[p_line].height_cache); + + int line_count = 0; + for (int i = 0; i < main->lines.size(); i++) { + if ((line_count <= p_line) && (line_count + main->lines[i].text_buf->get_line_count() >= p_line)) { + float line_offset = 0.f; + for (int j = 0; j < p_line - line_count; j++) { + line_offset += main->lines[i].text_buf->get_line_size(j).y; + } + vscroll->set_value(main->lines[i].offset.y + line_offset); + return; + } + line_count += main->lines[i].text_buf->get_line_count(); + } } int RichTextLabel::get_line_count() const { - return current_frame->lines.size(); + int line_count = 0; + for (int i = 0; i < main->lines.size(); i++) { + line_count += main->lines[i].text_buf->get_line_count(); + } + return line_count; } int RichTextLabel::get_visible_line_count() const { @@ -2472,95 +3559,240 @@ void RichTextLabel::set_selection_enabled(bool p_enabled) { } } +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); + + Line &l = p_frame->lines.write[p_line]; + + String text; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + switch (it->type) { + case ITEM_NEWLINE: { + text += "\n"; + } break; + case ITEM_TEXT: { + ItemText *t = (ItemText *)it; + text += t->text; + } break; + case ITEM_IMAGE: { + text += " "; + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + 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 = -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; + selection.from_item = _get_item_at_pos(l.from, it_to, sp); + 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()); + selection.to_char = sp + p_string.length(); + selection.active = true; + return true; + } + + return false; +} + bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p_search_previous) { ERR_FAIL_COND_V(!selection.enabled, false); - Item *it = main; - int charidx = 0; - if (p_from_selection && selection.active) { - it = selection.to; - charidx = selection.to_char + 1; + if (p_string.size() == 0) { + selection.active = false; + return false; } - while (it) { - if (it->type == ITEM_TEXT) { - ItemText *t = static_cast<ItemText *>(it); - int sp = t->text.findn(p_string, charidx); - if (sp != -1) { - selection.from = it; - selection.from_char = sp; - selection.to = it; - selection.to_char = sp + p_string.length() - 1; - selection.active = true; - update(); - - _validate_line_caches(main); + 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) { + // 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; + } + char_idx = p_search_previous ? -1 : 0; - int fh = _find_font(t).is_valid() ? _find_font(t)->get_height() : get_theme_font("normal_font")->get_height(); + // 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(); - float offset = 0; + 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); + } - int line = t->line; - Item *item = t; - while (item) { - if (item->type == ITEM_FRAME) { - ItemFrame *frame = static_cast<ItemFrame *>(item); - if (line >= 0 && line < frame->lines.size()) { - offset += frame->lines[line].height_accum_cache - frame->lines[line].height_cache; - line = frame->line; - } - } - item = item->parent; + // 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; } - vscroll->set_value(offset - fh); - - return true; } } - if (p_search_previous) { - it = _get_prev_item(it, true); - } else { - it = _get_next_item(it, true); - } - charidx = 0; + 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; + } -String RichTextLabel::get_selected_text() { - if (!selection.active || !selection.enabled) { - return ""; + 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++; } - String text; + 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; + } +} - RichTextLabel::Item *item = selection.from; +String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line, Selection p_selection) const { + String text; + ERR_FAIL_COND_V(p_frame == nullptr, text); + ERR_FAIL_COND_V(p_line < 0 || p_line >= p_frame->lines.size(), text); - while (item) { - if (item->type == ITEM_TEXT) { - String itext = static_cast<ItemText *>(item)->text; - if (item == selection.from && item == selection.to) { - text += itext.substr(selection.from_char, selection.to_char - selection.from_char + 1); - } else if (item == selection.from) { - text += itext.substr(selection.from_char, itext.size()); - } else if (item == selection.to) { - text += itext.substr(0, selection.to_char + 1); - } else { - text += itext; - } + Line &l = p_frame->lines.write[p_line]; - } else if (item->type == ITEM_NEWLINE) { - text += "\n"; + Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + int end_idx = 0; + if (it_to != nullptr) { + end_idx = it_to->index; + } else { + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + end_idx = it->index + 1; } - if (item == selection.to) { + } + for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + if ((p_selection.to_item != nullptr) && (p_selection.to_item->index < l.from->index)) { + break; + } + if ((p_selection.from_item != nullptr) && (p_selection.from_item->index >= end_idx)) { break; } + switch (it->type) { + case ITEM_NEWLINE: { + text += "\n"; + } break; + case ITEM_TEXT: { + ItemText *t = (ItemText *)it; + text += t->text; + } break; + case ITEM_IMAGE: { + text += " "; + } break; + case ITEM_TABLE: { + ItemTable *table = static_cast<ItemTable *>(it); + int idx = 0; + int col_count = table->columns.size(); + 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++) { + text += _get_line_text(frame, i, p_selection); + } + if (column == col_count - 1) { + text += "\n"; + } else { + text += " "; + } + idx++; + } + } break; + default: + break; + } + } + if ((l.from != nullptr) && (p_frame == p_selection.to_frame) && (p_selection.to_item != nullptr) && (p_selection.to_item->index >= l.from->index) && (p_selection.to_item->index < end_idx)) { + text = text.substr(0, p_selection.to_char); + } + if ((l.from != nullptr) && (p_frame == p_selection.from_frame) && (p_selection.from_item != nullptr) && (p_selection.from_item->index >= l.from->index) && (p_selection.from_item->index < end_idx)) { + text = text.substr(p_selection.from_char, -1); + } + return text; +} - item = _get_next_item(item, true); +String RichTextLabel::get_selected_text() const { + if (!selection.active || !selection.enabled) { + return ""; } + String text; + for (int i = 0; i < main->lines.size(); i++) { + text += _get_line_text(main, i, selection); + } return text; } @@ -2576,6 +3808,22 @@ bool RichTextLabel::is_selection_enabled() const { return selection.enabled; } +int RichTextLabel::get_selection_from() const { + if (!selection.active || !selection.enabled) { + return -1; + } + + return selection.from_frame->lines[selection.from_line].char_offset + selection.from_char; +} + +int RichTextLabel::get_selection_to() const { + if (!selection.active || !selection.enabled) { + return -1; + } + + return selection.to_frame->lines[selection.to_line].char_offset + selection.to_char - 1; +} + void RichTextLabel::set_bbcode(const String &p_bbcode) { bbcode = p_bbcode; if (is_inside_tree() && use_bbcode) { @@ -2596,6 +3844,7 @@ void RichTextLabel::set_use_bbcode(bool p_enable) { } use_bbcode = p_enable; set_bbcode(bbcode); + notify_property_list_changed(); } bool RichTextLabel::is_using_bbcode() const { @@ -2611,7 +3860,9 @@ String RichTextLabel::get_text() { text += t->text; } else if (it->type == ITEM_NEWLINE) { text += "\n"; - } else if (it->type == ITEM_INDENT) { + } else if (it->type == ITEM_IMAGE) { + text += " "; + } else if (it->type == ITEM_INDENT || it->type == ITEM_LIST) { text += "\t"; } it = _get_next_item(it, true); @@ -2624,18 +3875,73 @@ void RichTextLabel::set_text(const String &p_string) { add_text(p_string); } -void RichTextLabel::set_percent_visible(float p_percent) { - if (p_percent < 0 || p_percent >= 1) { - visible_characters = -1; - percent_visible = 1; +void RichTextLabel::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; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); + } +} - } else { - visible_characters = get_total_character_count() * p_percent; - percent_visible = p_percent; +void RichTextLabel::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { + if (st_parser != p_parser) { + st_parser = p_parser; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); } +} + +Control::StructuredTextParser RichTextLabel::get_structured_text_bidi_override() const { + return st_parser; +} + +void RichTextLabel::set_structured_text_bidi_override_options(Array p_args) { + st_args = p_args; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); update(); } +Array RichTextLabel::get_structured_text_bidi_override_options() const { + return st_args; +} + +Control::TextDirection RichTextLabel::get_text_direction() const { + return text_direction; +} + +void RichTextLabel::set_language(const String &p_language) { + if (language != p_language) { + language = p_language; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); + } +} + +String RichTextLabel::get_language() const { + return language; +} + +void RichTextLabel::set_percent_visible(float p_percent) { + if (percent_visible != p_percent) { + if (p_percent < 0 || p_percent >= 1) { + visible_characters = -1; + percent_visible = 1; + + } else { + visible_characters = get_total_character_count() * p_percent; + percent_visible = p_percent; + } + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(main); + update(); + } +} + float RichTextLabel::get_percent_visible() const { return percent_visible; } @@ -2647,7 +3953,9 @@ void RichTextLabel::set_effects(const Vector<Variant> &effects) { custom_effects.push_back(effect); } - parse_bbcode(bbcode); + if ((bbcode != "") && use_bbcode) { + parse_bbcode(bbcode); + } } Vector<Variant> RichTextLabel::get_effects() { @@ -2664,46 +3972,73 @@ void RichTextLabel::install_effect(const Variant effect) { if (rteffect.is_valid()) { custom_effects.push_back(effect); - parse_bbcode(bbcode); + if ((bbcode != "") && use_bbcode) { + parse_bbcode(bbcode); + } } } int RichTextLabel::get_content_height() const { int total_height = 0; if (main->lines.size()) { - total_height = main->lines[main->lines.size() - 1].height_accum_cache + get_theme_stylebox("normal")->get_minimum_size().height; + total_height = main->lines[main->lines.size() - 1].offset.y + main->lines[main->lines.size() - 1].text_buf->get_size().y; } return total_height; } +void RichTextLabel::_validate_property(PropertyInfo &property) const { + if (!use_bbcode && property.name == "bbcode_text") { + property.usage = PROPERTY_USAGE_NOEDITOR; + } +} + 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"), &RichTextLabel::add_image, DEFVAL(0), DEFVAL(0), DEFVAL(Color(1.0, 1.0, 1.0))); + 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); + ClassDB::bind_method(D_METHOD("push_font_size", "font_size"), &RichTextLabel::push_font_size); + ClassDB::bind_method(D_METHOD("push_font_features", "opentype_features"), &RichTextLabel::push_font_features); ClassDB::bind_method(D_METHOD("push_normal"), &RichTextLabel::push_normal); ClassDB::bind_method(D_METHOD("push_bold"), &RichTextLabel::push_bold); ClassDB::bind_method(D_METHOD("push_bold_italics"), &RichTextLabel::push_bold_italics); ClassDB::bind_method(D_METHOD("push_italics"), &RichTextLabel::push_italics); ClassDB::bind_method(D_METHOD("push_mono"), &RichTextLabel::push_mono); ClassDB::bind_method(D_METHOD("push_color", "color"), &RichTextLabel::push_color); - ClassDB::bind_method(D_METHOD("push_align", "align"), &RichTextLabel::push_align); + ClassDB::bind_method(D_METHOD("push_outline_size", "outline_size"), &RichTextLabel::push_outline_size); + ClassDB::bind_method(D_METHOD("push_outline_color", "color"), &RichTextLabel::push_outline_color); + ClassDB::bind_method(D_METHOD("push_paragraph", "align", "base_direction", "language", "st_parser"), &RichTextLabel::push_paragraph, DEFVAL(TextServer::DIRECTION_AUTO), DEFVAL(""), DEFVAL(STRUCTURED_TEXT_DEFAULT)); ClassDB::bind_method(D_METHOD("push_indent", "level"), &RichTextLabel::push_indent); - ClassDB::bind_method(D_METHOD("push_list", "type"), &RichTextLabel::push_list); + ClassDB::bind_method(D_METHOD("push_list", "level", "type", "capitalize"), &RichTextLabel::push_list); 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"), &RichTextLabel::push_table); + 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); + ClassDB::bind_method(D_METHOD("set_cell_border_color", "color"), &RichTextLabel::set_cell_border_color); + 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); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "parser"), &RichTextLabel::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override"), &RichTextLabel::get_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "args"), &RichTextLabel::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options"), &RichTextLabel::get_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("set_text_direction", "direction"), &RichTextLabel::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction"), &RichTextLabel::get_text_direction); + ClassDB::bind_method(D_METHOD("set_language", "language"), &RichTextLabel::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &RichTextLabel::get_language); + ClassDB::bind_method(D_METHOD("set_meta_underline", "enable"), &RichTextLabel::set_meta_underline); ClassDB::bind_method(D_METHOD("is_meta_underlined"), &RichTextLabel::is_meta_underlined); @@ -2719,6 +4054,7 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_v_scroll"), &RichTextLabel::get_v_scroll); ClassDB::bind_method(D_METHOD("scroll_to_line", "line"), &RichTextLabel::scroll_to_line); + ClassDB::bind_method(D_METHOD("scroll_to_paragraph", "paragraph"), &RichTextLabel::scroll_to_paragraph); ClassDB::bind_method(D_METHOD("set_tab_size", "spaces"), &RichTextLabel::set_tab_size); ClassDB::bind_method(D_METHOD("get_tab_size"), &RichTextLabel::get_tab_size); @@ -2729,6 +4065,11 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("set_selection_enabled", "enabled"), &RichTextLabel::set_selection_enabled); ClassDB::bind_method(D_METHOD("is_selection_enabled"), &RichTextLabel::is_selection_enabled); + ClassDB::bind_method(D_METHOD("get_selection_from"), &RichTextLabel::get_selection_from); + ClassDB::bind_method(D_METHOD("get_selection_to"), &RichTextLabel::get_selection_to); + + ClassDB::bind_method(D_METHOD("get_selected_text"), &RichTextLabel::get_selected_text); + ClassDB::bind_method(D_METHOD("parse_bbcode", "bbcode"), &RichTextLabel::parse_bbcode); ClassDB::bind_method(D_METHOD("append_bbcode", "bbcode"), &RichTextLabel::append_bbcode); @@ -2749,6 +4090,9 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_line_count"), &RichTextLabel::get_line_count); ClassDB::bind_method(D_METHOD("get_visible_line_count"), &RichTextLabel::get_visible_line_count); + ClassDB::bind_method(D_METHOD("get_paragraph_count"), &RichTextLabel::get_paragraph_count); + ClassDB::bind_method(D_METHOD("get_visible_paragraph_count"), &RichTextLabel::get_visible_paragraph_count); + ClassDB::bind_method(D_METHOD("get_content_height"), &RichTextLabel::get_content_height); ClassDB::bind_method(D_METHOD("parse_expressions_for_values", "expressions"), &RichTextLabel::parse_expressions_for_values); @@ -2776,7 +4120,14 @@ 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"); + + 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"); ADD_SIGNAL(MethodInfo("meta_clicked", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); ADD_SIGNAL(MethodInfo("meta_hover_started", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); @@ -2789,6 +4140,7 @@ void RichTextLabel::_bind_methods() { BIND_ENUM_CONSTANT(LIST_NUMBERS); BIND_ENUM_CONSTANT(LIST_LETTERS); + BIND_ENUM_CONSTANT(LIST_ROMAN); BIND_ENUM_CONSTANT(LIST_DOTS); BIND_ENUM_CONSTANT(ITEM_FRAME); @@ -2796,10 +4148,14 @@ void RichTextLabel::_bind_methods() { BIND_ENUM_CONSTANT(ITEM_IMAGE); BIND_ENUM_CONSTANT(ITEM_NEWLINE); BIND_ENUM_CONSTANT(ITEM_FONT); + BIND_ENUM_CONSTANT(ITEM_FONT_SIZE); + BIND_ENUM_CONSTANT(ITEM_FONT_FEATURES); BIND_ENUM_CONSTANT(ITEM_COLOR); + BIND_ENUM_CONSTANT(ITEM_OUTLINE_SIZE); + BIND_ENUM_CONSTANT(ITEM_OUTLINE_COLOR); BIND_ENUM_CONSTANT(ITEM_UNDERLINE); BIND_ENUM_CONSTANT(ITEM_STRIKETHROUGH); - BIND_ENUM_CONSTANT(ITEM_ALIGN); + BIND_ENUM_CONSTANT(ITEM_PARAGRAPH); BIND_ENUM_CONSTANT(ITEM_INDENT); BIND_ENUM_CONSTANT(ITEM_LIST); BIND_ENUM_CONSTANT(ITEM_TABLE); @@ -2808,12 +4164,23 @@ void RichTextLabel::_bind_methods() { BIND_ENUM_CONSTANT(ITEM_WAVE); BIND_ENUM_CONSTANT(ITEM_TORNADO); BIND_ENUM_CONSTANT(ITEM_RAINBOW); - BIND_ENUM_CONSTANT(ITEM_CUSTOMFX); + 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); } void RichTextLabel::set_visible_characters(int p_visible) { visible_characters = p_visible; + if (p_visible == -1) { + percent_visible = 1; + } else { + int total_char_count = get_total_character_count(); + if (total_char_count > 0) { + percent_visible = (float)p_visible / (float)total_char_count; + } + } update(); } @@ -2836,19 +4203,80 @@ void RichTextLabel::set_fixed_size_to_width(int p_width) { } Size2 RichTextLabel::get_minimum_size() const { - Size2 size(0, 0); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); + Size2 size = style->get_minimum_size(); + if (fixed_width != -1) { - size.x = fixed_width; + size.x += fixed_width; } if (fixed_width != -1 || fit_content_height) { const_cast<RichTextLabel *>(this)->_validate_line_caches(main); - size.y = get_content_height(); + size.y += get_content_height(); } 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()) { @@ -2929,44 +4357,19 @@ RichTextLabel::RichTextLabel() { main->lines.resize(1); main->lines.write[0].from = main; main->first_invalid_line = 0; + main->first_resized_line = 0; current_frame = main; - tab_size = 4; - default_align = ALIGN_LEFT; - underline_meta = true; - meta_hovering = nullptr; - override_selected_font_color = false; - - scroll_visible = false; - scroll_follow = false; - scroll_following = false; - updating_scroll = false; - scroll_active = true; - scroll_w = 0; - scroll_updated = false; vscroll = memnew(VScrollBar); add_child(vscroll); vscroll->set_drag_node(String("..")); vscroll->set_step(1); - vscroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 0); - vscroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); - vscroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); + vscroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_BEGIN, 0); + vscroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); + vscroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); vscroll->connect("value_changed", callable_mp(this, &RichTextLabel::_scroll_changed)); vscroll->set_step(1); vscroll->hide(); - current_idx = 1; - use_bbcode = false; - - selection.click = nullptr; - selection.active = false; - selection.enabled = false; - - visible_characters = -1; - percent_visible = 1; - visible_line_count = 0; - - fixed_width = -1; - fit_content_height = false; set_clip_contents(true); } diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index c5ed1cb3ef..ae04a7e684 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -33,13 +33,13 @@ #include "rich_text_effect.h" #include "scene/gui/scroll_bar.h" +#include "scene/resources/text_paragraph.h" class RichTextLabel : public Control { GDCLASS(RichTextLabel, Control); public: enum Align { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -47,23 +47,26 @@ public: }; enum ListType { - LIST_NUMBERS, LIST_LETTERS, + LIST_ROMAN, LIST_DOTS }; enum ItemType { - ITEM_FRAME, ITEM_TEXT, ITEM_IMAGE, ITEM_NEWLINE, ITEM_FONT, + ITEM_FONT_SIZE, + ITEM_FONT_FEATURES, ITEM_COLOR, + ITEM_OUTLINE_SIZE, + ITEM_OUTLINE_COLOR, ITEM_UNDERLINE, ITEM_STRIKETHROUGH, - ITEM_ALIGN, + ITEM_PARAGRAPH, ITEM_INDENT, ITEM_LIST, ITEM_TABLE, @@ -72,42 +75,44 @@ public: ITEM_WAVE, ITEM_TORNADO, ITEM_RAINBOW, + ITEM_BGCOLOR, + ITEM_FGCOLOR, ITEM_META, + ITEM_DROPCAP, ITEM_CUSTOMFX }; protected: + void _notification(int p_what); static void _bind_methods(); + void _validate_property(PropertyInfo &property) const override; private: struct Item; struct Line { - Item *from; - Vector<int> offset_caches; - Vector<int> height_caches; - Vector<int> ascent_caches; - Vector<int> descent_caches; - Vector<int> space_caches; - int height_cache; - int height_accum_cache; - int char_count; - int minimum_width; - int maximum_width; - - Line() { - from = nullptr; - char_count = 0; - } + Item *from = nullptr; + + Ref<TextParagraph> text_buf; + Color dc_color; + int dc_ol_size = 0; + Color dc_ol_color; + + Vector2 offset; + int char_offset = 0; + int char_count = 0; + + Line() { text_buf.instantiate(); } }; struct Item { - int index; - Item *parent; - ItemType type; + int index = 0; + int char_ofs = 0; + Item *parent = nullptr; + ItemType type = ITEM_FRAME; List<Item *> subitems; - List<Item *>::Element *E; - int line; + List<Item *>::Element *E = nullptr; + int line = 0; void _clear_children() { while (subitems.size()) { @@ -116,29 +121,26 @@ private: } } - Item() { - parent = nullptr; - E = nullptr; - line = 0; - index = 0; - type = ITEM_FRAME; - } virtual ~Item() { _clear_children(); } }; struct ItemFrame : public Item { - int parent_line; - bool cell; + bool cell = false; + Vector<Line> lines; - int first_invalid_line; - ItemFrame *parent_frame; - - ItemFrame() { - type = ITEM_FRAME; - parent_frame = nullptr; - cell = false; - parent_line = 0; - } + int first_invalid_line = 0; + int first_resized_line = 0; + + ItemFrame *parent_frame = nullptr; + + Color odd_row_bg = Color(0, 0, 0, 0); + Color even_row_bg = Color(0, 0, 0, 0); + Color border = Color(0, 0, 0, 0); + Size2 min_size_over = Size2(-1, -1); + Size2 max_size_over = Size2(-1, -1); + Rect2 padding; + + ItemFrame() { type = ITEM_FRAME; } }; struct ItemText : public Item { @@ -146,8 +148,20 @@ private: ItemText() { type = ITEM_TEXT; } }; + struct ItemDropcap : public Item { + String text; + Ref<Font> font; + int font_size = 0; + Color color; + int ol_size = 0; + Color ol_color; + Rect2 dropcap_margins; + ItemDropcap() { type = ITEM_DROPCAP; } + }; + struct ItemImage : public Item { Ref<Texture2D> image; + InlineAlign inline_align = INLINE_ALIGN_CENTER; Size2 size; Color color; ItemImage() { type = ITEM_IMAGE; } @@ -158,11 +172,31 @@ private: ItemFont() { type = ITEM_FONT; } }; + struct ItemFontSize : public Item { + int font_size = 16; + ItemFontSize() { type = ITEM_FONT_SIZE; } + }; + + struct ItemFontFeatures : public Item { + Dictionary opentype_features; + ItemFontFeatures() { type = ITEM_FONT_FEATURES; } + }; + struct ItemColor : public Item { Color color; ItemColor() { type = ITEM_COLOR; } }; + struct ItemOutlineSize : public Item { + int outline_size = 0; + ItemOutlineSize() { type = ITEM_OUTLINE_SIZE; } + }; + + struct ItemOutlineColor : public Item { + Color color; + ItemOutlineColor() { type = ITEM_OUTLINE_COLOR; } + }; + struct ItemUnderline : public Item { ItemUnderline() { type = ITEM_UNDERLINE; } }; @@ -176,18 +210,23 @@ private: ItemMeta() { type = ITEM_META; } }; - struct ItemAlign : public Item { - Align align; - ItemAlign() { type = ITEM_ALIGN; } + struct ItemParagraph : public Item { + Align align = ALIGN_LEFT; + String language; + Control::TextDirection direction = Control::TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + ItemParagraph() { type = ITEM_PARAGRAPH; } }; struct ItemIndent : public Item { - int level; + int level = 0; ItemIndent() { type = ITEM_INDENT; } }; struct ItemList : public Item { - ListType list_type; + ListType list_type = LIST_DOTS; + bool capitalize = false; + int level = 0; ItemList() { type = ITEM_LIST; } }; @@ -197,45 +236,40 @@ private: struct ItemTable : public Item { struct Column { - bool expand; - int expand_ratio; - int min_width; - int max_width; - int width; + bool expand = false; + int expand_ratio = 0; + int min_width = 0; + int max_width = 0; + int width = 0; }; Vector<Column> columns; - int total_width; + Vector<float> rows; + + int total_width = 0; + int total_height = 0; + InlineAlign inline_align = INLINE_ALIGN_TOP; ItemTable() { type = ITEM_TABLE; } }; struct ItemFade : public Item { - int starting_index; - int length; + int starting_index = 0; + int length = 0; ItemFade() { type = ITEM_FADE; } }; struct ItemFX : public Item { - float elapsed_time; - - ItemFX() { - elapsed_time = 0.0f; - } + double elapsed_time = 0.f; }; struct ItemShake : public ItemFX { - int strength; - float rate; - uint64_t _current_rng; - uint64_t _previous_rng; - - ItemShake() { - strength = 0; - rate = 0.0f; - _current_rng = 0; - type = ITEM_SHAKE; - } + int strength = 0; + float rate = 0.0f; + uint64_t _current_rng = 0; + uint64_t _previous_rng = 0; + + ItemShake() { type = ITEM_SHAKE; } void reroll_random() { _previous_rng = _current_rng; @@ -254,38 +288,35 @@ private: }; struct ItemWave : public ItemFX { - float frequency; - float amplitude; + float frequency = 1.0f; + float amplitude = 1.0f; - ItemWave() { - frequency = 1.0f; - amplitude = 1.0f; - type = ITEM_WAVE; - } + ItemWave() { type = ITEM_WAVE; } }; struct ItemTornado : public ItemFX { - float radius; - float frequency; + float radius = 1.0f; + float frequency = 1.0f; - ItemTornado() { - radius = 1.0f; - frequency = 1.0f; - type = ITEM_TORNADO; - } + ItemTornado() { type = ITEM_TORNADO; } }; struct ItemRainbow : public ItemFX { - float saturation; - float value; - float frequency; - - ItemRainbow() { - saturation = 0.8f; - value = 0.8f; - frequency = 1.0f; - type = ITEM_RAINBOW; - } + float saturation = 0.8f; + float value = 0.8f; + float frequency = 1.0f; + + 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 { @@ -294,8 +325,7 @@ private: ItemCustomFX() { type = ITEM_CUSTOMFX; - - char_fx_transform.instance(); + char_fx_transform.instantiate(); } virtual ~ItemCustomFX() { @@ -306,29 +336,31 @@ private: } }; - ItemFrame *main; - Item *current; - ItemFrame *current_frame; + ItemFrame *main = nullptr; + Item *current = nullptr; + ItemFrame *current_frame = nullptr; - VScrollBar *vscroll; + VScrollBar *vscroll = nullptr; - bool scroll_visible; - bool scroll_follow; - bool scroll_following; - bool scroll_active; - int scroll_w; - bool scroll_updated; - bool updating_scroll; - int current_idx; - int visible_line_count; + bool scroll_visible = false; + bool scroll_follow = false; + bool scroll_following = false; + bool scroll_active = true; + int scroll_w = 0; + bool scroll_updated = false; + bool updating_scroll = false; + int current_idx = 1; + int current_char_ofs = 0; + int visible_paragraph_count = 0; + int visible_line_count = 0; - int tab_size; - bool underline_meta; - bool override_selected_font_color; + int tab_size = 4; + bool underline_meta = true; + bool override_selected_font_color = false; - Align default_align; + Align default_align = ALIGN_LEFT; - ItemMeta *meta_hovering; + ItemMeta *meta_hovering = nullptr; Variant current_meta; Vector<Ref<RichTextEffect>> custom_effects; @@ -339,102 +371,133 @@ private: void _add_item(Item *p_item, bool p_enter = false, bool p_ensure_newline = false); void _remove_item(Item *p_item, const int p_line, const int p_subitem_line); - struct ProcessState { - int line_width; - }; - - enum ProcessMode { + String language; + TextDirection text_direction = TEXT_DIRECTION_AUTO; + Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + Array st_args; - PROCESS_CACHE, - PROCESS_DRAW, - PROCESS_POINTER + struct Selection { + ItemFrame *click_frame = nullptr; + int click_line = 0; + Item *click_item = nullptr; + int click_char = 0; + + ItemFrame *from_frame = nullptr; + int from_line = 0; + Item *from_item = nullptr; + int from_char = 0; + + ItemFrame *to_frame = nullptr; + int to_line = 0; + Item *to_item = nullptr; + int to_char = 0; + + bool active = false; // anything selected? i.e. from, to, etc. valid? + bool enabled = false; // allow selections? }; - struct Selection { - Item *click; - int click_char; + Selection selection; - Item *from; - int from_char; - Item *to; - int to_char; + int visible_characters = -1; + float percent_visible = 1.0; - bool active; // anything selected? i.e. from, to, etc. valid? - bool enabled; // allow selections? - }; + 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); - Selection selection; + 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, 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); - int visible_characters; - float percent_visible; + 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); + int _draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, const Color &p_base_color, int p_outline_size, const Color &p_outline_color, const Color &p_font_shadow_color, bool p_shadow_as_outline, const Point2 &shadow_ofs); + float _find_click_in_line(ItemFrame *p_frame, int p_line, const Vector2 &p_ofs, int p_width, const Point2i &p_click, ItemFrame **r_click_frame = nullptr, int *r_click_line = nullptr, Item **r_click_item = nullptr, int *r_click_char = nullptr); - int _process_line(ItemFrame *p_frame, const Vector2 &p_ofs, int &y, int p_width, int p_line, ProcessMode p_mode, const Ref<Font> &p_base_font, const Color &p_base_color, const Color &p_font_color_shadow, bool p_shadow_as_outline, const Point2 &shadow_ofs, const Point2i &p_click_pos = Point2i(), Item **r_click_item = nullptr, int *r_click_char = nullptr, bool *r_outside = nullptr, int p_char_count = 0); - void _find_click(ItemFrame *p_frame, const Point2i &p_click, Item **r_click_item = nullptr, int *r_click_char = nullptr, bool *r_outside = nullptr); + String _roman(int p_num, bool p_capitalize) const; + String _letters(int p_num, bool p_capitalize) const; + Item *_get_item_at_pos(Item *p_item_from, Item *p_item_to, int p_position); + void _find_frame(Item *p_item, ItemFrame **r_frame, int *r_line); Ref<Font> _find_font(Item *p_item); - int _find_margin(Item *p_item, const Ref<Font> &p_base_font); + int _find_font_size(Item *p_item); + Dictionary _find_font_features(Item *p_item); + int _find_outline_size(Item *p_item, int p_default); + ItemList *_find_list_item(Item *p_item); + ItemDropcap *_find_dc_item(Item *p_item); + int _find_list(Item *p_item, Vector<int> &r_index, Vector<ItemList *> &r_list); + int _find_margin(Item *p_item, const Ref<Font> &p_base_font, int p_base_font_size); Align _find_align(Item *p_item); + TextServer::Direction _find_direction(Item *p_item); + Control::StructuredTextParser _find_stt(Item *p_item); + String _find_language(Item *p_item); Color _find_color(Item *p_item, const Color &p_default_color); + Color _find_outline_color(Item *p_item, const Color &p_default_color); 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); - bool _find_by_type(Item *p_item, ItemType p_type); void _fetch_item_fx_stack(Item *p_item, Vector<ItemFX *> &r_stack); - static Color _get_color_from_string(const String &p_color_str, const Color &p_default_color); - 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); - Item *_get_next_item(Item *p_item, bool p_free = false); - Item *_get_prev_item(Item *p_item, bool p_free = false); + 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; Rect2 _get_text_rect(); Ref<RichTextEffect> _get_custom_effect_by_code(String p_bbcode_identifier); virtual Dictionary parse_expressions_for_values(Vector<String> p_expressions); - bool use_bbcode; - String bbcode; + 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); - void _update_all_lines(); + bool use_bbcode = false; + String bbcode; - int fixed_width; + int fixed_width = -1; - bool fit_content_height; - -protected: - void _notification(int p_what); + bool fit_content_height = false; 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)); + 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)); void push_font(const Ref<Font> &p_font); + void push_font_size(int p_font_size); + void push_font_features(const Dictionary &p_features); + void push_outline_size(int p_font_size); void push_normal(); void push_bold(); void push_bold_italics(); void push_italics(); void push_mono(); void push_color(const Color &p_color); + void push_outline_color(const Color &p_color); void push_underline(); void push_strikethrough(); - void push_align(Align p_align); + void push_paragraph(Align p_align, Control::TextDirection p_direction = Control::TEXT_DIRECTION_INHERITED, const String &p_language = "", Control::StructuredTextParser p_st_parser = STRUCTURED_TEXT_DEFAULT); void push_indent(int p_level); - void push_list(ListType p_list); + 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); + 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); + void set_cell_border_color(const Color &p_color); + void set_cell_size_override(const Size2 &p_min_size, const Size2 &p_max_size); + void set_cell_padding(const Rect2 &p_padding); int get_current_table_column() const; void push_cell(); void pop(); @@ -463,6 +526,10 @@ public: bool search(const String &p_string, bool p_from_selection = false, bool p_search_previous = false); + void scroll_to_paragraph(int p_paragraph); + int get_paragraph_count() const; + int get_visible_paragraph_count() const; + void scroll_to_line(int p_line); int get_line_count() const; int get_visible_line_count() const; @@ -475,7 +542,9 @@ public: void set_selection_enabled(bool p_enabled); bool is_selection_enabled() const; - String get_selected_text(); + int get_selection_from() const; + int get_selection_to() const; + String get_selected_text() const; void selection_copy(); Error parse_bbcode(const String &p_bbcode); @@ -489,6 +558,18 @@ public: void set_text(const String &p_string); + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; + + void set_language(const String &p_language); + String get_language() 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_visible_characters(int p_visible); int get_visible_characters() const; int get_total_character_count() const; diff --git a/scene/gui/scroll_bar.cpp b/scene/gui/scroll_bar.cpp index 9340d98ede..08bcb0bdda 100644 --- a/scene/gui/scroll_bar.cpp +++ b/scene/gui/scroll_bar.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -41,10 +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; @@ -52,24 +54,24 @@ void ScrollBar::_gui_input(Ref<InputEvent> p_event) { if (b.is_valid()) { accept_event(); - if (b->get_button_index() == BUTTON_WHEEL_DOWN && b->is_pressed()) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && b->is_pressed()) { set_value(get_value() + get_page() / 4.0); accept_event(); } - if (b->get_button_index() == BUTTON_WHEEL_UP && b->is_pressed()) { + if (b->get_button_index() == MOUSE_BUTTON_WHEEL_UP && b->is_pressed()) { set_value(get_value() - get_page() / 4.0); accept_event(); } - if (b->get_button_index() != BUTTON_LEFT) { + if (b->get_button_index() != MOUSE_BUTTON_LEFT) { return; } 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(); @@ -138,7 +140,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; @@ -148,8 +150,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(); @@ -213,17 +215,17 @@ 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 = highlight == HIGHLIGHT_DECR ? get_theme_icon(SNAME("decrement_highlight")) : get_theme_icon(SNAME("decrement")); + Ref<Texture2D> incr = highlight == HIGHLIGHT_INCR ? get_theme_icon(SNAME("increment_highlight")) : 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; @@ -259,11 +261,11 @@ void ScrollBar::_notification(int p_what) { grabber_rect.size.width = get_grabber_size(); grabber_rect.size.height = get_size().height; grabber_rect.position.y = 0; - grabber_rect.position.x = get_grabber_offset() + decr->get_width() + bg->get_margin(MARGIN_LEFT); + grabber_rect.position.x = get_grabber_offset() + decr->get_width() + bg->get_margin(SIDE_LEFT); } else { grabber_rect.size.width = get_size().width; grabber_rect.size.height = get_grabber_size(); - grabber_rect.position.y = get_grabber_offset() + decr->get_height() + bg->get_margin(MARGIN_TOP); + grabber_rect.position.y = get_grabber_offset() + decr->get_height() + bg->get_margin(SIDE_TOP); grabber_rect.position.x = 0; } @@ -387,7 +389,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; } @@ -413,17 +415,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; @@ -434,41 +436,29 @@ double ScrollBar::get_area_size() const { } double ScrollBar::get_area_offset() const { - double ofs = 0; + double ofs = 0.0; if (orientation == VERTICAL) { - ofs += get_theme_stylebox("hscroll")->get_margin(MARGIN_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(MARGIN_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; } -double ScrollBar::get_click_pos(const Point2 &p_pos) const { - float pos = (orientation == VERTICAL) ? p_pos.y : p_pos.x; - pos -= get_area_offset(); - - float area = get_area_size(); - if (area == 0) { - return 0; - } else { - return pos / area; - } -} - double ScrollBar::get_grabber_offset() const { return (get_area_size()) * get_as_ratio(); } 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) { @@ -607,7 +597,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 6ae76e453a..fbc035397f 100644 --- a/scene/gui/scroll_bar.h +++ b/scene/gui/scroll_bar.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -47,21 +47,20 @@ class ScrollBar : public Range { Orientation orientation; Size2 size; - float custom_step = -1; + float custom_step = -1.0; HighlightStatus highlight = HIGHLIGHT_NONE; struct Drag { bool active = false; - float pos_at_click; - float value_at_click; + float pos_at_click = 0.0; + float value_at_click = 0.0; } drag; double get_grabber_size() const; double get_grabber_min_size() const; double get_area_size() const; double get_area_offset() const; - double get_click_pos(const Point2 &p_pos) const; double get_grabber_offset() const; static void set_can_focus_by_default(bool p_can_focus); @@ -74,20 +73,20 @@ class ScrollBar : public Range { Vector2 drag_node_accum; Vector2 drag_node_from; Vector2 last_drag_node_accum; - float last_drag_node_time; - float time_since_motion; + float last_drag_node_time = 0.0; + float time_since_motion = 0.0; bool drag_node_touching = false; bool drag_node_touching_deaccel = false; - bool click_handled; + bool click_handled = false; bool scrolling = false; - double target_scroll = 0; + double target_scroll = 0.0; bool smooth_scroll_enabled = false; 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 f4e31c45d2..b5daa8b3f1 100644 --- a/scene/gui/scroll_container.cpp +++ b/scene/gui/scroll_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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,44 +77,46 @@ 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(); double prev_h_scroll = h_scroll->get_value(); Ref<InputEventMouseButton> mb = p_gui_input; if (mb.is_valid()) { - if (mb->get_button_index() == BUTTON_WHEEL_UP && mb->is_pressed()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && mb->is_pressed()) { // only horizontal is enabled, scroll horizontally - if (h_scroll->is_visible() && (!v_scroll->is_visible() || mb->get_shift())) { + if (h_scroll->is_visible() && (!v_scroll->is_visible() || mb->is_shift_pressed())) { h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() / 8 * mb->get_factor()); } else if (v_scroll->is_visible_in_tree()) { v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() / 8 * mb->get_factor()); } } - if (mb->get_button_index() == BUTTON_WHEEL_DOWN && mb->is_pressed()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && mb->is_pressed()) { // only horizontal is enabled, scroll horizontally - if (h_scroll->is_visible() && (!v_scroll->is_visible() || mb->get_shift())) { + if (h_scroll->is_visible() && (!v_scroll->is_visible() || mb->is_shift_pressed())) { h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() / 8 * mb->get_factor()); } else if (v_scroll->is_visible()) { v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() / 8 * mb->get_factor()); } } - if (mb->get_button_index() == BUTTON_WHEEL_LEFT && mb->is_pressed()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_LEFT && mb->is_pressed()) { if (h_scroll->is_visible_in_tree()) { h_scroll->set_value(h_scroll->get_value() - h_scroll->get_page() * mb->get_factor() / 8); } } - if (mb->get_button_index() == BUTTON_WHEEL_RIGHT && mb->is_pressed()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_RIGHT && mb->is_pressed()) { if (h_scroll->is_visible_in_tree()) { h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * mb->get_factor() / 8); } @@ -132,7 +130,7 @@ void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { return; } - if (mb->get_button_index() != BUTTON_LEFT) { + if (mb->get_button_index() != MOUSE_BUTTON_LEFT) { return; } @@ -175,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 @@ -213,114 +211,128 @@ void ScrollContainer::_gui_input(const Ref<InputEvent> &p_gui_input) { } void ScrollContainer::_update_scrollbar_position() { + if (!_updating_scrollbars) { + return; + } + Size2 hmin = h_scroll->get_combined_minimum_size(); Size2 vmin = v_scroll->get_combined_minimum_size(); - h_scroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_BEGIN, 0); - h_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - h_scroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_END, -hmin.height); - h_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + h_scroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_BEGIN, 0); + h_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + h_scroll->set_anchor_and_offset(SIDE_TOP, ANCHOR_END, -hmin.height); + h_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, 0); - v_scroll->set_anchor_and_margin(MARGIN_LEFT, ANCHOR_END, -vmin.width); - v_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, 0); - v_scroll->set_anchor_and_margin(MARGIN_TOP, ANCHOR_BEGIN, 0); - v_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, 0); + v_scroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -vmin.width); + v_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, 0); + 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; - if (v_scroll->is_visible()) { - right_margin += v_scroll->get_size().x; - } - float bottom_margin = 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)); - 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::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED) { - call_deferred("_update_scrollbar_position"); - }; +void ScrollContainer::_update_dimensions() { + child_max_size = Size2(0, 0); + Size2 size = get_size(); + Point2 ofs; - if (p_what == NOTIFICATION_READY) { - get_viewport()->connect("gui_focus_changed", callable_mp(this, &ScrollContainer::_ensure_focused_visible)); - } + Ref<StyleBox> sb = get_theme_stylebox(SNAME("bg")); + size -= sb->get_minimum_size(); + ofs += sb->get_offset(); + bool rtl = is_layout_rtl(); - if (p_what == NOTIFICATION_SORT_CHILDREN) { - child_max_size = Size2(0, 0); - Size2 size = get_size(); - Point2 ofs; + if (h_scroll->is_visible_in_tree() && h_scroll->get_parent() == this) { //scrolls may have been moved out for reasons + size.y -= h_scroll->get_minimum_size().y; + } - Ref<StyleBox> sb = get_theme_stylebox("bg"); - size -= sb->get_minimum_size(); - ofs += sb->get_offset(); + if (v_scroll->is_visible_in_tree() && v_scroll->get_parent() == this) { //scrolls may have been moved out for reasons + size.x -= v_scroll->get_minimum_size().x; + } - if (h_scroll->is_visible_in_tree() && h_scroll->get_parent() == this) { //scrolls may have been moved out for reasons - size.y -= h_scroll->get_minimum_size().y; + for (int i = 0; i < get_child_count(); i++) { + Control *c = Object::cast_to<Control>(get_child(i)); + if (!c) { + continue; } - - if (v_scroll->is_visible_in_tree() && v_scroll->get_parent() == this) { //scrolls may have been moved out for reasons - size.x -= v_scroll->get_minimum_size().x; + if (c->is_set_as_top_level()) { + continue; } - - for (int i = 0; i < get_child_count(); i++) { - Control *c = Object::cast_to<Control>(get_child(i)); - if (!c) { - continue; - } - if (c->is_set_as_top_level()) { - continue; - } - if (c == h_scroll || c == v_scroll) { - continue; - } - Size2 minsize = c->get_combined_minimum_size(); - child_max_size.x = MAX(child_max_size.x, minsize.x); - child_max_size.y = MAX(child_max_size.y, minsize.y); - - Rect2 r = Rect2(-scroll, minsize); - if (!scroll_h || (!h_scroll->is_visible_in_tree() && c->get_h_size_flags() & SIZE_EXPAND)) { - r.position.x = 0; - if (c->get_h_size_flags() & SIZE_EXPAND) { - r.size.width = MAX(size.width, minsize.width); - } else { - r.size.width = minsize.width; - } + if (c == h_scroll || c == v_scroll) { + continue; + } + Size2 minsize = c->get_combined_minimum_size(); + child_max_size.x = MAX(child_max_size.x, minsize.x); + child_max_size.y = MAX(child_max_size.y, minsize.y); + + Rect2 r = Rect2(-Size2(get_h_scroll(), get_v_scroll()), minsize); + if (!scroll_h || (!h_scroll->is_visible_in_tree() && c->get_h_size_flags() & SIZE_EXPAND)) { + r.position.x = 0; + if (c->get_h_size_flags() & SIZE_EXPAND) { + r.size.width = MAX(size.width, minsize.width); + } else { + r.size.width = minsize.width; } - if (!scroll_v || (!v_scroll->is_visible_in_tree() && c->get_v_size_flags() & SIZE_EXPAND)) { - r.position.y = 0; - if (c->get_v_size_flags() & SIZE_EXPAND) { - r.size.height = MAX(size.height, minsize.height); - } else { - r.size.height = minsize.height; - } + } + if (!scroll_v || (!v_scroll->is_visible_in_tree() && c->get_v_size_flags() & SIZE_EXPAND)) { + r.position.y = 0; + if (c->get_v_size_flags() & SIZE_EXPAND) { + r.size.height = MAX(size.height, minsize.height); + } else { + r.size.height = minsize.height; } - r.position += ofs; - fit_child_in_rect(c, r); } + r.position += ofs; + if (rtl && v_scroll->is_visible_in_tree() && v_scroll->get_parent() == this) { + r.position.x += v_scroll->get_minimum_size().x; + } + r.position = r.position.floor(); + fit_child_in_rect(c, r); + } - update(); + update(); +} + +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(SNAME("_update_scrollbar_position")); + }; + + if (p_what == NOTIFICATION_READY) { + get_viewport()->connect("gui_focus_changed", callable_mp(this, &ScrollContainer::_gui_focus_changed)); + _update_dimensions(); + } + + if (p_what == NOTIFICATION_SORT_CHILDREN) { + _update_dimensions(); }; 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(); @@ -397,7 +409,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; @@ -411,52 +423,45 @@ void ScrollContainer::update_scrollbars() { Size2 min = child_max_size; - bool hide_scroll_v = !scroll_v || min.height <= size.height; - bool hide_scroll_h = !scroll_h || min.width <= size.width; - - if (hide_scroll_v) { - v_scroll->hide(); - scroll.y = 0; - } else { - v_scroll->show(); - v_scroll->set_max(min.height); - if (hide_scroll_h) { - v_scroll->set_page(size.height); - } else { - v_scroll->set_page(size.height - hmin.height); - } + bool hide_scroll_h = !scroll_h || min.width <= size.width || !h_scroll_visible; + bool hide_scroll_v = !scroll_v || min.height <= size.height || !v_scroll_visible; - scroll.y = v_scroll->get_value(); - } + h_scroll->set_max(min.width); + h_scroll->set_page(size.width - (hide_scroll_v ? 0 : vmin.width)); + h_scroll->set_visible(!hide_scroll_h); - if (hide_scroll_h) { - h_scroll->hide(); - scroll.x = 0; - } else { - h_scroll->show(); - h_scroll->set_max(min.width); - if (hide_scroll_v) { - h_scroll->set_page(size.width); - } else { - h_scroll->set_page(size.width - vmin.width); - } - - scroll.x = h_scroll->get_value(); - } + v_scroll->set_max(min.height); + v_scroll->set_page(size.height - (hide_scroll_h ? 0 : hmin.height)); + v_scroll->set_visible(!hide_scroll_v); // Avoid scrollbar overlapping. - h_scroll->set_anchor_and_margin(MARGIN_RIGHT, ANCHOR_END, hide_scroll_v ? 0 : -vmin.width); - v_scroll->set_anchor_and_margin(MARGIN_BOTTOM, ANCHOR_END, hide_scroll_h ? 0 : -hmin.height); + h_scroll->set_anchor_and_offset(SIDE_RIGHT, ANCHOR_END, hide_scroll_v ? 0 : -vmin.width); + v_scroll->set_anchor_and_offset(SIDE_BOTTOM, ANCHOR_END, hide_scroll_h ? 0 : -hmin.height); } void ScrollContainer::_scroll_moved(float) { - scroll.x = h_scroll->get_value(); - scroll.y = v_scroll->get_value(); queue_sort(); - update(); }; +void ScrollContainer::set_h_scroll(int p_pos) { + h_scroll->set_value(p_pos); + _cancel_drag(); +} + +int ScrollContainer::get_h_scroll() const { + return h_scroll->get_value(); +} + +void ScrollContainer::set_v_scroll(int p_pos) { + v_scroll->set_value(p_pos); + _cancel_drag(); +} + +int ScrollContainer::get_v_scroll() const { + return v_scroll->get_value(); +} + void ScrollContainer::set_enable_h_scroll(bool p_enable) { if (scroll_h == p_enable) { return; @@ -485,22 +490,30 @@ bool ScrollContainer::is_v_scroll_enabled() const { return scroll_v; } -int ScrollContainer::get_v_scroll() const { - return v_scroll->get_value(); +void ScrollContainer::set_h_scroll_visible(bool p_visible) { + if (h_scroll_visible == p_visible) { + return; + } + + h_scroll_visible = p_visible; + update_scrollbars(); } -void ScrollContainer::set_v_scroll(int p_pos) { - v_scroll->set_value(p_pos); - _cancel_drag(); +bool ScrollContainer::is_h_scroll_visible() const { + return h_scroll_visible; } -int ScrollContainer::get_h_scroll() const { - return h_scroll->get_value(); +void ScrollContainer::set_v_scroll_visible(bool p_visible) { + if (v_scroll_visible == p_visible) { + return; + } + + v_scroll_visible = p_visible; + update_scrollbars(); } -void ScrollContainer::set_h_scroll(int p_pos) { - h_scroll->set_value(p_pos); - _cancel_drag(); +bool ScrollContainer::is_v_scroll_visible() const { + return v_scroll_visible; } int ScrollContainer::get_deadzone() const { @@ -519,8 +532,8 @@ void ScrollContainer::set_follow_focus(bool p_follow) { follow_focus = p_follow; } -String ScrollContainer::get_configuration_warning() const { - String warning = Container::get_configuration_warning(); +TypedArray<String> ScrollContainer::get_configuration_warnings() const { + TypedArray<String> warnings = Container::get_configuration_warnings(); int found = 0; @@ -540,12 +553,10 @@ String ScrollContainer::get_configuration_warning() const { } if (found != 1) { - if (!warning.empty()) { - warning += "\n\n"; - } - warning += TTR("ScrollContainer is intended to work with a single child control.\nUse a container as child (VBox, HBox, etc.), or a Control and set the custom minimum size manually."); + warnings.push_back(TTR("ScrollContainer is intended to work with a single child control.\nUse a container as child (VBox, HBox, etc.), or a Control and set the custom minimum size manually.")); } - return warning; + + return warnings; } HScrollBar *ScrollContainer::get_h_scrollbar() { @@ -557,23 +568,35 @@ VScrollBar *ScrollContainer::get_v_scrollbar() { } void ScrollContainer::_bind_methods() { - ClassDB::bind_method(D_METHOD("_gui_input"), &ScrollContainer::_gui_input); - ClassDB::bind_method(D_METHOD("set_enable_h_scroll", "enable"), &ScrollContainer::set_enable_h_scroll); - ClassDB::bind_method(D_METHOD("is_h_scroll_enabled"), &ScrollContainer::is_h_scroll_enabled); - ClassDB::bind_method(D_METHOD("set_enable_v_scroll", "enable"), &ScrollContainer::set_enable_v_scroll); - ClassDB::bind_method(D_METHOD("is_v_scroll_enabled"), &ScrollContainer::is_v_scroll_enabled); 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); ClassDB::bind_method(D_METHOD("get_h_scroll"), &ScrollContainer::get_h_scroll); + ClassDB::bind_method(D_METHOD("set_v_scroll", "value"), &ScrollContainer::set_v_scroll); ClassDB::bind_method(D_METHOD("get_v_scroll"), &ScrollContainer::get_v_scroll); + + ClassDB::bind_method(D_METHOD("set_enable_h_scroll", "enable"), &ScrollContainer::set_enable_h_scroll); + ClassDB::bind_method(D_METHOD("is_h_scroll_enabled"), &ScrollContainer::is_h_scroll_enabled); + + ClassDB::bind_method(D_METHOD("set_enable_v_scroll", "enable"), &ScrollContainer::set_enable_v_scroll); + ClassDB::bind_method(D_METHOD("is_v_scroll_enabled"), &ScrollContainer::is_v_scroll_enabled); + + ClassDB::bind_method(D_METHOD("set_h_scroll_visible", "visible"), &ScrollContainer::set_h_scroll_visible); + ClassDB::bind_method(D_METHOD("is_h_scroll_visible"), &ScrollContainer::is_h_scroll_visible); + + ClassDB::bind_method(D_METHOD("set_v_scroll_visible", "visible"), &ScrollContainer::set_v_scroll_visible); + ClassDB::bind_method(D_METHOD("is_v_scroll_visible"), &ScrollContainer::is_v_scroll_visible); + ClassDB::bind_method(D_METHOD("set_deadzone", "deadzone"), &ScrollContainer::set_deadzone); ClassDB::bind_method(D_METHOD("get_deadzone"), &ScrollContainer::get_deadzone); + ClassDB::bind_method(D_METHOD("set_follow_focus", "enabled"), &ScrollContainer::set_follow_focus); ClassDB::bind_method(D_METHOD("is_following_focus"), &ScrollContainer::is_following_focus); 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")); @@ -581,10 +604,12 @@ void ScrollContainer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "follow_focus"), "set_follow_focus", "is_following_focus"); ADD_GROUP("Scroll", "scroll_"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_horizontal_enabled"), "set_enable_h_scroll", "is_h_scroll_enabled"); ADD_PROPERTY(PropertyInfo(Variant::INT, "scroll_horizontal"), "set_h_scroll", "get_h_scroll"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_vertical_enabled"), "set_enable_v_scroll", "is_v_scroll_enabled"); ADD_PROPERTY(PropertyInfo(Variant::INT, "scroll_vertical"), "set_v_scroll", "get_v_scroll"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_horizontal_enabled"), "set_enable_h_scroll", "is_h_scroll_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_vertical_enabled"), "set_enable_v_scroll", "is_v_scroll_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_horizontal_visible"), "set_h_scroll_visible", "is_h_scroll_visible"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_vertical_visible"), "set_v_scroll_visible", "is_v_scroll_visible"); ADD_PROPERTY(PropertyInfo(Variant::INT, "scroll_deadzone"), "set_deadzone", "get_deadzone"); GLOBAL_DEF("gui/common/default_scroll_deadzone", 0); @@ -601,15 +626,7 @@ ScrollContainer::ScrollContainer() { add_child(v_scroll); v_scroll->connect("value_changed", callable_mp(this, &ScrollContainer::_scroll_moved)); - drag_speed = Vector2(); - drag_touching = false; - drag_touching_deaccel = false; - beyond_deadzone = false; - scroll_h = true; - scroll_v = true; - deadzone = GLOBAL_GET("gui/common/default_scroll_deadzone"); - follow_focus = false; set_clip_contents(true); }; diff --git a/scene/gui/scroll_container.h b/scene/gui/scroll_container.h index b28d66ed53..9f4ec558dc 100644 --- a/scene/gui/scroll_container.h +++ b/scene/gui/scroll_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -42,7 +42,6 @@ class ScrollContainer : public Container { VScrollBar *v_scroll; Size2 child_max_size; - Size2 scroll; void update_scrollbars(); @@ -50,39 +49,42 @@ class ScrollContainer : public Container { Vector2 drag_accum; Vector2 drag_from; Vector2 last_drag_accum; - float last_drag_time; - float time_since_motion; - bool drag_touching; - bool drag_touching_deaccel; - bool click_handled; - bool beyond_deadzone; + float time_since_motion = 0.0f; + bool drag_touching = false; + bool drag_touching_deaccel = false; + bool beyond_deadzone = false; - bool scroll_h; - bool scroll_v; + bool scroll_h = true; + bool scroll_v = true; - int deadzone; - bool follow_focus; + bool h_scroll_visible = true; + bool v_scroll_visible = true; + + int deadzone = 0; + bool follow_focus = false; void _cancel_drag(); 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); void _scroll_moved(float); static void _bind_methods(); + bool _updating_scrollbars = false; void _update_scrollbar_position(); - void _ensure_focused_visible(Control *p_node); public: - int get_v_scroll() const; - void set_v_scroll(int p_pos); - - int get_h_scroll() const; void set_h_scroll(int p_pos); + int get_h_scroll() const; + + void set_v_scroll(int p_pos); + int get_v_scroll() const; void set_enable_h_scroll(bool p_enable); bool is_h_scroll_enabled() const; @@ -90,6 +92,12 @@ public: void set_enable_v_scroll(bool p_enable); bool is_v_scroll_enabled() const; + void set_h_scroll_visible(bool p_visible); + bool is_h_scroll_visible() const; + + void set_v_scroll_visible(bool p_visible); + bool is_v_scroll_visible() const; + int get_deadzone() const; void set_deadzone(int p_deadzone); @@ -98,10 +106,9 @@ public: HScrollBar *get_h_scrollbar(); VScrollBar *get_v_scrollbar(); + void ensure_control_visible(Control *p_control); - virtual bool clips_input() const override; - - virtual String get_configuration_warning() const override; + TypedArray<String> get_configuration_warnings() const override; ScrollContainer(); }; diff --git a/scene/gui/separator.cpp b/scene/gui/separator.cpp index 4f7e5720b8..1f3cb7aa24 100644 --- a/scene/gui/separator.cpp +++ b/scene/gui/separator.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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/separator.h b/scene/gui/separator.h index eec989cfea..77162c68fa 100644 --- a/scene/gui/separator.h +++ b/scene/gui/separator.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,7 +36,7 @@ class Separator : public Control { GDCLASS(Separator, Control); protected: - Orientation orientation; + Orientation orientation = Orientation::HORIZONTAL; void _notification(int p_what); public: diff --git a/scene/gui/shortcut.cpp b/scene/gui/shortcut.cpp deleted file mode 100644 index f8c7bc44a7..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-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 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/slider.cpp b/scene/gui/slider.cpp index 3dd5e022f0..352f87954e 100644 --- a/scene/gui/slider.cpp +++ b/scene/gui/slider.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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,9 @@ 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) { return; } @@ -53,7 +55,7 @@ void Slider::_gui_input(Ref<InputEvent> p_event) { Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid()) { - if (mb->get_button_index() == BUTTON_LEFT) { + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (mb->is_pressed()) { Ref<Texture2D> grabber = get_theme_icon(mouse_inside || has_focus() ? "grabber_highlight" : "grabber"); grab.pos = orientation == VERTICAL ? mb->get_position().y : mb->get_position().x; @@ -72,9 +74,11 @@ void Slider::_gui_input(Ref<InputEvent> p_event) { grab.active = false; } } else if (scrollable) { - if (mb->is_pressed() && mb->get_button_index() == BUTTON_WHEEL_UP) { + if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP) { + grab_focus(); set_value(get_value() + get_step()); - } else if (mb->is_pressed() && mb->get_button_index() == BUTTON_WHEEL_DOWN) { + } else if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN) { + grab_focus(); set_value(get_value() - get_step()); } } @@ -85,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; @@ -157,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); @@ -249,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); @@ -269,12 +272,5 @@ void Slider::_bind_methods() { Slider::Slider(Orientation p_orientation) { orientation = p_orientation; - mouse_inside = false; - grab.active = false; - ticks = 0; - ticks_on_borders = false; - custom_step = -1; - editable = true; - scrollable = true; set_focus_mode(FOCUS_ALL); } diff --git a/scene/gui/slider.h b/scene/gui/slider.h index b4b56f2856..46fa08bbf0 100644 --- a/scene/gui/slider.h +++ b/scene/gui/slider.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -37,23 +37,23 @@ class Slider : public Range { GDCLASS(Slider, Range); struct Grab { - int pos; - float uvalue; - bool active; + int pos = 0; + float uvalue = 0.0; + bool active = false; } grab; - int ticks; - bool mouse_inside; + int ticks = 0; + bool mouse_inside = false; Orientation orientation; - float custom_step; - bool editable; - bool scrollable; + float custom_step = -1.0; + bool editable = true; + 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; + bool ticks_on_borders = false; public: virtual Size2 get_minimum_size() const override; diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp index ae2f99e91d..65a3eb3adf 100644 --- a/scene/gui/spin_box.cpp +++ b/scene/gui/spin_box.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -40,7 +40,7 @@ Size2 SpinBox::get_minimum_size() const { } void SpinBox::_value_changed(double) { - String value = String::num(get_value(), Math::range_step_decimals(get_step())); + String value = TS->format_number(String::num(get_value(), Math::range_step_decimals(get_step()))); if (prefix != "") { value = prefix + " " + value; } @@ -50,11 +50,13 @@ 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 - Error err = expr->parse(p_string.trim_prefix(prefix + " ").trim_suffix(" " + suffix)); + Error err = expr->parse(num.trim_prefix(prefix + " ").trim_suffix(" " + suffix)); if (err != OK) { return; } @@ -74,7 +76,7 @@ void SpinBox::_line_edit_input(const Ref<InputEvent> &p_event) { } void SpinBox::_range_click_timeout() { - if (!drag.enabled && Input::get_singleton()->is_mouse_button_pressed(BUTTON_LEFT)) { + if (!drag.enabled && Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) { bool up = get_local_mouse_position().y < (get_size().height / 2); set_value(get_value() + (up ? get_step() : -get_step())); @@ -89,7 +91,17 @@ void SpinBox::_range_click_timeout() { } } -void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { +void SpinBox::_release_mouse() { + if (drag.enabled) { + drag.enabled = false; + Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); + warp_mouse(drag.capture_pos); + } +} + +void SpinBox::gui_input(const Ref<InputEvent> &p_event) { + ERR_FAIL_COND(p_event.is_null()); + if (!is_editable()) { return; } @@ -100,7 +112,7 @@ void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { bool up = mb->get_position().y < (get_size().height / 2); switch (mb->get_button_index()) { - case BUTTON_LEFT: { + case MOUSE_BUTTON_LEFT: { line_edit->grab_focus(); set_value(get_value() + (up ? get_step() : -get_step())); @@ -112,40 +124,37 @@ void SpinBox::_gui_input(const Ref<InputEvent> &p_event) { drag.allowed = true; drag.capture_pos = mb->get_position(); } break; - case BUTTON_RIGHT: { + case MOUSE_BUTTON_RIGHT: { line_edit->grab_focus(); set_value((up ? get_max() : get_min())); } break; - case BUTTON_WHEEL_UP: { + case MOUSE_BUTTON_WHEEL_UP: { if (line_edit->has_focus()) { set_value(get_value() + get_step() * mb->get_factor()); accept_event(); } } break; - case BUTTON_WHEEL_DOWN: { + case MOUSE_BUTTON_WHEEL_DOWN: { if (line_edit->has_focus()) { set_value(get_value() - get_step() * mb->get_factor()); accept_event(); } } break; + default: + break; } } - if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { //set_default_cursor_shape(CURSOR_ARROW); range_click_timer->stop(); - - if (drag.enabled) { - drag.enabled = false; - Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); - warp_mouse(drag.capture_pos); - } + _release_mouse(); drag.allowed = false; } Ref<InputEventMouseMotion> mm = p_event; - if (mm.is_valid() && mm->get_button_mask() & BUTTON_MASK_LEFT) { + if (mm.is_valid() && mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT) { if (drag.enabled) { drag.diff_y += mm->get_relative().y; float diff_y = -0.01 * Math::pow(ABS(drag.diff_y), 1.8f) * SGN(drag.diff_y); @@ -161,40 +170,51 @@ 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) { int w = icon->get_width(); - if (w != last_w) { - line_edit->set_margin(MARGIN_RIGHT, -w); + if ((w != last_w)) { + line_edit->set_offset(SIDE_LEFT, 0); + line_edit->set_offset(SIDE_RIGHT, -w); last_w = w; } } 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); RID ci = get_canvas_item(); Size2i size = get_size(); - updown->draw(ci, Point2i(size.width - updown->get_width(), (size.height - updown->get_height()) / 2)); + if (is_layout_rtl()) { + updown->draw(ci, Point2i(0, (size.height - updown->get_height()) / 2)); + } else { + updown->draw(ci, Point2i(size.width - updown->get_width(), (size.height - updown->get_height()) / 2)); + } } 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(); } } @@ -233,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); @@ -257,17 +277,17 @@ void SpinBox::_bind_methods() { } SpinBox::SpinBox() { - last_w = 0; line_edit = memnew(LineEdit); add_child(line_edit); - line_edit->set_anchors_and_margins_preset(Control::PRESET_WIDE); + 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)); - drag.enabled = false; range_click_timer = memnew(Timer); range_click_timer->connect("timeout", callable_mp(this, &SpinBox::_range_click_timeout)); diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h index 1b3dc9d79e..9ec3885f1f 100644 --- a/scene/gui/spin_box.h +++ b/scene/gui/spin_box.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -39,12 +39,13 @@ class SpinBox : public Range { GDCLASS(SpinBox, Range); LineEdit *line_edit; - int last_w; + int last_w = 0; Timer *range_click_timer; 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; @@ -52,11 +53,11 @@ class SpinBox : public Range { void _line_edit_input(const Ref<InputEvent> &p_event); struct Drag { - float base_val; - bool allowed; - bool enabled; + float base_val = 0.0; + bool allowed = false; + bool enabled = false; Vector2 capture_pos; - float diff_y; + float diff_y = 0.0; } drag; void _line_edit_focus_exit(); @@ -64,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 6508be1e43..4736a1ad37 100644 --- a/scene/gui/split_container.cpp +++ b/scene/gui/split_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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 @@ -102,7 +102,7 @@ void SplitContainer::_resort() { middle_sep += clamped_split_offset; if (should_clamp_split_offset) { split_offset = clamped_split_offset; - _change_notify("split_offset"); + should_clamp_split_offset = false; } } @@ -112,9 +112,16 @@ void SplitContainer::_resort() { int sofs = middle_sep + sep; fit_child_in_rect(second, Rect2(Point2(0, sofs), Size2(get_size().width, get_size().height - sofs))); } else { - fit_child_in_rect(first, Rect2(Point2(0, 0), Size2(middle_sep, get_size().height))); - int sofs = middle_sep + sep; - fit_child_in_rect(second, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height))); + if (is_layout_rtl()) { + middle_sep = get_size().width - middle_sep - sep; + fit_child_in_rect(second, Rect2(Point2(0, 0), Size2(middle_sep, get_size().height))); + int sofs = middle_sep + sep; + fit_child_in_rect(first, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height))); + } else { + fit_child_in_rect(first, Rect2(Point2(0, 0), Size2(middle_sep, get_size().height))); + int sofs = middle_sep + sep; + fit_child_in_rect(second, Rect2(Point2(sofs, 0), Size2(get_size().width - sofs, get_size().height))); + } } update(); @@ -124,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++) { @@ -157,12 +164,16 @@ Size2 SplitContainer::get_minimum_size() const { void SplitContainer::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + queue_sort(); + } break; case NOTIFICATION_SORT_CHILDREN: { _resort(); } break; case NOTIFICATION_MOUSE_EXIT: { mouse_inside = false; - if (get_theme_constant("autohide")) { + if (get_theme_constant(SNAME("autohide"))) { update(); } } break; @@ -171,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; } @@ -179,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) { @@ -195,7 +206,9 @@ 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) { return; } @@ -203,9 +216,9 @@ void SplitContainer::_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid()) { - if (mb->get_button_index() == BUTTON_LEFT) { + 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) { @@ -231,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(); } } @@ -247,10 +260,14 @@ void SplitContainer::_gui_input(const Ref<InputEvent> &p_event) { return; } - split_offset = drag_ofs + ((vertical ? mm->get_position().y : mm->get_position().x) - drag_from); + if (!vertical && is_layout_rtl()) { + split_offset = drag_ofs + (drag_from - (vertical ? mm->get_position().y : mm->get_position().x)); + } else { + split_offset = drag_ofs + ((vertical ? mm->get_position().y : mm->get_position().x) - drag_from); + } should_clamp_split_offset = true; queue_sort(); - emit_signal("dragged", get_split_offset()); + emit_signal(SNAME("dragged"), get_split_offset()); } } @@ -260,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) { @@ -320,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); @@ -336,7 +351,7 @@ void SplitContainer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "split_offset"), "set_split_offset", "get_split_offset"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "collapsed"), "set_collapsed", "is_collapsed"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "dragger_visibility", PROPERTY_HINT_ENUM, "Visible,Hidden,Hidden & Collapsed"), "set_dragger_visibility", "get_dragger_visibility"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "dragger_visibility", PROPERTY_HINT_ENUM, "Visible,Hidden,Hidden and Collapsed"), "set_dragger_visibility", "get_dragger_visibility"); BIND_ENUM_CONSTANT(DRAGGER_VISIBLE); BIND_ENUM_CONSTANT(DRAGGER_HIDDEN); @@ -344,12 +359,5 @@ void SplitContainer::_bind_methods() { } SplitContainer::SplitContainer(bool p_vertical) { - mouse_inside = false; - split_offset = 0; - should_clamp_split_offset = false; - middle_sep = 0; vertical = p_vertical; - dragging = false; - collapsed = false; - dragger_visibility = DRAGGER_VISIBLE; } diff --git a/scene/gui/split_container.h b/scene/gui/split_container.h index e345016f3d..47fd30a122 100644 --- a/scene/gui/split_container.h +++ b/scene/gui/split_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -44,23 +44,23 @@ public: }; private: - bool should_clamp_split_offset; - int split_offset; - int middle_sep; - bool vertical; - bool dragging; - int drag_from; - int drag_ofs; - bool collapsed; - DraggerVisibility dragger_visibility; - bool mouse_inside; + bool should_clamp_split_offset = false; + int split_offset = 0; + int middle_sep = 0; + bool vertical = false; + bool dragging = false; + int drag_from = 0; + int drag_ofs = 0; + bool collapsed = false; + DraggerVisibility dragger_visibility = DRAGGER_VISIBLE; + bool mouse_inside = false; Control *_getch(int p_idx) const; 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 c5f56fe8e2..53ea32e1b7 100644 --- a/scene/gui/subviewport_container.cpp +++ b/scene/gui/subviewport_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -139,7 +139,9 @@ 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()) { return; } @@ -160,11 +162,13 @@ 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()) { return; } @@ -185,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); @@ -203,8 +205,6 @@ void SubViewportContainer::_bind_methods() { } SubViewportContainer::SubViewportContainer() { - stretch = false; - shrink = 1; set_process_input(true); set_process_unhandled_input(true); } diff --git a/scene/gui/subviewport_container.h b/scene/gui/subviewport_container.h index e82ad772ce..7853f1590e 100644 --- a/scene/gui/subviewport_container.h +++ b/scene/gui/subviewport_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,8 +36,8 @@ class SubViewportContainer : public Container { GDCLASS(SubViewportContainer, Container); - bool stretch; - int shrink; + bool stretch = false; + int shrink = 1; protected: void _notification(int p_what); @@ -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 d92f41af2d..5884d2a854 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -31,6 +31,8 @@ #include "tab_container.h" #include "core/object/message_queue.h" +#include "core/string/translation.h" + #include "scene/gui/box_container.h" #include "scene/gui/label.h" #include "scene/gui/texture_rect.h" @@ -41,18 +43,19 @@ int TabContainer::_get_top_margin() const { } // Respect the minimum tab height. - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - 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_bg->get_minimum_size().height, tab_fg->get_minimum_size().height), tab_disabled->get_minimum_size().height); + int tab_height = MAX(MAX(tab_unselected->get_minimum_size().height, tab_selected->get_minimum_size().height), tab_disabled->get_minimum_size().height); // Font height or higher icon wins. - Ref<Font> font = get_theme_font("font"); - int content_height = font->get_height(); + int content_height = 0; Vector<Control *> tabs = _get_tabs(); for (int i = 0; i < tabs.size(); i++) { + content_height = MAX(content_height, text_buf[i]->get_size().y); + Control *c = tabs[i]; if (!c->has_meta("_tab_icon")) { continue; @@ -68,33 +71,48 @@ 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; Popup *popup = get_popup(); - if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { Point2 pos(mb->get_position().x, mb->get_position().y); Size2 size = get_size(); // Click must be on tabs in the tab header area. - if (pos.x < tabs_ofs_cache || pos.y > _get_top_margin()) { + if (pos.y > _get_top_margin()) { return; } // Handle menu button. - Ref<Texture2D> menu = get_theme_icon("menu"); + Ref<Texture2D> menu = get_theme_icon(SNAME("menu")); - if (popup && pos.x > size.width - menu->get_width()) { - emit_signal("pre_popup_pressed"); + if (is_layout_rtl()) { + if (popup && pos.x < menu->get_width()) { + emit_signal(SNAME("pre_popup_pressed")); - Vector2 popup_pos = get_screen_position(); - popup_pos.x += size.width - popup->get_size().width; - popup_pos.y += menu->get_height(); + Vector2 popup_pos = get_screen_position(); + popup_pos.y += menu->get_height(); - popup->set_position(popup_pos); - popup->popup(); - return; + popup->set_position(popup_pos); + popup->popup(); + return; + } + } else { + if (popup && pos.x > size.width - menu->get_width()) { + emit_signal(SNAME("pre_popup_pressed")); + + Vector2 popup_pos = get_screen_position(); + popup_pos.x += size.width - popup->get_size().width; + popup_pos.y += menu->get_height(); + + popup->set_position(popup_pos); + popup->popup(); + return; + } } // Do not activate tabs when tabs is empty. @@ -111,24 +129,48 @@ 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"); - if (pos.x > size.width - increment->get_width() - popup_ofs) { - if (last_tab_cache < tabs.size() - 1) { - first_tab_cache += 1; - update(); + 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) { + first_tab_cache += 1; + update(); + } + return; + } else if (pos.x < popup_ofs + increment->get_width() + decrement->get_width()) { + if (first_tab_cache > 0) { + first_tab_cache -= 1; + update(); + } + return; } - return; - } else if (pos.x > size.width - increment->get_width() - decrement->get_width() - popup_ofs) { - if (first_tab_cache > 0) { - first_tab_cache -= 1; - update(); + } else { + if (pos.x > size.width - increment->get_width() - popup_ofs && pos.x) { + if (last_tab_cache < tabs.size() - 1) { + first_tab_cache += 1; + update(); + } + return; + } else if (pos.x > size.width - increment->get_width() - decrement->get_width() - popup_ofs) { + if (first_tab_cache > 0) { + first_tab_cache -= 1; + update(); + } + return; } - return; } } // Activate the clicked tab. + if (is_layout_rtl()) { + pos.x = size.width - pos.x; + } + + if (pos.x < tabs_ofs_cache) { + return; + } + pos.x -= tabs_ofs_cache; for (int i = first_tab_cache; i <= last_tab_cache; i++) { if (get_tab_hidden(i)) { @@ -152,7 +194,7 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) { Size2 size = get_size(); // Mouse must be on tabs in the tab header area. - if (pos.x < tabs_ofs_cache || pos.y > _get_top_margin()) { + if (pos.y > _get_top_margin()) { if (menu_hovered || highlight_arrow > -1) { menu_hovered = false; highlight_arrow = -1; @@ -161,18 +203,32 @@ 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 (pos.x >= size.width - menu->get_width()) { - if (!menu_hovered) { - menu_hovered = true; - highlight_arrow = -1; + if (is_layout_rtl()) { + if (pos.x <= menu->get_width()) { + if (!menu_hovered) { + menu_hovered = true; + highlight_arrow = -1; + update(); + return; + } + } else if (menu_hovered) { + menu_hovered = false; + update(); + } + } else { + if (pos.x >= size.width - menu->get_width()) { + if (!menu_hovered) { + menu_hovered = true; + highlight_arrow = -1; + update(); + return; + } + } else if (menu_hovered) { + menu_hovered = false; update(); - return; } - } else if (menu_hovered) { - menu_hovered = false; - update(); } if (menu_hovered) { @@ -192,37 +248,51 @@ 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"); - if (pos.x >= size.width - increment->get_width() - popup_ofs) { - if (highlight_arrow != 1) { - highlight_arrow = 1; + 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 (highlight_arrow != 1) { + highlight_arrow = 1; + update(); + } + } else if (pos.x <= popup_ofs + increment->get_width() + decrement->get_width()) { + if (highlight_arrow != 0) { + highlight_arrow = 0; + update(); + } + } else if (highlight_arrow > -1) { + highlight_arrow = -1; update(); } - } else if (pos.x >= size.width - increment->get_width() - decrement->get_width() - popup_ofs) { - if (highlight_arrow != 0) { - highlight_arrow = 0; + } else { + if (pos.x >= size.width - increment->get_width() - popup_ofs) { + if (highlight_arrow != 1) { + highlight_arrow = 1; + update(); + } + } else if (pos.x >= size.width - increment->get_width() - decrement->get_width() - popup_ofs) { + if (highlight_arrow != 0) { + highlight_arrow = 0; + update(); + } + } else if (highlight_arrow > -1) { + highlight_arrow = -1; update(); } - } else if (highlight_arrow > -1) { - highlight_arrow = -1; - update(); } } } void TabContainer::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_TRANSLATION_CHANGED: { - minimum_size_changed(); - update(); - } break; 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. @@ -259,29 +329,29 @@ void TabContainer::_notification(int p_what) { case NOTIFICATION_DRAW: { RID canvas = get_canvas_item(); Size2 size = get_size(); + 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_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - 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"); - Ref<Font> font = get_theme_font("font"); - Color font_color_fg = get_theme_color("font_color_fg"); - Color font_color_bg = get_theme_color("font_color_bg"); - Color font_color_disabled = get_theme_color("font_color_disabled"); - 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; @@ -324,6 +394,7 @@ void TabContainer::_notification(int p_what) { Vector<int> tab_widths; for (int i = first_tab_cache; i < tabs.size(); i++) { if (get_tab_hidden(i)) { + tab_widths.push_back(0); continue; } int tab_width = _get_tab_width(i); @@ -347,75 +418,129 @@ void TabContainer::_notification(int p_what) { break; } + if (all_tabs_in_front) { + // Draw the tab area. + panel->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height)); + } + // Draw unselected tabs in back int x = 0; int x_current = 0; + int index = 0; for (int i = 0; i < tab_widths.size(); i++) { - if (get_tab_hidden(i)) { + index = i + first_tab_cache; + if (get_tab_hidden(index)) { continue; } int tab_width = tab_widths[i]; - if (get_tab_disabled(i + first_tab_cache)) { - _draw_tab(tab_disabled, font_color_disabled, i, tabs_ofs_cache + x); - } else if (i + first_tab_cache == current) { + 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 { - _draw_tab(tab_bg, font_color_bg, i, tabs_ofs_cache + x); + if (rtl) { + _draw_tab(tab_unselected, font_unselected_color, index, size.width - (tabs_ofs_cache + x) - tab_width); + } else { + _draw_tab(tab_unselected, font_unselected_color, index, tabs_ofs_cache + x); + } } x += tab_width; - last_tab_cache = i + first_tab_cache; + last_tab_cache = index; } - // Draw the tab area. - panel->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height)); + if (!all_tabs_in_front) { + // Draw the tab area. + panel->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height)); + } - // Draw selected tab in front. Need to check tabs.size() in case of no contents at all. - if (tabs.size() > 0) { - _draw_tab(tab_fg, font_color_fg, current, tabs_ofs_cache + x_current); + // 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) { + if (rtl) { + _draw_tab(tab_selected, 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 the popup menu. - x = get_size().width; + if (rtl) { + x = 0; + } else { + x = get_size().width; + } if (popup) { - x -= menu->get_width(); + if (!rtl) { + x -= menu->get_width(); + } if (menu_hovered) { menu_hl->draw(get_canvas_item(), Size2(x, (header_height - menu_hl->get_height()) / 2)); } else { menu->draw(get_canvas_item(), Size2(x, (header_height - menu->get_height()) / 2)); } + if (rtl) { + x += menu->get_width(); + } } // Draw the navigation buttons. if (buttons_visible_cache) { - x -= increment->get_width(); - if (last_tab_cache < tabs.size() - 1) { - draw_texture(highlight_arrow == 1 ? increment_hl : increment, Point2(x, (header_height - increment->get_height()) / 2)); + if (rtl) { + if (last_tab_cache < tabs.size() - 1) { + draw_texture(highlight_arrow == 1 ? decrement_hl : decrement, Point2(x, (header_height - increment->get_height()) / 2)); + } else { + draw_texture(decrement, Point2(x, (header_height - increment->get_height()) / 2), Color(1, 1, 1, 0.5)); + } + x += increment->get_width(); + + if (first_tab_cache > 0) { + draw_texture(highlight_arrow == 0 ? increment_hl : increment, Point2(x, (header_height - decrement->get_height()) / 2)); + } else { + draw_texture(increment, Point2(x, (header_height - decrement->get_height()) / 2), Color(1, 1, 1, 0.5)); + } + x += decrement->get_width(); } else { - draw_texture(increment, Point2(x, (header_height - increment->get_height()) / 2), Color(1, 1, 1, 0.5)); - } - - x -= decrement->get_width(); - if (first_tab_cache > 0) { - draw_texture(highlight_arrow == 0 ? decrement_hl : decrement, Point2(x, (header_height - decrement->get_height()) / 2)); - } else { - draw_texture(decrement, Point2(x, (header_height - decrement->get_height()) / 2), Color(1, 1, 1, 0.5)); + x -= increment->get_width(); + if (last_tab_cache < tabs.size() - 1) { + draw_texture(highlight_arrow == 1 ? increment_hl : increment, Point2(x, (header_height - increment->get_height()) / 2)); + } else { + draw_texture(increment, Point2(x, (header_height - increment->get_height()) / 2), Color(1, 1, 1, 0.5)); + } + + x -= decrement->get_width(); + if (first_tab_cache > 0) { + draw_texture(highlight_arrow == 0 ? decrement_hl : decrement, Point2(x, (header_height - decrement->get_height()) / 2)); + } else { + draw_texture(decrement, Point2(x, (header_height - decrement->get_height()) / 2), Color(1, 1, 1, 0.5)); + } } } } break; + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { - minimum_size_changed(); - call_deferred("_on_theme_changed"); // Wait until all changed theme. + Vector<Control *> tabs = _get_tabs(); + for (int i = 0; i < tabs.size(); i++) { + text_buf.write[i]->clear(); + } + _theme_changing = true; + 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"); - 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(); @@ -424,11 +549,10 @@ 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 + first_tab_cache]); - 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(MARGIN_LEFT); - int top_margin = p_tab_style->get_margin(MARGIN_TOP); + int x_content = tab_rect.position.x + p_tab_style->get_margin(SIDE_LEFT); + int top_margin = p_tab_style->get_margin(SIDE_TOP); int y_center = top_margin + (tab_rect.size.y - p_tab_style->get_minimum_size().y) / 2; // Draw the tab icon. @@ -444,32 +568,61 @@ void TabContainer::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, in } // Draw the tab text. - Point2i text_pos(x_content, y_center - (font->get_height() / 2) + font->get_ascent()); - font->draw(canvas, text_pos, text, p_font_color); + Point2i text_pos(x_content, y_center - text_buf[p_index]->get_size().y / 2); + if (outline_size > 0 && font_outline_color.a > 0) { + text_buf[p_index]->draw_outline(canvas, text_pos, outline_size, font_outline_color); + } + text_buf[p_index]->draw(canvas, text_pos, p_font_color); +} + +void TabContainer::_refresh_texts() { + text_buf.clear(); + Vector<Control *> tabs = _get_tabs(); + bool rtl = is_layout_rtl(); + 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(atr(String(control->get_meta("_tab_name")))) : String(atr(control->get_name())); + + Ref<TextLine> name; + 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); + } } void TabContainer::_on_theme_changed() { + if (!_theme_changing) { + return; + } + + _refresh_texts(); + + minimum_size_changed(); if (get_tab_count() > 0) { _repaint(); update(); } + _theme_changing = false; } 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]; if (i == current) { c->show(); - c->set_anchors_and_margins_preset(Control::PRESET_WIDE); + c->set_anchors_and_offsets_preset(Control::PRESET_WIDE); if (tabs_visible) { - c->set_margin(MARGIN_TOP, _get_top_margin()); + c->set_offset(SIDE_TOP, _get_top_margin()); } - c->set_margin(Margin(MARGIN_TOP), c->get_margin(Margin(MARGIN_TOP)) + sb->get_margin(Margin(MARGIN_TOP))); - c->set_margin(Margin(MARGIN_LEFT), c->get_margin(Margin(MARGIN_LEFT)) + sb->get_margin(Margin(MARGIN_LEFT))); - c->set_margin(Margin(MARGIN_RIGHT), c->get_margin(Margin(MARGIN_RIGHT)) - sb->get_margin(Margin(MARGIN_RIGHT))); - c->set_margin(Margin(MARGIN_BOTTOM), c->get_margin(Margin(MARGIN_BOTTOM)) - sb->get_margin(Margin(MARGIN_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(); @@ -493,9 +646,10 @@ int TabContainer::_get_tab_width(int p_index) const { } // Get the width of the text displayed on the tab. - Ref<Font> font = get_theme_font("font"); - String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(control->get_name()); - int width = font->get_string_size(text).width; + 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. if (control->has_meta("_tab_icon")) { @@ -503,21 +657,21 @@ 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_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - 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) { - width += tab_fg->get_minimum_size().width; + width += tab_selected->get_minimum_size().width; } else { - width += tab_bg->get_minimum_size().width; + width += tab_unselected->get_minimum_size().width; } return width; @@ -527,7 +681,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; } @@ -537,6 +691,7 @@ Vector<Control *> TabContainer::_get_tabs() const { } void TabContainer::_child_renamed_callback() { + _refresh_texts(); update(); } @@ -544,41 +699,52 @@ 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; } + Vector<Control *> tabs = _get_tabs(); + _refresh_texts(); + bool first = false; - if (get_tab_count() != 1) { + if (tabs.size() != 1) { c->hide(); } else { c->show(); - //call_deferred("set_current_tab",0); + //call_deferred(SNAME("set_current_tab"),0); first = true; current = 0; previous = 0; } - c->set_anchors_and_margins_preset(Control::PRESET_WIDE); + c->set_anchors_and_offsets_preset(Control::PRESET_WIDE); if (tabs_visible) { - c->set_margin(MARGIN_TOP, _get_top_margin()); + c->set_offset(SIDE_TOP, _get_top_margin()); } - Ref<StyleBox> sb = get_theme_stylebox("panel"); - c->set_margin(Margin(MARGIN_TOP), c->get_margin(Margin(MARGIN_TOP)) + sb->get_margin(Margin(MARGIN_TOP))); - c->set_margin(Margin(MARGIN_LEFT), c->get_margin(Margin(MARGIN_LEFT)) + sb->get_margin(Margin(MARGIN_LEFT))); - c->set_margin(Margin(MARGIN_RIGHT), c->get_margin(Margin(MARGIN_RIGHT)) - sb->get_margin(Margin(MARGIN_RIGHT))); - c->set_margin(Margin(MARGIN_BOTTOM), c->get_margin(Margin(MARGIN_BOTTOM)) - sb->get_margin(Margin(MARGIN_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); + + Control *c = Object::cast_to<Control>(p_child); + if (!c || c->is_set_as_top_level()) { + return; + } + + _update_current_tab(); + update(); +} + int TabContainer::get_tab_count() const { return _get_tabs().size(); } @@ -591,14 +757,12 @@ void TabContainer::set_current_tab(int p_current) { _repaint(); - _change_notify("current_tab"); - 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(); @@ -622,18 +786,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); - call_deferred("_update_current_tab"); + Control *c = Object::cast_to<Control>(p_child); + if (!c || c->is_set_as_top_level()) { + return; + } + + // 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)); @@ -641,6 +806,8 @@ void TabContainer::remove_child_notify(Node *p_child) { } void TabContainer::_update_current_tab() { + _refresh_texts(); + int tc = get_tab_count(); if (current >= tc) { current = tc - 1; @@ -729,7 +896,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 @@ -742,9 +909,9 @@ void TabContainer::drop_data(const Point2 &p_point, const Variant &p_data) { 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); } } } @@ -757,30 +924,38 @@ int TabContainer::get_tab_idx_at_point(const Point2 &p_point) const { } // must be on tabs in the tab header area. - if (p_point.x < tabs_ofs_cache || p_point.y > _get_top_margin()) { + if (p_point.y > _get_top_margin()) { return -1; } Size2 size = get_size(); - int right_ofs = 0; + int button_ofs = 0; + int px = p_point.x; + + if (is_layout_rtl()) { + px = size.width - px; + } + + if (px < tabs_ofs_cache) { + return -1; + } Popup *popup = get_popup(); if (popup) { - Ref<Texture2D> menu = get_theme_icon("menu"); - right_ofs += menu->get_width(); + 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"); - right_ofs += increment->get_width() + decrement->get_width(); + 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 (p_point.x > size.width - right_ofs) { + if (px > size.width - button_ofs) { return -1; } // get the tab at the point Vector<Control *> tabs = _get_tabs(); - int px = p_point.x; px -= tabs_ofs_cache; for (int i = first_tab_cache; i <= last_tab_cache; i++) { int tab_width = _get_tab_width(i); @@ -796,8 +971,6 @@ void TabContainer::set_tab_align(TabAlign p_align) { ERR_FAIL_INDEX(p_align, 3); align = p_align; update(); - - _change_notify("tab_align"); } TabContainer::TabAlign TabContainer::get_tab_align() const { @@ -815,9 +988,9 @@ void TabContainer::set_tabs_visible(bool p_visible) { for (int i = 0; i < tabs.size(); i++) { Control *c = tabs[i]; if (p_visible) { - c->set_margin(MARGIN_TOP, _get_top_margin()); + c->set_offset(SIDE_TOP, _get_top_margin()); } else { - c->set_margin(MARGIN_TOP, 0); + c->set_offset(SIDE_TOP, 0); } } @@ -829,19 +1002,30 @@ bool TabContainer::are_tabs_visible() const { return tabs_visible; } -Control *TabContainer::_get_tab(int p_idx) const { - return get_tab_control(p_idx); +void TabContainer::set_all_tabs_in_front(bool p_in_front) { + if (p_in_front == all_tabs_in_front) { + return; + } + + all_tabs_in_front = p_in_front; + + update(); +} + +bool TabContainer::is_all_tabs_in_front() const { + return all_tabs_in_front; } 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(); update(); } 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"); @@ -851,14 +1035,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"); @@ -868,14 +1052,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"); @@ -885,7 +1069,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(); @@ -904,7 +1088,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"); @@ -946,17 +1130,17 @@ Size2 TabContainer::get_minimum_size() const { ms.y = MAX(ms.y, cms.y); } - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - 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_bg->get_minimum_size().y, tab_fg->get_minimum_size().y), tab_disabled->get_minimum_size().y); - ms.y += font->get_height(); + 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; @@ -1009,7 +1193,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); @@ -1020,6 +1203,8 @@ void TabContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tab_align"), &TabContainer::get_tab_align); ClassDB::bind_method(D_METHOD("set_tabs_visible", "visible"), &TabContainer::set_tabs_visible); ClassDB::bind_method(D_METHOD("are_tabs_visible"), &TabContainer::are_tabs_visible); + ClassDB::bind_method(D_METHOD("set_all_tabs_in_front", "is_front"), &TabContainer::set_all_tabs_in_front); + ClassDB::bind_method(D_METHOD("is_all_tabs_in_front"), &TabContainer::is_all_tabs_in_front); ClassDB::bind_method(D_METHOD("set_tab_title", "tab_idx", "title"), &TabContainer::set_tab_title); ClassDB::bind_method(D_METHOD("get_tab_title", "tab_idx"), &TabContainer::get_tab_title); ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &TabContainer::set_tab_icon); @@ -1046,6 +1231,7 @@ void TabContainer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_align", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_tab_align", "get_tab_align"); ADD_PROPERTY(PropertyInfo(Variant::INT, "current_tab", PROPERTY_HINT_RANGE, "-1,4096,1", PROPERTY_USAGE_EDITOR), "set_current_tab", "get_current_tab"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "tabs_visible"), "set_tabs_visible", "are_tabs_visible"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "all_tabs_in_front"), "set_all_tabs_in_front", "is_all_tabs_in_front"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_hidden_tabs_for_min_size"), "set_use_hidden_tabs_for_min_size", "get_use_hidden_tabs_for_min_size"); @@ -1055,19 +1241,5 @@ void TabContainer::_bind_methods() { } TabContainer::TabContainer() { - first_tab_cache = 0; - last_tab_cache = 0; - buttons_visible_cache = false; - menu_hovered = false; - highlight_arrow = -1; - tabs_ofs_cache = 0; - current = 0; - previous = 0; - align = ALIGN_CENTER; - tabs_visible = true; - drag_to_rearrange_enabled = false; - tabs_rearrange_group = -1; - use_hidden_tabs_for_min_size = false; - connect("mouse_exited", callable_mp(this, &TabContainer::_on_mouse_exited)); } diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h index 6ac07b5845..fe96df25e8 100644 --- a/scene/gui/tab_container.h +++ b/scene/gui/tab_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -33,48 +33,53 @@ #include "scene/gui/container.h" #include "scene/gui/popup.h" +#include "scene/resources/text_line.h" + class TabContainer : public Container { GDCLASS(TabContainer, Container); public: enum TabAlign { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT }; private: - int first_tab_cache; - int tabs_ofs_cache; - int last_tab_cache; - int current; - int previous; - bool tabs_visible; - bool buttons_visible_cache; - bool menu_hovered; - int highlight_arrow; - TabAlign align; - Control *_get_tab(int p_idx) const; + int first_tab_cache = 0; + int tabs_ofs_cache = 0; + int last_tab_cache = 0; + int current = 0; + int previous = 0; + bool tabs_visible = true; + bool all_tabs_in_front = false; + bool buttons_visible_cache = false; + bool menu_hovered = false; + int highlight_arrow = -1; + TabAlign align = ALIGN_CENTER; int _get_top_margin() const; mutable ObjectID popup_obj_id; - bool drag_to_rearrange_enabled; - bool use_hidden_tabs_for_min_size; - int tabs_rearrange_group; + bool drag_to_rearrange_enabled = false; + bool use_hidden_tabs_for_min_size = false; + int tabs_rearrange_group = -1; + Vector<Ref<TextLine>> text_buf; Vector<Control *> _get_tabs() const; int _get_tab_width(int p_index) const; + bool _theme_changing = false; void _on_theme_changed(); void _repaint(); void _on_mouse_exited(); void _update_current_tab(); void _draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x); + void _refresh_texts(); 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; virtual void remove_child_notify(Node *p_child) override; Variant get_drag_data(const Point2 &p_point) override; @@ -91,6 +96,9 @@ public: void set_tabs_visible(bool p_visible); bool are_tabs_visible() const; + void set_all_tabs_in_front(bool p_is_front); + bool is_all_tabs_in_front() const; + void set_tab_title(int p_tab, const String &p_title); String get_tab_title(int p_tab) const; diff --git a/scene/gui/tabs.cpp b/scene/gui/tabs.cpp index eefe8cc3bc..3ca2d1c1e9 100644 --- a/scene/gui/tabs.cpp +++ b/scene/gui/tabs.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -31,59 +31,68 @@ #include "tabs.h" #include "core/object/message_queue.h" +#include "core/string/translation.h" + #include "scene/gui/box_container.h" #include "scene/gui/label.h" #include "scene/gui/texture_rect.h" Size2 Tabs::get_minimum_size() const { - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - 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")); + + int y_margin = MAX(MAX(tab_unselected->get_minimum_size().height, tab_selected->get_minimum_size().height), tab_disabled->get_minimum_size().height); - Size2 ms(0, MAX(MAX(tab_bg->get_minimum_size().height, tab_fg->get_minimum_size().height), tab_disabled->get_minimum_size().height) + font->get_height()); + Size2 ms(0, 0); for (int i = 0; i < tabs.size(); i++) { Ref<Texture2D> tex = tabs[i].icon; 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")); } } - ms.width += Math::ceil(font->get_string_size(tabs[i].xl_text).width); + ms.width += Math::ceil(tabs[i].text_buf->get_size().x); + ms.height = MAX(ms.height, tabs[i].text_buf->get_size().y + y_margin); if (tabs[i].disabled) { ms.width += tab_disabled->get_minimum_size().width; } else if (current == i) { - ms.width += tab_fg->get_minimum_size().width; + ms.width += tab_selected->get_minimum_size().width; } else { - ms.width += tab_bg->get_minimum_size().width; + ms.width += tab_unselected->get_minimum_size().width; } 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_bg->get_minimum_size().height, ms.height); + 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_bg->get_minimum_size().height, ms.height); + ms.height = MAX(bms.height + tab_unselected->get_minimum_size().height, ms.height); } } - ms.width = 0; //TODO: should make this optional + if (clip_tabs) { + ms.width = 0; + } + 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; if (mm.is_valid()) { @@ -91,15 +100,22 @@ 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"); - - int limit = get_size().width - incr->get_width() - decr->get_width(); - - if (pos.x > limit + decr->get_width()) { - highlight_arrow = 1; - } else if (pos.x > limit) { - highlight_arrow = 0; + 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()) { + highlight_arrow = 1; + } else if (pos.x < incr->get_width() + decr->get_width()) { + highlight_arrow = 0; + } + } else { + int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); + if (pos.x > limit_minus_buttons + decr->get_width()) { + highlight_arrow = 1; + } else if (pos.x > limit_minus_buttons) { + highlight_arrow = 0; + } } } @@ -111,7 +127,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseButton> mb = p_event; if (mb.is_valid()) { - if (mb->is_pressed() && mb->get_button_index() == BUTTON_WHEEL_UP && !mb->get_command()) { + if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && !mb->is_command_pressed()) { if (scrolling_enabled && buttons_visible) { if (offset > 0) { offset--; @@ -120,7 +136,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { } } - if (mb->is_pressed() && mb->get_button_index() == BUTTON_WHEEL_DOWN && !mb->get_command()) { + if (mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && !mb->is_command_pressed()) { if (scrolling_enabled && buttons_visible) { if (missing_right) { offset++; @@ -129,48 +145,63 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { } } - if (rb_pressing && !mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + 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; update(); } - if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == BUTTON_LEFT) { + if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == MOUSE_BUTTON_LEFT) { if (cb_hover != -1) { //pressed - emit_signal("tab_close", cb_hover); + emit_signal(SNAME("tab_closed"), cb_hover); } cb_pressing = false; update(); } - if (mb->is_pressed() && (mb->get_button_index() == BUTTON_LEFT || (select_with_rmb && mb->get_button_index() == BUTTON_RIGHT))) { + if (mb->is_pressed() && (mb->get_button_index() == MOUSE_BUTTON_LEFT || (select_with_rmb && mb->get_button_index() == MOUSE_BUTTON_RIGHT))) { // clicks 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"); - - int limit = get_size().width - incr->get_width() - decr->get_width(); - - if (pos.x > limit + decr->get_width()) { - if (missing_right) { - offset++; - update(); + 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()) { + if (missing_right) { + offset++; + update(); + } + return; + } else if (pos.x < incr->get_width() + decr->get_width()) { + if (offset > 0) { + offset--; + update(); + } + return; } - return; - } else if (pos.x > limit) { - if (offset > 0) { - offset--; - update(); + } else { + int limit = get_size().width - incr->get_width() - decr->get_width(); + if (pos.x > limit + decr->get_width()) { + if (missing_right) { + offset++; + update(); + } + return; + } else if (pos.x > limit) { + if (offset > 0) { + offset--; + update(); + } + return; } - return; } } @@ -188,7 +219,7 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) { return; } - if (pos.x >= tabs[i].ofs_cache && pos.x < tabs[i].ofs_cache + tabs[i].size_cache) { + if (pos.x >= get_tab_rect(i).position.x && pos.x < get_tab_rect(i).position.x + tabs[i].size_cache) { if (!tabs[i].disabled) { found = i; } @@ -198,18 +229,39 @@ 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(SNAME("font")); + int font_size = get_theme_font_size(SNAME("font_size")); + + 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); + } else { + tabs.write[p_tab].text_buf->set_direction((TextServer::Direction)tabs[p_tab].text_direction); + } + + tabs.write[p_tab].text_buf->add_string(tabs.write[p_tab].xl_text, font, font_size, tabs[p_tab].opentype_features, (tabs[p_tab].language != "") ? tabs[p_tab].language : TranslationServer::get_singleton()->get_tool_locale()); +} + void Tabs::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + _update_cache(); + update(); + } break; + case NOTIFICATION_THEME_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { for (int i = 0; i < tabs.size(); ++i) { - tabs.write[i].xl_text = tr(tabs[i].text); + _shape(i); } + _update_cache(); minimum_size_changed(); update(); } break; @@ -222,14 +274,18 @@ void Tabs::_notification(int p_what) { _update_cache(); RID ci = get_canvas_item(); - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<Font> font = get_theme_font("font"); - Color color_fg = get_theme_color("font_color_fg"); - Color color_bg = get_theme_color("font_color_bg"); - Color color_disabled = get_theme_color("font_color_disabled"); - Ref<Texture2D> close = get_theme_icon("close"); + 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(); int h = get_size().height; int w = 0; @@ -250,12 +306,13 @@ 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 - incr->get_size().width - decr->get_size().width; + int limit = get_size().width; + int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); missing_right = false; @@ -269,16 +326,17 @@ void Tabs::_notification(int p_what) { if (tabs[i].disabled) { sb = tab_disabled; - col = color_disabled; + col = font_disabled_color; } else if (i == current) { - sb = tab_fg; - col = color_fg; + sb = tab_selected; + col = font_selected_color; } else { - sb = tab_bg; - col = color_bg; + sb = tab_unselected; + col = font_unselected_color; } - if (w + lsize > limit) { + int new_width = w + lsize; + if (new_width > limit || (i < tabs.size() - 1 && new_width > limit_minus_buttons)) { // For the last tab, we accept if the tab covers the buttons. max_drawn_tab = i - 1; missing_right = true; break; @@ -286,88 +344,139 @@ void Tabs::_notification(int p_what) { max_drawn_tab = i; } - Rect2 sb_rect = Rect2(w, 0, tabs[i].size_cache, h); + Rect2 sb_rect; + if (rtl) { + sb_rect = Rect2(size.width - w - tabs[i].size_cache, 0, tabs[i].size_cache, h); + } else { + sb_rect = Rect2(w, 0, tabs[i].size_cache, h); + } sb->draw(ci, sb_rect); - w += sb->get_margin(MARGIN_LEFT); + w += sb->get_margin(SIDE_LEFT); Size2i sb_ms = sb->get_minimum_size(); Ref<Texture2D> icon = tabs[i].icon; if (icon.is_valid()) { - icon->draw(ci, Point2i(w, sb->get_margin(MARGIN_TOP) + ((sb_rect.size.y - sb_ms.y) - icon->get_height()) / 2)); + if (rtl) { + icon->draw(ci, Point2i(size.width - w - icon->get_width(), sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - icon->get_height()) / 2)); + } else { + 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")); } } - font->draw(ci, Point2i(w, sb->get_margin(MARGIN_TOP) + ((sb_rect.size.y - sb_ms.y) - font->get_height()) / 2 + font->get_ascent()), tabs[i].xl_text, col, tabs[i].size_text); + if (rtl) { + Vector2 text_pos = Point2i(size.width - w - tabs[i].text_buf->get_size().x, sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - tabs[i].text_buf->get_size().y) / 2); + if (outline_size > 0 && font_outline_color.a > 0) { + tabs[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + tabs[i].text_buf->draw(ci, text_pos, col); + } else { + Vector2 text_pos = Point2i(w, sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - tabs[i].text_buf->get_size().y) / 2); + if (outline_size > 0 && font_outline_color.a > 0) { + tabs[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + tabs[i].text_buf->draw(ci, text_pos, col); + } 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(); - rb_rect.position.x = w; - rb_rect.position.y = sb->get_margin(MARGIN_TOP) + ((sb_rect.size.y - sb_ms.y) - (rb_rect.size.y)) / 2; + if (rtl) { + rb_rect.position.x = size.width - w - rb_rect.size.x; + } else { + rb_rect.position.x = w; + } + rb_rect.position.y = sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - (rb_rect.size.y)) / 2; 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); } } - rb->draw(ci, Point2i(w + style->get_margin(MARGIN_LEFT), rb_rect.position.y + style->get_margin(MARGIN_TOP))); + if (rtl) { + rb->draw(ci, Point2i(size.width - w - rb_rect.size.x + style->get_margin(SIDE_LEFT), rb_rect.position.y + style->get_margin(SIDE_TOP))); + } else { + rb->draw(ci, Point2i(w + style->get_margin(SIDE_LEFT), rb_rect.position.y + style->get_margin(SIDE_TOP))); + } w += rb->get_width(); tabs.write[i].rb_rect = rb_rect; } 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(); - cb_rect.position.x = w; - cb_rect.position.y = sb->get_margin(MARGIN_TOP) + ((sb_rect.size.y - sb_ms.y) - (cb_rect.size.y)) / 2; + if (rtl) { + cb_rect.position.x = size.width - w - cb_rect.size.x; + } else { + cb_rect.position.x = w; + } + cb_rect.position.y = sb->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - (cb_rect.size.y)) / 2; 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); } } - cb->draw(ci, Point2i(w + style->get_margin(MARGIN_LEFT), cb_rect.position.y + style->get_margin(MARGIN_TOP))); + if (rtl) { + cb->draw(ci, Point2i(size.width - w - cb_rect.size.x + style->get_margin(SIDE_LEFT), cb_rect.position.y + style->get_margin(SIDE_TOP))); + } else { + cb->draw(ci, Point2i(w + style->get_margin(SIDE_LEFT), cb_rect.position.y + style->get_margin(SIDE_TOP))); + } w += cb->get_width(); tabs.write[i].cb_rect = cb_rect; } - w += sb->get_margin(MARGIN_RIGHT); + w += sb->get_margin(SIDE_RIGHT); } if (offset > 0 || missing_right) { int vofs = (get_size().height - incr->get_size().height) / 2; - if (offset > 0) { - draw_texture(highlight_arrow == 0 ? decr_hl : decr, Point2(limit, vofs)); - } else { - draw_texture(decr, Point2(limit, vofs), Color(1, 1, 1, 0.5)); - } + if (rtl) { + if (missing_right) { + draw_texture(highlight_arrow == 1 ? decr_hl : decr, Point2(0, vofs)); + } else { + draw_texture(decr, Point2(0, vofs), Color(1, 1, 1, 0.5)); + } - if (missing_right) { - draw_texture(highlight_arrow == 1 ? incr_hl : incr, Point2(limit + decr->get_size().width, vofs)); + if (offset > 0) { + draw_texture(highlight_arrow == 0 ? incr_hl : incr, Point2(incr->get_size().width, vofs)); + } else { + draw_texture(incr, Point2(incr->get_size().width, vofs), Color(1, 1, 1, 0.5)); + } } else { - draw_texture(incr, Point2(limit + decr->get_size().width, vofs), Color(1, 1, 1, 0.5)); + if (offset > 0) { + draw_texture(highlight_arrow == 0 ? decr_hl : decr, Point2(limit_minus_buttons, vofs)); + } else { + draw_texture(decr, Point2(limit_minus_buttons, vofs), Color(1, 1, 1, 0.5)); + } + + if (missing_right) { + draw_texture(highlight_arrow == 1 ? incr_hl : incr, Point2(limit_minus_buttons + decr->get_size().width, vofs)); + } else { + draw_texture(incr, Point2(limit_minus_buttons + decr->get_size().width, vofs), Color(1, 1, 1, 0.5)); + } } buttons_visible = true; @@ -391,11 +500,10 @@ void Tabs::set_current_tab(int p_current) { previous = current; current = p_current; - _change_notify("current_tab"); _update_cache(); update(); - emit_signal("tab_changed", p_current); + emit_signal(SNAME("tab_changed"), p_current); } int Tabs::get_current_tab() const { @@ -421,7 +529,8 @@ 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(); } @@ -431,6 +540,61 @@ String Tabs::get_tab_title(int p_tab) const { return tabs[p_tab].text; } +void Tabs::set_tab_text_direction(int p_tab, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (tabs[p_tab].text_direction != p_text_direction) { + tabs.write[p_tab].text_direction = p_text_direction; + _shape(p_tab); + update(); + } +} + +Control::TextDirection Tabs::get_tab_text_direction(int p_tab) const { + ERR_FAIL_INDEX_V(p_tab, tabs.size(), Control::TEXT_DIRECTION_INHERITED); + return tabs[p_tab].text_direction; +} + +void Tabs::clear_tab_opentype_features(int p_tab) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + tabs.write[p_tab].opentype_features.clear(); + _shape(p_tab); + update(); +} + +void Tabs::set_tab_opentype_feature(int p_tab, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!tabs[p_tab].opentype_features.has(tag) || (int)tabs[p_tab].opentype_features[tag] != p_value) { + tabs.write[p_tab].opentype_features[tag] = p_value; + _shape(p_tab); + update(); + } +} + +int Tabs::get_tab_opentype_feature(int p_tab, const String &p_name) const { + ERR_FAIL_INDEX_V(p_tab, tabs.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!tabs[p_tab].opentype_features.has(tag)) { + return -1; + } + return tabs[p_tab].opentype_features[tag]; +} + +void Tabs::set_tab_language(int p_tab, const String &p_language) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + if (tabs[p_tab].language != p_language) { + tabs.write[p_tab].language = p_language; + _shape(p_tab); + update(); + } +} + +String Tabs::get_tab_language(int p_tab) const { + ERR_FAIL_INDEX_V(p_tab, tabs.size(), ""); + return tabs[p_tab].language; +} + void Tabs::set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon) { ERR_FAIL_INDEX(p_tab, tabs.size()); tabs.write[p_tab].icon = p_icon; @@ -495,7 +659,7 @@ void Tabs::_update_hover() { } if (hover != hover_now) { hover = hover_now; - emit_signal("tab_hover", hover); + emit_signal(SNAME("tab_hovered"), hover); } if (hover_buttons == -1) { // no hover @@ -505,13 +669,12 @@ void Tabs::_update_hover() { } void Tabs::_update_cache() { - Ref<StyleBox> tab_disabled = get_theme_stylebox("tab_disabled"); - Ref<StyleBox> tab_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - Ref<Font> font = get_theme_font("font"); - Ref<Texture2D> incr = get_theme_icon("increment"); - Ref<Texture2D> decr = get_theme_icon("decrement"); - int limit = get_size().width - incr->get_width() - decr->get_width(); + 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; int mw = 0; @@ -520,7 +683,8 @@ void Tabs::_update_cache() { for (int i = 0; i < tabs.size(); i++) { tabs.write[i].ofs_cache = mw; tabs.write[i].size_cache = get_tab_width(i); - tabs.write[i].size_text = Math::ceil(font->get_string_size(tabs[i].xl_text).width); + tabs.write[i].size_text = Math::ceil(tabs[i].text_buf->get_size().x); + tabs.write[i].text_buf->set_width(-1); mw += tabs[i].size_cache; if (tabs[i].size_cache <= min_width || i == current) { size_fixed += tabs[i].size_cache; @@ -530,30 +694,30 @@ void Tabs::_update_cache() { } int m_width = min_width; if (count_resize > 0) { - m_width = MAX((limit - size_fixed) / count_resize, min_width); + m_width = MAX((limit_minus_buttons - size_fixed) / count_resize, min_width); } for (int i = offset; i < tabs.size(); i++) { Ref<StyleBox> sb; if (tabs[i].disabled) { sb = tab_disabled; } else if (i == current) { - sb = tab_fg; + sb = tab_selected; } else { - sb = tab_bg; + sb = tab_unselected; } int lsize = tabs[i].size_cache; int slen = tabs[i].size_text; - if (min_width > 0 && mw > limit && i != current) { + if (min_width > 0 && mw > limit_minus_buttons && i != current) { if (lsize > m_width) { - slen = m_width - (sb->get_margin(MARGIN_LEFT) + sb->get_margin(MARGIN_RIGHT)); + 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; @@ -562,6 +726,7 @@ void Tabs::_update_cache() { tabs.write[i].ofs_cache = w; tabs.write[i].size_cache = lsize; tabs.write[i].size_text = slen; + tabs.write[i].text_buf->set_width(slen); w += lsize; } } @@ -577,7 +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.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(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; @@ -585,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(); } @@ -594,7 +762,7 @@ void Tabs::clear_tabs() { tabs.clear(); current = 0; previous = 0; - call_deferred("_update_hover"); + call_deferred(SNAME("_update_hover")); update(); } @@ -605,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(); @@ -702,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 @@ -719,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(); } } @@ -749,6 +917,19 @@ Tabs::TabAlign Tabs::get_tab_align() const { return tab_align; } +void Tabs::set_clip_tabs(bool p_clip_tabs) { + if (clip_tabs == p_clip_tabs) { + return; + } + clip_tabs = p_clip_tabs; + update(); + minimum_size_changed(); +} + +bool Tabs::get_clip_tabs() const { + return clip_tabs; +} + void Tabs::move_tab(int from, int to) { if (from == to) { return; @@ -768,10 +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_bg = get_theme_stylebox("tab_bg"); - Ref<StyleBox> tab_fg = get_theme_stylebox("tab_fg"); - 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")); int x = 0; @@ -779,30 +959,30 @@ 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")); } } - x += Math::ceil(font->get_string_size(tabs[p_idx].xl_text).width); + x += Math::ceil(tabs[p_idx].text_buf->get_size().x); if (tabs[p_idx].disabled) { x += tab_disabled->get_minimum_size().width; } else if (current == p_idx) { - x += tab_fg->get_minimum_size().width; + x += tab_selected->get_minimum_size().width; } else { - x += tab_bg->get_minimum_size().width; + x += tab_unselected->get_minimum_size().width; } 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; @@ -813,10 +993,11 @@ 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 - incr->get_width() - decr->get_width(); + int limit = get_size().width; + int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); while (offset > 0) { int total_w = 0; @@ -824,7 +1005,7 @@ void Tabs::_ensure_no_over_offset() { total_w += tabs[i].size_cache; } - if (total_w < limit) { + if ((buttons_visible && total_w < limit_minus_buttons) || total_w < limit) { // For the last tab, we accept if the tab covers the buttons. offset--; update(); } else { @@ -853,11 +1034,14 @@ 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"); - int limit = get_size().width - incr->get_width() - decr->get_width(); + 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(); + for (int i = offset; i <= p_idx; i++) { - if (tabs[i].ofs_cache + tabs[i].size_cache > limit) { + int total_w = tabs[i].ofs_cache + tabs[i].size_cache; + if (total_w > limit || (buttons_visible && total_w > limit_minus_buttons)) { offset++; } } @@ -869,7 +1053,11 @@ void Tabs::ensure_tab_visible(int p_idx) { Rect2 Tabs::get_tab_rect(int p_tab) const { ERR_FAIL_INDEX_V(p_tab, tabs.size(), Rect2()); - return Rect2(tabs[p_tab].ofs_cache, 0, tabs[p_tab].size_cache, get_size().height); + if (is_layout_rtl()) { + return Rect2(get_size().width - tabs[p_tab].ofs_cache - tabs[p_tab].size_cache, 0, tabs[p_tab].size_cache, get_size().height); + } else { + return Rect2(tabs[p_tab].ofs_cache, 0, tabs[p_tab].size_cache, get_size().height); + } } void Tabs::set_tab_close_display_policy(CloseButtonDisplayPolicy p_policy) { @@ -919,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); @@ -927,6 +1114,13 @@ void Tabs::_bind_methods() { ClassDB::bind_method(D_METHOD("get_previous_tab"), &Tabs::get_previous_tab); ClassDB::bind_method(D_METHOD("set_tab_title", "tab_idx", "title"), &Tabs::set_tab_title); ClassDB::bind_method(D_METHOD("get_tab_title", "tab_idx"), &Tabs::get_tab_title); + ClassDB::bind_method(D_METHOD("set_tab_text_direction", "tab_idx", "direction"), &Tabs::set_tab_text_direction); + ClassDB::bind_method(D_METHOD("get_tab_text_direction", "tab_idx"), &Tabs::get_tab_text_direction); + ClassDB::bind_method(D_METHOD("set_tab_opentype_feature", "tab_idx", "tag", "values"), &Tabs::set_tab_opentype_feature); + ClassDB::bind_method(D_METHOD("get_tab_opentype_feature", "tab_idx", "tag"), &Tabs::get_tab_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_tab_opentype_features", "tab_idx"), &Tabs::clear_tab_opentype_features); + ClassDB::bind_method(D_METHOD("set_tab_language", "tab_idx", "language"), &Tabs::set_tab_language); + ClassDB::bind_method(D_METHOD("get_tab_language", "tab_idx"), &Tabs::get_tab_language); ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &Tabs::set_tab_icon); ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &Tabs::get_tab_icon); ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &Tabs::set_tab_disabled); @@ -935,6 +1129,8 @@ void Tabs::_bind_methods() { ClassDB::bind_method(D_METHOD("add_tab", "title", "icon"), &Tabs::add_tab, DEFVAL(""), DEFVAL(Ref<Texture2D>())); ClassDB::bind_method(D_METHOD("set_tab_align", "align"), &Tabs::set_tab_align); ClassDB::bind_method(D_METHOD("get_tab_align"), &Tabs::get_tab_align); + ClassDB::bind_method(D_METHOD("set_clip_tabs", "clip_tabs"), &Tabs::set_clip_tabs); + ClassDB::bind_method(D_METHOD("get_clip_tabs"), &Tabs::get_clip_tabs); ClassDB::bind_method(D_METHOD("get_tab_offset"), &Tabs::get_tab_offset); ClassDB::bind_method(D_METHOD("get_offset_buttons_visible"), &Tabs::get_offset_buttons_visible); ClassDB::bind_method(D_METHOD("ensure_tab_visible", "idx"), &Tabs::ensure_tab_visible); @@ -954,13 +1150,14 @@ void Tabs::_bind_methods() { ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("right_button_pressed", PropertyInfo(Variant::INT, "tab"))); - ADD_SIGNAL(MethodInfo("tab_close", PropertyInfo(Variant::INT, "tab"))); - ADD_SIGNAL(MethodInfo("tab_hover", PropertyInfo(Variant::INT, "tab"))); + ADD_SIGNAL(MethodInfo("tab_closed", PropertyInfo(Variant::INT, "tab"))); + ADD_SIGNAL(MethodInfo("tab_hovered", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("reposition_active_tab_request", PropertyInfo(Variant::INT, "idx_to"))); ADD_SIGNAL(MethodInfo("tab_clicked", PropertyInfo(Variant::INT, "tab"))); ADD_PROPERTY(PropertyInfo(Variant::INT, "current_tab", PROPERTY_HINT_RANGE, "-1,4096,1", PROPERTY_USAGE_EDITOR), "set_current_tab", "get_current_tab"); ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_align", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_tab_align", "get_tab_align"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_tabs"), "set_clip_tabs", "get_clip_tabs"); ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_close_display_policy", PROPERTY_HINT_ENUM, "Show Never,Show Active Only,Show Always"), "set_tab_close_display_policy", "get_tab_close_display_policy"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scrolling_enabled"), "set_scrolling_enabled", "get_scrolling_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled"); @@ -977,27 +1174,5 @@ void Tabs::_bind_methods() { } Tabs::Tabs() { - current = 0; - previous = 0; - tab_align = ALIGN_CENTER; - rb_hover = -1; - rb_pressing = false; - highlight_arrow = -1; - - cb_hover = -1; - cb_pressing = false; - cb_displaypolicy = CLOSE_BUTTON_SHOW_NEVER; - offset = 0; - max_drawn_tab = 0; - - select_with_rmb = false; - - min_width = 0; - scrolling_enabled = true; - buttons_visible = false; - hover = -1; - drag_to_rearrange_enabled = false; - tabs_rearrange_group = -1; - connect("mouse_exited", callable_mp(this, &Tabs::_on_mouse_exited)); } diff --git a/scene/gui/tabs.h b/scene/gui/tabs.h index b94c4a37a1..b044453803 100644 --- a/scene/gui/tabs.h +++ b/scene/gui/tabs.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -32,13 +32,13 @@ #define TABS_H #include "scene/gui/control.h" +#include "scene/resources/text_line.h" class Tabs : public Control { GDCLASS(Tabs, Control); public: enum TabAlign { - ALIGN_LEFT, ALIGN_CENTER, ALIGN_RIGHT, @@ -46,7 +46,6 @@ public: }; enum CloseButtonDisplayPolicy { - CLOSE_BUTTON_SHOW_NEVER, CLOSE_BUTTON_SHOW_ACTIVE_ONLY, CLOSE_BUTTON_SHOW_ALWAYS, @@ -57,43 +56,50 @@ private: struct Tab { String text; String xl_text; + + Dictionary opentype_features; + String language; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_INHERITED; + + Ref<TextLine> text_buf; Ref<Texture2D> icon; - int ofs_cache; - bool disabled; - int size_cache; - int size_text; - int x_cache; - int x_size_cache; + int ofs_cache = 0; + bool disabled = false; + int size_cache = 0; + int size_text = 0; + int x_cache = 0; + int x_size_cache = 0; Ref<Texture2D> right_button; Rect2 rb_rect; Rect2 cb_rect; }; - int offset; - int max_drawn_tab; - int highlight_arrow; - bool buttons_visible; - bool missing_right; + int offset = 0; + int max_drawn_tab = 0; + int highlight_arrow = -1; + bool buttons_visible = false; + bool missing_right = false; Vector<Tab> tabs; - int current; - int previous; + int current = 0; + int previous = 0; int _get_top_margin() const; - TabAlign tab_align; - int rb_hover; - bool rb_pressing; + TabAlign tab_align = ALIGN_CENTER; + bool clip_tabs = true; + int rb_hover = -1; + bool rb_pressing = false; - bool select_with_rmb; + bool select_with_rmb = false; - int cb_hover; - bool cb_pressing; - CloseButtonDisplayPolicy cb_displaypolicy; + int cb_hover = -1; + bool cb_pressing = false; + CloseButtonDisplayPolicy cb_displaypolicy = CLOSE_BUTTON_SHOW_NEVER; - int hover; // Hovered tab. - int min_width; - bool scrolling_enabled; - bool drag_to_rearrange_enabled; - int tabs_rearrange_group; + int hover = -1; // Hovered tab. + int min_width = 0; + bool scrolling_enabled = true; + bool drag_to_rearrange_enabled = false; + int tabs_rearrange_group = -1; int get_tab_width(int p_idx) const; void _ensure_no_over_offset(); @@ -103,8 +109,10 @@ private: void _on_mouse_exited(); + 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(); @@ -119,6 +127,16 @@ public: void set_tab_title(int p_tab, const String &p_title); String get_tab_title(int p_tab) const; + void set_tab_text_direction(int p_tab, TextDirection p_text_direction); + TextDirection get_tab_text_direction(int p_tab) const; + + void set_tab_opentype_feature(int p_tab, const String &p_name, int p_value); + int get_tab_opentype_feature(int p_tab, const String &p_name) const; + void clear_tab_opentype_features(int p_tab); + + void set_tab_language(int p_tab, const String &p_language); + String get_tab_language(int p_tab) const; + void set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon); Ref<Texture2D> get_tab_icon(int p_tab) const; @@ -131,6 +149,9 @@ public: void set_tab_align(TabAlign p_align); TabAlign get_tab_align() const; + void set_clip_tabs(bool p_clip_tabs); + bool get_clip_tabs() const; + void move_tab(int from, int to); void set_tab_close_display_policy(CloseButtonDisplayPolicy p_policy); diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index cbe6c6bdb9..a731f80741 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -32,22 +32,19 @@ #include "core/config/project_settings.h" #include "core/input/input.h" +#include "core/input/input_map.h" #include "core/object/message_queue.h" #include "core/object/script_language.h" #include "core/os/keyboard.h" #include "core/os/os.h" +#include "core/string/translation.h" + #include "scene/main/window.h" #ifdef TOOLS_ENABLED #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); } @@ -60,116 +57,158 @@ 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 == '}'; +/////////////////////////////////////////////////////////////////////////////// +/// 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; } -static bool _is_pair_left_symbol(char32_t c) { - return c == '"' || - c == '\'' || - c == '(' || - c == '[' || - c == '{'; +void TextEdit::Text::set_tab_size(int p_tab_size) { + tab_size = p_tab_size; } -static bool _is_pair_symbol(char32_t c) { - return _is_pair_left_symbol(c) || _is_pair_right_symbol(c); +int TextEdit::Text::get_tab_size() const { + return tab_size; } -static char32_t _get_right_pair_symbol(char32_t c) { - if (c == '"') { - return '"'; - } - if (c == '\'') { - return '\''; - } - if (c == '(') { - return ')'; +void TextEdit::Text::set_font_features(const Dictionary &p_features) { + if (opentype_features.hash() == p_features.hash()) { + return; } - if (c == '[') { - return ']'; + opentype_features = p_features; + is_dirty = true; +} + +void TextEdit::Text::set_direction_and_language(TextServer::Direction p_direction, const String &p_language) { + if (direction == p_direction && language == p_language) { + return; } - if (c == '{') { - 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; } - return 0; + draw_control_chars = p_draw_control_chars; + is_dirty = true; } -static int _find_first_non_whitespace_column_of_line(const String &line) { - int left = 0; - while (left < line.length() && _is_whitespace(line[left])) { - left++; +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 left; + 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); -void TextEdit::Text::set_font(const Ref<Font> &p_font) { - font = p_font; + return text[p_line].data_buf->get_line_size(p_wrap_index).y; } -void TextEdit::Text::set_indent_size(int p_indent_size) { - indent_size = p_indent_size; +void TextEdit::Text::set_width(float p_width) { + width = p_width; } -void TextEdit::Text::_update_line_cache(int p_line) const { - int w = 0; +int TextEdit::Text::get_line_wrap_amount(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); - int len = text[p_line].data.length(); - const char32_t *str = text[p_line].data.get_data(); + return text[p_line].data_buf->get_line_count() - 1; +} - // Update width. +Vector<Vector2i> TextEdit::Text::get_line_wrap_ranges(int p_line) const { + Vector<Vector2i> ret; + ERR_FAIL_INDEX_V(p_line, text.size(), ret); - for (int i = 0; i < len; i++) { - w += get_char_width(str[i], str[i + 1], w); + for (int i = 0; i < text[p_line].data_buf->get_line_count(); i++) { + ret.push_back(text[p_line].data_buf->get_line_range(i)); } - - text.write[p_line].width_cache = w; - text.write[p_line].wrap_amount_cache = -1; + return ret; } -int TextEdit::Text::get_line_width(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), -1); - - if (text[p_line].width_cache == -1) { - _update_line_cache(p_line); - } +const Ref<TextParagraph> TextEdit::Text::get_line_data(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), Ref<TextParagraph>()); + return text[p_line].data_buf; +} - return text[p_line].width_cache; +_FORCE_INLINE_ const String &TextEdit::Text::operator[](int p_line) const { + return text[p_line].data; } -void TextEdit::Text::set_line_wrap_amount(int p_line, int p_wrap_amount) const { +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()); - text.write[p_line].wrap_amount_cache = p_wrap_amount; -} + if (font.is_null() || font_size <= 0) { + return; // Not in tree? + } -int TextEdit::Text::get_line_wrap_amount(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), -1); + text.write[p_line].data_buf->clear(); + text.write[p_line].data_buf->set_width(width); + text.write[p_line].data_buf->set_direction((TextServer::Direction)direction); + text.write[p_line].data_buf->set_preserve_control(draw_control_chars); + if (p_ime_text.length() > 0) { + text.write[p_line].data_buf->add_string(p_ime_text, font, font_size, opentype_features, language); + if (!p_bidi_override.is_empty()) { + TS->shaped_text_set_bidi_override(text.write[p_line].data_buf->get_rid(), p_bidi_override); + } + } else { + text.write[p_line].data_buf->add_string(text[p_line].data, font, font_size, opentype_features, language); + if (!text[p_line].bidi_override.is_empty()) { + TS->shaped_text_set_bidi_override(text.write[p_line].data_buf->get_rid(), text[p_line].bidi_override); + } + } - return text[p_line].wrap_amount_cache; + // Apply tab align. + if (tab_size > 0) { + Vector<float> tabs; + tabs.push_back(font->get_char_size(' ', 0, font_size).width * tab_size); + text.write[p_line].data_buf->tab_align(tabs); + } } -void TextEdit::Text::clear_width_cache() { +void TextEdit::Text::invalidate_all_lines() { for (int i = 0; i < text.size(); i++) { - text.write[i].width_cache = -1; + text.write[i].data_buf->set_width(width); + 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); + } } } -void TextEdit::Text::clear_wrap_cache() { +void TextEdit::Text::invalidate_all() { + if (!is_dirty) { + return; + } + for (int i = 0; i < text.size(); i++) { - text.write[i].wrap_amount_cache = -1; + invalidate_cache(i); } + is_dirty = false; } void TextEdit::Text::clear() { text.clear(); - insert(0, ""); + insert(0, "", Vector<Vector2i>()); } int TextEdit::Text::get_max_width(bool p_exclude_hidden) const { @@ -184,46 +223,29 @@ int TextEdit::Text::get_max_width(bool p_exclude_hidden) const { return max; } -void TextEdit::Text::set(int p_line, const String &p_text) { +void TextEdit::Text::set(int p_line, const String &p_text, const Vector<Vector2i> &p_bidi_override) { ERR_FAIL_INDEX(p_line, text.size()); - text.write[p_line].width_cache = -1; - text.write[p_line].wrap_amount_cache = -1; text.write[p_line].data = p_text; + text.write[p_line].bidi_override = p_bidi_override; + invalidate_cache(p_line); } -void TextEdit::Text::insert(int p_at, const String &p_text) { +void TextEdit::Text::insert(int p_at, const String &p_text, const Vector<Vector2i> &p_bidi_override) { Line line; line.gutters.resize(gutter_count); - line.marked = false; line.hidden = false; - line.width_cache = -1; - line.wrap_amount_cache = -1; line.data = p_text; + line.bidi_override = p_bidi_override; text.insert(p_at, line); + + invalidate_cache(p_at); } void TextEdit::Text::remove(int p_at) { text.remove(p_at); } -int TextEdit::Text::get_char_width(char32_t c, char32_t next_c, int px) const { - int tab_w = font->get_char_size(' ').width * indent_size; - int w = 0; - - if (c == '\t') { - int left = px % tab_w; - if (left == 0) { - w = tab_w; - } else { - w = tab_w - px % tab_w; // Is right. - } - } else { - w = font->get_char_size(c, next_c).width; - } - return w; -} - void TextEdit::Text::add_gutter(int p_at) { for (int i = 0; i < text.size(); i++) { if (p_at < 0 || p_at > gutter_count) { @@ -248,267 +270,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(MARGIN_TOP))); - v_scroll->set_end(Point2(size.width, size.height - cache.style_normal->get_margin(MARGIN_TOP) - cache.style_normal->get_margin(MARGIN_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(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(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(); - } -} - -void TextEdit::_update_selection_mode_pointer() { - dragging_selection = true; - Point2 mp = get_local_mouse_position(); - - 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_position(); - - int row, col; - _get_mouse_pos(Point2i(mp.x, mp.y), row, col); - - String line = text[row]; - int beg = CLAMP(col, 0, line.length()); - // If its the first selection and on whitespace make sure we grab the word instead. - if (!selection.active) { - while (beg > 0 && line[beg] <= 32) { - beg--; - } - } - int end = beg; - bool symbol = beg < line.length() && _is_symbol(line[beg]); - - // Get the word end and begin points. - while (beg > 0 && line[beg - 1] > 32 && (symbol == _is_symbol(line[beg - 1]))) { - beg--; - } - while (end < line.length() && line[end + 1] > 32 && (symbol == _is_symbol(line[end + 1]))) { - end++; - } - if (end < line.length()) { - end += 1; - } - - // 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_position(); - - 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_position(); - - int xmargin_end = get_size().width - cache.style_normal->get_margin(MARGIN_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_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_position(); - 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(); + _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")); } } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_THEME_CHANGED: { _update_caches(); - _update_wrap_at(); + _update_wrap_at_column(true); } break; case NOTIFICATION_WM_WINDOW_FOCUS_IN: { window_has_focus = true; @@ -543,8 +335,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; } @@ -556,55 +348,37 @@ void TextEdit::_notification(int p_what) { } Size2 size = get_size(); - if ((!has_focus() && !menu->has_focus()) || !window_has_focus) { + bool rtl = is_layout_rtl(); + 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(MARGIN_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(MARGIN_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 ascent = cache.font->get_ascent(); + int visible_rows = get_visible_line_count() + 1; - int visible_rows = get_visible_rows() + 1; + Color color = !editable ? font_readonly_color : font_color; - Color color = readonly ? cache.font_color_readonly : cache.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').width * line_length_guideline_hard_col - cursor.x_ofs; - if (hard_x > xmargin_beg && hard_x < xmargin_end) { - 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').width * line_length_guideline_soft_col - cursor.x_ofs; - if (hard_x != soft_x && soft_x > xmargin_beg && soft_x < xmargin_end) { - 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; @@ -616,10 +390,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 == '[') { @@ -633,8 +407,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. @@ -684,8 +458,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 == ']') { @@ -699,8 +473,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. @@ -751,28 +525,23 @@ void TextEdit::_notification(int p_what) { } } - Point2 cursor_pos; - int cursor_insert_offset_y = 0; - // Get the highlighted words. - String highlighted_text = get_selection_text(); + String highlighted_text = get_selected_text(); - // Check if highlighted words contains only whitespaces (tabs or spaces). + // 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; @@ -781,17 +550,20 @@ 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 - Color viewport_color = (cache.background_color.get_v() < 0.5) ? Color(1, 1, 1, 0.1) : Color(0, 0, 0, 0.1); - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), viewport_offset_y, cache.minimap_width, viewport_height), viewport_color); + Color viewport_color = (background_color.get_v() < 0.5) ? Color(1, 1, 1, 0.1) : Color(0, 0, 0, 0.1); + if (rtl) { + 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, minimap_width, viewport_height), viewport_color); + } for (int i = 0; i < minimap_draw_amount; i++) { minimap_line++; @@ -799,7 +571,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; @@ -812,13 +584,15 @@ void TextEdit::_notification(int p_what) { Dictionary color_map = _get_line_syntax_highlighting(minimap_line); - Color current_color = cache.font_color; - if (readonly) { - current_color = cache.font_color_readonly; + Color line_background_color = text.get_line_background_color(minimap_line); + line_background_color.a *= 0.6; + 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++) { @@ -831,7 +605,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; @@ -840,8 +614,18 @@ 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) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2((xmargin_end + 2), i * 3, cache.minimap_width, 2), cache.current_line_color); + 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) - 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, 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) - 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, minimap_width, 2), line_background_color); + } } Color previous_color; @@ -850,8 +634,8 @@ void TextEdit::_notification(int p_what) { 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_color_readonly.a; + if (!editable) { + current_color.a = font_readonly_color.a; } } color = current_color; @@ -861,7 +645,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) { @@ -888,7 +672,11 @@ void TextEdit::_notification(int p_what) { // take one for zero indexing, and if we hit whitespace / the end of a word. int chars = MAX(0, (j - (characters - 1)) - (is_whitespace ? 1 : 0)) + 1; int char_x_ofs = indent_px + ((xmargin_end + minimap_char_size.x) + (minimap_char_size.x * chars)) + tabs; - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_x_ofs, minimap_line_height * i), Point2(minimap_char_size.x * characters, minimap_char_size.y)), previous_color); + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(size.width - char_x_ofs - minimap_char_size.x * characters, minimap_line_height * i), Point2(minimap_char_size.x * characters, minimap_char_size.y)), previous_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_x_ofs, minimap_line_height * i), Point2(minimap_char_size.x * characters, minimap_char_size.y)), previous_color); + } } if (out_of_bounds) { @@ -906,7 +694,19 @@ void TextEdit::_notification(int p_what) { } } + int top_limit_y = 0; + int bottom_limit_y = get_size().height; + if (!editable) { + top_limit_y += style_readonly->get_margin(SIDE_TOP); + bottom_limit_y -= style_readonly->get_margin(SIDE_BOTTOM); + } else { + top_limit_y += style_normal->get_margin(SIDE_TOP); + bottom_limit_y -= style_normal->get_margin(SIDE_BOTTOM); + } + // draw main text + caret.visible = false; + int row_height = get_line_height(); int line = first_visible_line; for (int i = 0; i < draw_amount; i++) { line++; @@ -915,7 +715,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; @@ -926,20 +726,17 @@ void TextEdit::_notification(int p_what) { continue; } - const String &fullstr = text[line]; - Dictionary color_map = _get_line_syntax_highlighting(line); // Ensure we at least use the font color. - Color current_color = readonly ? cache.font_color_readonly : cache.font_color; + Color current_color = !editable ? font_readonly_color : font_color; - bool underlined = false; + 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); - int last_wrap_column = 0; + 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 + 1; line_wrap_index++) { + for (int line_wrap_index = 0; line_wrap_index <= line_wrap_amount; line_wrap_index++) { if (line_wrap_index != 0) { i++; if (i >= draw_amount) { @@ -948,75 +745,77 @@ void TextEdit::_notification(int p_what) { } const String &str = wrap_rows[line_wrap_index]; - int indent_px = line_wrap_index != 0 ? get_indent_level(line) * cache.font->get_char_size(' ').width : 0; - if (indent_px >= wrap_at) { - indent_px = 0; - } - - if (line_wrap_index > 0) { - last_wrap_column += wrap_rows[line_wrap_index - 1].length(); - } + int char_margin = xmargin_beg - caret.x_ofs; - int char_margin = xmargin_beg - cursor.x_ofs; - char_margin += indent_px; - int char_ofs = 0; - - int ofs_readonly = 0; int ofs_x = 0; - if (readonly) { - ofs_readonly = cache.style_readonly->get_offset().y / 2; - ofs_x = cache.style_readonly->get_offset().x / 2; + int ofs_y = 0; + 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 = style_normal->get_offset().y / 2; } - int ofs_y = (i * get_row_height() + cache.line_spacing / 2) + ofs_readonly; - ofs_y -= cursor.wrap_ofs * get_row_height(); - ofs_y -= get_v_scroll_offset() * get_row_height(); - - // Check if line contains highlighted word. - int highlighted_text_col = -1; - int search_text_col = -1; - int highlighted_word_col = -1; + ofs_y += i * row_height + line_spacing / 2; + ofs_y -= caret.wrap_ofs * row_height; + ofs_y -= _get_v_scroll_offset() * row_height; - if (!search_text.empty()) { - search_text_col = _get_column_pos_of_word(search_text, str, search_flags, 0); + bool clipped = false; + if (ofs_y + row_height < top_limit_y) { + // Line is outside the top margin, clip current line. + // Still need to go through the process to prepare color changes for next lines. + clipped = true; } - if (highlighted_text.length() != 0 && highlighted_text != search_text) { - highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); + if (ofs_y > bottom_limit_y) { + // Line is outside the bottom margin, clip any remaining text. + i = draw_amount; + break; } - if (select_identifiers_enabled && highlighted_word.length() != 0) { - if (_is_char(highlighted_word[0]) || highlighted_word[0] == '.') { - highlighted_word_col = _get_column_pos_of_word(highlighted_word, fullstr, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); + if (text.get_line_background_color(line) != Color(0, 0, 0, 0)) { + if (rtl) { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(size.width - ofs_x - xmargin_end, ofs_y, xmargin_end - xmargin_beg, row_height), text.get_line_background_color(line)); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, xmargin_end - xmargin_beg, row_height), text.get_line_background_color(line)); } } - if (text.is_marked(line)) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, xmargin_end - xmargin_beg, get_row_height()), cache.mark_color); - } - if (str.length() == 0) { - // Draw line background if empty as we won't loop at at all. - if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, get_row_height()), cache.current_line_color); + // Draw line background if empty as we won't loop at all. + 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), current_line_color); + } else { + 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(' ').width; - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, char_w, get_row_height()), cache.selection_color); + 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), selection_color); + } else { + 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) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(0, ofs_y, xmargin_beg + ofs_x, get_row_height()), cache.current_line_color); + 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), current_line_color); + } else { + RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(ofs_x, ofs_y, xmargin_end, row_height), current_line_color); + } } } 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(MARGIN_LEFT); + int gutter_offset = style_normal->get_margin(SIDE_LEFT); for (int g = 0; g < gutters.size(); g++) { const GutterInfo gutter = gutters[g]; @@ -1031,16 +830,23 @@ void TextEdit::_notification(int p_what) { break; } - int yofs = ofs_y + (get_row_height() - cache.font->get_height()) / 2; - cache.font->draw(ci, Point2(gutter_offset + ofs_x, yofs + cache.font->get_ascent()), text, get_line_gutter_item_color(line, g)); + Ref<TextLine> tl; + tl.instantiate(); + tl->add_string(text, font, font_size); + + int yofs = ofs_y + (row_height - tl->get_size().y) / 2; + 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; } - Rect2i gutter_rect = Rect2i(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, get_row_height())); + Rect2 gutter_rect = Rect2(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, row_height)); int horizontal_padding = gutter_rect.size.x / 6; int vertical_padding = gutter_rect.size.y / 6; @@ -1048,13 +854,28 @@ void TextEdit::_notification(int p_what) { gutter_rect.position += Point2(horizontal_padding, vertical_padding); gutter_rect.size -= Point2(horizontal_padding, vertical_padding) * 2; + // Correct icon aspect ratio. + float icon_ratio = icon->get_width() / icon->get_height(); + float gutter_ratio = gutter_rect.size.x / gutter_rect.size.y; + if (gutter_ratio > icon_ratio) { + gutter_rect.size.x = floor(icon->get_width() * (gutter_rect.size.y / icon->get_height())); + } else { + gutter_rect.size.y = floor(icon->get_height() * (gutter_rect.size.x / icon->get_width())); + } + if (rtl) { + gutter_rect.position.x = size.width - gutter_rect.position.x - gutter_rect.size.x; + } + 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) { - Rect2i gutter_rect = Rect2i(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, get_row_height())); + Rect2i gutter_rect = Rect2i(Point2i(gutter_offset, ofs_y), Size2i(gutter.width, row_height)); + if (rtl) { + gutter_rect.position.x = size.width - gutter_rect.position.x - gutter_rect.size.x; + } cdo->call(gutter.custom_draw_callback, line, g, Rect2(gutter_rect)); } } @@ -1065,483 +886,355 @@ void TextEdit::_notification(int p_what) { } } - // Loop through characters in one line. - int j = 0; - for (; 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_color_readonly.a) { - current_color.a = cache.font_color_readonly.a; - } - } - color = current_color; - - int char_w; + // Draw line. + RID rid = ldata->get_line_rid(line_wrap_index); + float text_height = TS->shaped_text_get_size(rid).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM); - // Handle tabulator. - char_w = text.get_char_width(str[j], str[j + 1], char_ofs); - - if ((char_ofs + char_margin) < xmargin_beg) { - char_ofs += char_w; + if (rtl) { + char_margin = size.width - char_margin - TS->shaped_text_get_size(rid).x; + } - // Line highlighting handle horizontal clipping. - if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { - if (j == str.length() - 1) { - // End of line when last char is skipped. - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, xmargin_end - (char_ofs + char_margin + char_w), get_row_height()), cache.current_line_color); - } else if ((char_ofs + char_margin) > xmargin_beg) { - // Char next to margin is skipped. - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(xmargin_beg + ofs_x, ofs_y, (char_ofs + char_margin) - (xmargin_beg + ofs_x), get_row_height()), cache.current_line_color); - } + if (!clipped && selection.active && line >= selection.from_line && line <= selection.to_line) { // Selection + int sel_from = (line > selection.from_line) ? TS->shaped_text_get_range(rid).x : selection.from_column; + int sel_to = (line < selection.to_line) ? TS->shaped_text_get_range(rid).y : selection.to_column; + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, sel_from, sel_to); + 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) { + continue; } - continue; - } - - if ((char_ofs + char_margin + char_w) >= xmargin_end) { - break; - } - - bool in_search_result = false; - - if (search_text_col != -1) { - // If we are at the end check for new search result on same line. - if (j >= search_text_col + search_text.length()) { - search_text_col = _get_column_pos_of_word(search_text, str, search_flags, j); + if (rect.position.x < xmargin_beg) { + rect.size.x -= (xmargin_beg - rect.position.x); + rect.position.x = xmargin_beg; } - - in_search_result = j >= search_text_col && j < search_text_col + search_text.length(); - - if (in_search_result) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin, ofs_y), Size2i(char_w, get_row_height())), cache.search_result_color); + if (rect.position.x + rect.size.x > xmargin_end) { + rect.size.x = xmargin_end - rect.position.x; } + draw_rect(rect, selection_color, true); } + } - // Current line highlighting. - bool in_selection = (selection.active && line >= selection.from_line && line <= selection.to_line && (line > selection.from_line || last_wrap_column + j >= selection.from_column) && (line < selection.to_line || last_wrap_column + j < selection.to_column)); - - if (line == cursor.line && cursor_wrap_index == line_wrap_index && highlight_current_line) { - // Draw the wrap indent offset highlight. - if (line_wrap_index != 0 && j == 0) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(char_ofs + char_margin + ofs_x - indent_px, ofs_y, indent_px, get_row_height()), cache.current_line_color); - } - // If its the last char draw to end of the line. - if (j == str.length() - 1) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(char_ofs + char_margin + char_w + ofs_x, ofs_y, xmargin_end - (char_ofs + char_margin + char_w), get_row_height()), cache.current_line_color); - } - // Actual text. - if (!in_selection) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(char_w, get_row_height())), cache.current_line_color); + int start = TS->shaped_text_get_range(rid).x; + if (!clipped && !search_text.is_empty()) { // Search highhlight + int search_text_col = _get_column_pos_of_word(search_text, str, search_flags, 0); + while (search_text_col != -1) { + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, search_text_col + start, search_text_col + search_text.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) { + continue; + } + 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) { + rect.size.x = xmargin_end - rect.position.x; + } + draw_rect(rect, search_result_color, true); + draw_rect(rect, search_result_border_color, false); } - } - if (in_selection) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(char_w, get_row_height())), cache.selection_color); + search_text_col = _get_column_pos_of_word(search_text, str, search_flags, search_text_col + 1); } + } - if (in_search_result) { - Color border_color = (line == search_result_line && j >= search_result_col && j < search_result_col + search_text.length()) ? cache.font_color : cache.search_result_border_color; - - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(char_w, 1)), border_color); - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y + get_row_height() - 1), Size2i(char_w, 1)), border_color); - - if (j == search_text_col) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(1, get_row_height())), border_color); - } - if (j == search_text_col + search_text.length() - 1) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + char_w + ofs_x - 1, ofs_y), Size2i(1, get_row_height())), border_color); - } - } - - if (highlight_all_occurrences && !only_whitespaces_highlighted) { - if (highlighted_text_col != -1) { - // If we are at the end check for new word on same line. - if (j > highlighted_text_col + highlighted_text.length()) { - highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, j); - } - - bool in_highlighted_word = (j >= highlighted_text_col && j < highlighted_text_col + highlighted_text.length()); - - // If this is the original highlighted text we don't want to highlight it again. - if (cursor.line == line && cursor_wrap_index == line_wrap_index && (cursor.column >= highlighted_text_col && cursor.column <= highlighted_text_col + highlighted_text.length())) { - in_highlighted_word = false; + if (!clipped && highlight_all_occurrences && !only_whitespaces_highlighted && !highlighted_text.is_empty()) { // Highlight + int highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, 0); + while (highlighted_text_col != -1) { + Vector<Vector2> sel = TS->shaped_text_get_selection(rid, highlighted_text_col + start, highlighted_text_col + highlighted_text.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) { + continue; } - - if (in_highlighted_word) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2i(char_ofs + char_margin + ofs_x, ofs_y), Size2i(char_w, get_row_height())), cache.word_highlighted_color); + 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) { + rect.size.x = xmargin_end - rect.position.x; } + draw_rect(rect, word_highlighted_color); } - } - if (highlighted_word_col != -1) { - if (j + last_wrap_column > highlighted_word_col + highlighted_word.length()) { - highlighted_word_col = _get_column_pos_of_word(highlighted_word, fullstr, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, j + last_wrap_column); - } - underlined = (j + last_wrap_column >= highlighted_word_col && j + last_wrap_column < highlighted_word_col + highlighted_word.length()); + highlighted_text_col = _get_column_pos_of_word(highlighted_text, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, highlighted_text_col + 1); } + } - if (brace_matching_enabled) { - int yofs = ofs_y + (get_row_height() - cache.font->get_height()) / 2; - if ((brace_open_match_line == line && brace_open_match_column == last_wrap_column + j) || - (cursor.column == last_wrap_column + j && cursor.line == line && cursor_wrap_index == line_wrap_index && (brace_open_matching || brace_open_mismatch))) { - if (brace_open_mismatch) { - color = cache.brace_mismatch_color; + 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 + 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) { + continue; + } + 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) { + rect.size.x = xmargin_end - rect.position.x; + } + 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); } - drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, yofs + ascent), '_', str[j + 1], in_selection && override_selected_font_color ? cache.font_color_selected : color); - } - if ((brace_close_match_line == line && brace_close_match_column == last_wrap_column + j) || - (cursor.column == last_wrap_column + j + 1 && cursor.line == line && cursor_wrap_index == line_wrap_index && (brace_close_matching || brace_close_mismatch))) { - if (brace_close_mismatch) { - color = cache.brace_mismatch_color; - } - drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, yofs + ascent), '_', str[j + 1], in_selection && override_selected_font_color ? cache.font_color_selected : color); + highlighted_word_col = _get_column_pos_of_word(lookup_symbol_word, str, SEARCH_MATCH_CASE | SEARCH_WHOLE_WORDS, highlighted_word_col + 1); } } + } - if (cursor.column == last_wrap_column + j && cursor.line == line && cursor_wrap_index == line_wrap_index) { - cursor_pos = Point2i(char_ofs + char_margin + ofs_x, ofs_y); - cursor_pos.y += (get_row_height() - cache.font->get_height()) / 2; - - if (insert_mode) { - cursor_insert_offset_y = (cache.font->get_height() - 3); - cursor_pos.y += cursor_insert_offset_y; - } - - int caret_w = (str[j] == '\t') ? cache.font->get_char_size(' ').width : char_w; - if (ime_text.length() > 0) { - int ofs = 0; - while (true) { - if (ofs >= ime_text.length()) { - break; - } + ofs_y += (row_height - text_height) / 2; - char32_t cchar = ime_text[ofs]; - char32_t next = ime_text[ofs + 1]; - int im_char_width = cache.font->get_char_size(cchar, next).width; + const Vector<TextServer::Glyph> visual = TS->shaped_text_get_glyphs(rid); + const TextServer::Glyph *glyphs = visual.ptr(); + int gl_size = visual.size(); - if ((char_ofs + char_margin + im_char_width) >= xmargin_end) { - break; - } - - bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 3)), color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 1)), color); + ofs_y += ldata->get_line_ascent(line_wrap_index); + int char_ofs = 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, outline_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, outline_color); } - - drawer.draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color); - - char_ofs += im_char_width; - ofs++; } + char_ofs += glyphs[j].advance; } - if (ime_text.length() == 0) { - if (draw_caret) { - if (insert_mode) { -#ifdef TOOLS_ENABLED - int caret_h = (block_caret) ? 4 : 2 * EDSCALE; -#else - int caret_h = (block_caret) ? 4 : 2; -#endif - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, caret_h)), cache.caret_color); - } else { -#ifdef TOOLS_ENABLED - caret_w = (block_caret) ? caret_w : 2 * EDSCALE; -#else - caret_w = (block_caret) ? caret_w : 2; -#endif - - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, cache.font->get_height())), cache.caret_color); - } - } + if ((char_ofs + char_margin) >= xmargin_end) { + break; } } - - if (cursor.column == last_wrap_column + j && cursor.line == line && cursor_wrap_index == line_wrap_index && block_caret && draw_caret && !insert_mode) { - color = cache.caret_background_color; - } else if (block_caret) { - color = readonly ? cache.font_color_readonly : cache.font_color; - } - - if (str[j] >= 32) { - int yofs = ofs_y + (get_row_height() - cache.font->get_height()) / 2; - int w = drawer.draw_char(ci, Point2i(char_ofs + char_margin + ofs_x, yofs + ascent), str[j], str[j + 1], in_selection && override_selected_font_color ? cache.font_color_selected : color); - if (underlined) { - float line_width = cache.font->get_underline_thickness(); -#ifdef TOOLS_ENABLED - line_width *= EDSCALE; -#endif - - draw_rect(Rect2(char_ofs + char_margin + ofs_x, yofs + ascent + cache.font->get_underline_position(), w, line_width), in_selection && override_selected_font_color ? cache.font_color_selected : color); + 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 (!editable && current_color.a > font_readonly_color.a) { + current_color.a = font_readonly_color.a; } - } else if (draw_tabs && str[j] == '\t') { - int yofs = (get_row_height() - cache.tab_icon->get_height()) / 2; - cache.tab_icon->draw(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + yofs), in_selection && override_selected_font_color ? cache.font_color_selected : color); - } - - if (draw_spaces && str[j] == ' ') { - int yofs = (get_row_height() - cache.space_icon->get_height()) / 2; - cache.space_icon->draw(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + yofs), in_selection && override_selected_font_color ? cache.font_color_selected : color); - } - - char_ofs += char_w; - - if (line_wrap_index == line_wrap_amount && j == str.length() - 1 && is_folded(line)) { - int yofs = (get_row_height() - cache.folded_eol_icon->get_height()) / 2; - int xofs = cache.folded_eol_icon->get_width() / 2; - Color eol_color = cache.code_folding_color; - eol_color.a = 1; - cache.folded_eol_icon->draw(ci, Point2(char_ofs + char_margin + xofs + ofs_x, ofs_y + yofs), eol_color); } - } - if (cursor.column == (last_wrap_column + j) && cursor.line == line && cursor_wrap_index == line_wrap_index && (char_ofs + char_margin) >= xmargin_beg) { - cursor_pos = Point2i(char_ofs + char_margin + ofs_x, ofs_y); - cursor_pos.y += (get_row_height() - cache.font->get_height()) / 2; + if (selection.active && line >= selection.from_line && line <= selection.to_line) { // Selection + int sel_from = (line > selection.from_line) ? TS->shaped_text_get_range(rid).x : selection.from_column; + int sel_to = (line < selection.to_line) ? TS->shaped_text_get_range(rid).y : selection.to_column; - if (insert_mode) { - cursor_insert_offset_y = cache.font->get_height() - 3; - cursor_pos.y += cursor_insert_offset_y; + if (glyphs[j].start >= sel_from && glyphs[j].end <= sel_to && override_selected_font_color) { + current_color = font_selected_color; + } } - if (ime_text.length() > 0) { - int ofs = 0; - while (true) { - if (ofs >= ime_text.length()) { - break; - } - char32_t cchar = ime_text[ofs]; - char32_t next = ime_text[ofs + 1]; - int im_char_width = cache.font->get_char_size(cchar, next).width; - - if ((char_ofs + char_margin + im_char_width) >= xmargin_end) { - break; + int char_pos = char_ofs + char_margin + ofs_x; + if (char_pos >= xmargin_beg) { + if (highlight_matching_braces_enabled) { + if ((brace_open_match_line == line && brace_open_match_column == glyphs[j].start) || + (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 = brace_mismatch_color; + } + 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); } - bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y; - if (selected) { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 3)), color); - } else { - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 1)), color); + if ((brace_close_match_line == line && brace_close_match_column == glyphs[j].start) || + (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 = brace_mismatch_color; + } + 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); } + } - drawer.draw_char(ci, Point2(char_ofs + char_margin + ofs_x, ofs_y + ascent), cchar, next, color); - - char_ofs += im_char_width; - ofs++; + if (draw_tabs && ((glyphs[j].flags & TextServer::GRAPHEME_IS_TAB) == TextServer::GRAPHEME_IS_TAB)) { + 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 - 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); } } - if (ime_text.length() == 0) { - if (draw_caret) { - if (insert_mode) { - int char_w = cache.font->get_char_size(' ').width; -#ifdef TOOLS_ENABLED - int caret_h = (block_caret) ? 4 : 2 * EDSCALE; -#else - int caret_h = (block_caret) ? 4 : 2; -#endif - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(char_w, caret_h)), cache.caret_color); - } else { - int char_w = cache.font->get_char_size(' ').width; -#ifdef TOOLS_ENABLED - int caret_w = (block_caret) ? char_w : 2 * EDSCALE; -#else - int caret_w = (block_caret) ? char_w : 2; -#endif - RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, cache.font->get_height())), cache.caret_color); + for (int k = 0; k < glyphs[j].repeat; k++) { + if (!clipped && (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(glyphs[j].font_rid, ci, glyphs[j].font_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, current_color); + } else if ((glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + TS->draw_hex_code_box(ci, glyphs[j].font_size, Vector2(char_margin + char_ofs + ofs_x + glyphs[j].x_off, ofs_y + glyphs[j].y_off), glyphs[j].index, current_color); } } + char_ofs += glyphs[j].advance; } - } - } - } - - bool completion_below = false; - if (completion_active && completion_options.size() > 0) { - // Code completion box. - Ref<StyleBox> csb = get_theme_stylebox("completion"); - int maxlines = get_theme_constant("completion_lines"); - int cmax_width = get_theme_constant("completion_max_width") * cache.font->get_char_size('x').x; - int scrollw = get_theme_constant("completion_scroll_width"); - Color scrollc = get_theme_color("completion_scroll_color"); - - const int completion_options_size = completion_options.size(); - int lines = MIN(completion_options_size, maxlines); - int w = 0; - int h = lines * get_row_height(); - int nofs = cache.font->get_string_size(completion_base).width; - - if (completion_options_size < 50) { - for (int i = 0; i < completion_options_size; i++) { - int w2 = MIN(cache.font->get_string_size(completion_options[i].display).x, cmax_width); - if (w2 > w) { - w = w2; + if ((char_ofs + char_margin) >= xmargin_end) { + break; } } - } else { - w = cmax_width; - } - - // Add space for completion icons. - const int icon_hsep = get_theme_constant("hseparation", "ItemList"); - Size2 icon_area_size(get_row_height(), get_row_height()); - w += icon_area_size.width + icon_hsep; - - int line_from = CLAMP(completion_index - lines / 2, 0, completion_options_size - lines); - - for (int i = 0; i < lines; i++) { - int l = line_from + i; - ERR_CONTINUE(l < 0 || l >= completion_options_size); - if (completion_options[l].default_value.get_type() == Variant::COLOR) { - w += icon_area_size.width; - break; - } - } - - int th = h + csb->get_minimum_size().y; - - if (cursor_pos.y + get_row_height() + th > get_size().height) { - completion_rect.position.y = cursor_pos.y - th - (cache.line_spacing / 2.0f) - cursor_insert_offset_y; - } else { - completion_rect.position.y = cursor_pos.y + cache.font->get_height() + (cache.line_spacing / 2.0f) + csb->get_offset().y - cursor_insert_offset_y; - completion_below = true; - } - - if (cursor_pos.x - nofs + w + scrollw > get_size().width) { - completion_rect.position.x = get_size().width - w - scrollw; - } else { - completion_rect.position.x = cursor_pos.x - nofs; - } - - completion_rect.size.width = w + 2; - completion_rect.size.height = h; - if (completion_options_size <= maxlines) { - scrollw = 0; - } - - draw_style_box(csb, Rect2(completion_rect.position - csb->get_offset(), completion_rect.size + csb->get_minimum_size() + Size2(scrollw, 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(scrollw, 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(nofs, completion_rect.size.width - (icon_area_size.x + icon_hsep)), completion_rect.size.height)), cache.completion_existing_color); - - for (int i = 0; i < lines; i++) { - int l = line_from + i; - ERR_CONTINUE(l < 0 || l >= completion_options_size); - int yofs = (get_row_height() - cache.font->get_height()) / 2; - Point2 title_pos(completion_rect.position.x, completion_rect.position.y + i * get_row_height() + cache.font->get_ascent() + 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 * get_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; - - 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); - } - - draw_string(cache.font, title_pos, completion_options[l].display, completion_options[l].font_color, completion_rect.size.width - (icon_area_size.x + icon_hsep)); - } - - if (scrollw) { - // 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, scrollw, 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 (completion_hint != "") { - if (completion_active) { - if (completion_below && !callhint_below) { - show_hint = true; - } else if (!completion_below && callhint_below) { - show_hint = true; + // 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 - folded_eol_icon->get_height()) / 2 - ldata->get_line_ascent(line_wrap_index); + Color eol_color = code_folding_color; + eol_color.a = 1; + folded_eol_icon->draw(ci, Point2(xofs, ofs_y + yofs), eol_color); + } } - } 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).x; - max_w = MAX(len, max_w); - if (i == 0) { - offset = font->get_string_size(l.substr(0, l.find(String::chr(0xFFFF)))).x; - } else { - spacing += cache.line_spacing; - } - } + // Carets +#ifdef TOOLS_ENABLED + int caret_width = Math::round(EDSCALE); +#else + int caret_width = 1; +#endif - Size2 size2 = Size2(max_w, sc * font->get_height() + spacing); - Size2 minsize = size2 + sb->get_minimum_size(); + 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 (completion_hint_offset == -0xFFFF) { - completion_hint_offset = cursor_pos.x - offset; - } + 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, caret.column, l_caret, l_dir, t_caret, t_dir); + } else { + // No carets, add one at the start. + 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)); + } else { + l_dir = TextServer::DIRECTION_LTR; + l_caret = Rect2(Vector2(char_ofs, -h / 2), Size2(caret_width * 4, h)); + } + } - Point2 hint_ofs = Vector2(completion_hint_offset, cursor_pos.y) + callhint_offset; + if ((l_caret != Rect2() && (l_dir == TextServer::DIRECTION_AUTO || l_dir == (TextServer::Direction)input_direction)) || (t_caret == Rect2())) { + caret.draw_pos.x = char_margin + ofs_x + l_caret.position.x; + } else { + caret.draw_pos.x = char_margin + ofs_x + t_caret.position.x; + } - if (callhint_below) { - hint_ofs.y += get_row_height() + sb->get_offset().y; - } else { - hint_ofs.y -= minsize.y + sb->get_offset().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_style_box(sb, Rect2(hint_ofs, minsize)); + 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 = caret_width; - spacing = 0; - for (int i = 0; i < sc; i++) { - int begin = 0; - int end = 0; - String l = completion_hint.get_slice("\n", i); + draw_rect(l_caret, caret_color); - if (l.find(String::chr(0xFFFF)) != -1) { - begin = font->get_string_size(l.substr(0, l.find(String::chr(0xFFFF)))).x; - end = font->get_string_size(l.substr(0, l.rfind(String::chr(0xFFFF)))).x; - } + t_caret.position += Vector2(char_margin + ofs_x, ofs_y); + t_caret.size.x = caret_width; - Point2 round_ofs = hint_ofs + sb->get_offset() + Vector2(0, font->get_ascent() + font->get_height() * i + spacing); - round_ofs = round_ofs.round(); - draw_string(font, round_ofs, l.replace(String::chr(0xFFFF), ""), font_color); - if (end > 0) { - Vector2 b = hint_ofs + sb->get_offset() + Vector2(begin, font->get_height() + font->get_height() * i + spacing - 1); - draw_line(b, b + Vector2(end - begin, 0), font_color); + draw_rect(t_caret, caret_color); + } + } + } + } else { + { + // IME Intermediate text range. + 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) { + continue; + } + 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) { + rect.size.x = xmargin_end - rect.position.x; + } + rect.size.y = caret_width; + draw_rect(rect, caret_color); + caret.draw_pos.x = rect.position.x; + } + } + { + // IME caret. + 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) { + continue; + } + 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) { + rect.size.x = xmargin_end - rect.position.x; + } + rect.size.y = caret_width * 3; + draw_rect(rect, caret_color); + caret.draw_pos.x = rect.position.x; + } + } + } } - spacing += cache.line_spacing; } } if (has_focus()) { - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + 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 + Point2(0, get_row_height()), get_viewport()->get_window_id()); + DisplayServer::get_singleton()->window_set_ime_position(get_global_position() + caret.draw_pos, get_viewport()->get_window_id()); } } } break; @@ -1552,14 +1245,28 @@ void TextEdit::_notification(int p_what) { draw_caret = true; } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + 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()); - Point2 cursor_pos = Point2(cursor_get_column(), cursor_get_line()) * get_row_height(); - 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() + get_caret_draw_pos(), get_viewport()->get_window_id()); } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { - DisplayServer::get_singleton()->virtual_keyboard_show(get_text(), get_global_rect(), true); + int caret_start = -1; + int caret_end = -1; + + if (!selection.active) { + String full_text = _base_get_text(0, 0, caret.line, caret.column); + + caret_start = full_text.length(); + } else { + String pre_text = _base_get_text(0, 0, selection.from_line, selection.from_column); + String post_text = get_selected_text(); + + 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, caret_start, caret_end); } } break; case NOTIFICATION_FOCUS_EXIT: { @@ -1567,12 +1274,13 @@ void TextEdit::_notification(int p_what) { caret_blink_timer->stop(); } - if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID) { + if (get_viewport()->get_window_id() != DisplayServer::INVALID_WINDOW_ID && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_IME)) { DisplayServer::get_singleton()->window_set_ime_position(Point2(), get_viewport()->get_window_id()); DisplayServer::get_singleton()->window_set_ime_active(false, get_viewport()->get_window_id()); } ime_text = ""; ime_selection = Point2(); + 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(); @@ -1582,568 +1290,89 @@ void TextEdit::_notification(int p_what) { if (has_focus()) { ime_text = DisplayServer::get_singleton()->ime_get_text(); ime_selection = DisplayServer::get_singleton()->ime_get_selection(); - 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; + String t; + 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; } - 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, true, true); - cursor_set_column(prev_column); -} - -void TextEdit::indent_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 this much spaces we want move 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_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 it's 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 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; - } - } - - // Fix selection and cursor being off by one on the last line. - if (is_selection_active() && last_line_text != get_line(end_line)) { - select(selection.from_line, selection.from_column - removed_characters, - 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::_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(MARGIN_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; // TODO. - } - - 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(MARGIN_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; + text.invalidate_cache(caret.line, caret.column, t, structured_text_parser(st_parser, st_args, t)); + update(); } - } - } - - r_row = row; - r_col = col; -} - -Vector2i TextEdit::_get_cursor_pixel_pos() { - adjust_viewport_to_cursor(); - int row = (cursor.line - get_first_visible_line() - cursor.wrap_ofs); - // Correct for hidden and wrapped lines - for (int i = get_first_visible_line(); i < cursor.line; i++) { - if (is_line_hidden(i)) { - row -= 1; - continue; - } - row += times_line_wraps(i); - } - // Row might be wrapped. Adjust row and r_column - Vector<String> rows2 = get_wrap_rows_text(cursor.line); - while (rows2.size() > 1) { - if (cursor.column >= rows2[0].length()) { - cursor.column -= rows2[0].length(); - rows2.remove(0); - row++; - } else { - break; - } - } - - // Calculate final pixel position - int y = (row - get_v_scroll_offset() + 1 /*Bottom of line*/) * get_row_height(); - int x = cache.style_normal->get_margin(MARGIN_LEFT) + gutters_width + gutter_padding - cursor.x_ofs; - int ix = 0; - while (ix < rows2[0].size() && ix < cursor.column) { - if (cache.font != nullptr) { - x += cache.font->get_char_size(rows2[0].get(ix)).width; - } - ix++; + } break; } - x += get_indent_level(cursor.line) * cache.font->get_char_size(' ').width; - - 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(MARGIN_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); +void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { + ERR_FAIL_COND(p_gui_input.is_null()); - // 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) { double prev_v_scroll = v_scroll->get_value(); double prev_h_scroll = h_scroll->get_value(); Ref<InputEventMouseButton> mb = p_gui_input; if (mb.is_valid()) { - if (completion_active && completion_rect.has_point(mb->get_position())) { - if (!mb->is_pressed()) { - return; - } - - if (mb->get_button_index() == BUTTON_WHEEL_UP) { - if (completion_index > 0) { - completion_index--; - completion_current = completion_options[completion_index]; - update(); - } - } - if (mb->get_button_index() == BUTTON_WHEEL_DOWN) { - if (completion_index < completion_options.size() - 1) { - completion_index++; - completion_current = completion_options[completion_index]; - update(); - } - } - - if (mb->get_button_index() == BUTTON_LEFT) { - completion_index = CLAMP(completion_line_ofs + (mb->get_position().y - completion_rect.position.y) / get_row_height(), 0, completion_options.size() - 1); - - completion_current = completion_options[completion_index]; - update(); - if (mb->is_doubleclick()) { - _confirm_completion(); - } - } + Vector2i mpos = mb->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; + } + if (ime_text.length() != 0) { + // Ignore mouse clicks in IME input mode. return; - } else { - _cancel_completion(); - _cancel_code_hint(); } if (mb->is_pressed()) { - if (mb->get_button_index() == BUTTON_WHEEL_UP && !mb->get_command()) { - if (mb->get_shift()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_UP && !mb->is_command_pressed()) { + if (mb->is_shift_pressed()) { h_scroll->set_value(h_scroll->get_value() - (100 * mb->get_factor())); + } else if (mb->is_alt_pressed()) { + // Scroll 5 times as fast as normal (like in Visual Studio Code). + _scroll_up(15 * mb->get_factor()); } else if (v_scroll->is_visible()) { + // Scroll 3 lines. _scroll_up(3 * mb->get_factor()); } } - if (mb->get_button_index() == BUTTON_WHEEL_DOWN && !mb->get_command()) { - if (mb->get_shift()) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_DOWN && !mb->is_command_pressed()) { + if (mb->is_shift_pressed()) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); + } else if (mb->is_alt_pressed()) { + // Scroll 5 times as fast as normal (like in Visual Studio Code). + _scroll_down(15 * mb->get_factor()); } else if (v_scroll->is_visible()) { + // Scroll 3 lines. _scroll_down(3 * mb->get_factor()); } } - if (mb->get_button_index() == BUTTON_WHEEL_LEFT) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_LEFT) { h_scroll->set_value(h_scroll->get_value() - (100 * mb->get_factor())); } - if (mb->get_button_index() == BUTTON_WHEEL_RIGHT) { + if (mb->get_button_index() == MOUSE_BUTTON_WHEEL_RIGHT) { h_scroll->set_value(h_scroll->get_value() + (100 * mb->get_factor())); } - if (mb->get_button_index() == BUTTON_LEFT) { + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { _reset_caret_blink_timer(); - int row, col; - _get_mouse_pos(Point2i(mb->get_position().x, mb->get_position().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(MARGIN_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 (mb->get_position().x > left_margin && mb->get_position().x <= (left_margin + gutters[i].width) - 3) { - emit_signal("gutter_clicked", row, i); + if (mpos.x > left_margin && mpos.x <= (left_margin + gutters[i].width) - 3) { + 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 (mb->get_position().x > left_margin && mb->get_position().x <= left_margin + cache.folded_eol_icon->get_width() + 3) { - unfold_line(row); - return; - } - } - // minimap if (draw_minimap) { _update_minimap_click(); @@ -2152,20 +1381,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, true, false); - cursor_set_column(col); + set_caret_line(row, false, false); + set_caret_column(col); - if (mb->get_shift() && (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); @@ -2178,23 +1407,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) { - SWAP(selection.from_column, selection.to_column); - SWAP(selection.from_line, selection.to_line); 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; @@ -2202,7 +1429,6 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { update(); } - } else { selection.active = false; selection.selecting_mode = SelectionMode::SELECTION_MODE_POINTER; @@ -2210,29 +1436,34 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { selection.selecting_column = col; } - if (!mb->is_doubleclick() && (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_doubleclick() && 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(); } - if (mb->get_button_index() == BUTTON_RIGHT && context_menu_enabled) { + if (mb->get_button_index() == MOUSE_BUTTON_RIGHT && context_menu_enabled) { _reset_caret_blink_timer(); - int row, col; - _get_mouse_pos(Point2i(mb->get_position().x, mb->get_position().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(); @@ -2243,28 +1474,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); } } - menu->set_position(get_screen_transform().xform(get_local_mouse_position())); + _generate_context_menu(); + menu->set_position(get_screen_transform().xform(mpos)); menu->set_size(Vector2(1, 1)); - // menu->set_scale(get_global_transform().get_scale()); menu->popup(); grab_focus(); } } else { - if (mb->get_button_index() == BUTTON_LEFT) { - if (mb->get_command() && highlighted_word != String()) { - int row, col; - _get_mouse_pos(Point2i(mb->get_position().x, mb->get_position().y), row, col); - - emit_signal("symbol_lookup", highlighted_word, row, col); - return; - } - + if (mb->get_button_index() == MOUSE_BUTTON_LEFT) { dragging_minimap = false; dragging_selection = false; can_drag_minimap = false; @@ -2295,20 +1518,12 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventMouseMotion> mm = p_gui_input; if (mm.is_valid()) { - if (select_identifiers_enabled) { - if (!dragging_minimap && !dragging_selection && mm->get_command() && mm->get_button_mask() == 0) { - String new_word = get_word_at_pos(mm->get_position()); - if (new_word != highlighted_word) { - emit_signal("symbol_validate", new_word); - } - } else { - if (highlighted_word != String()) { - set_highlighted_word(String()); - } - } + Vector2i mpos = mm->get_position(); + if (is_layout_rtl()) { + mpos.x = get_size().x - mpos.x; } - if (mm->get_button_mask() & BUTTON_MASK_LEFT && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. + if (mm->get_button_mask() & MOUSE_BUTTON_MASK_LEFT && get_viewport()->gui_get_drag_data() == Variant()) { // Ignore if dragging. _reset_caret_blink_timer(); if (draw_minimap && !dragging_selection) { @@ -2341,1937 +1556,1891 @@ void TextEdit::_gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventKey> k = p_gui_input; if (k.is_valid()) { - k = k->duplicate(); // It will be modified later on. - -#ifdef OSX_ENABLED - if (k->get_keycode() == KEY_META) { -#else - if (k->get_keycode() == KEY_CONTROL) { - -#endif - if (select_identifiers_enabled) { - if (k->is_pressed() && !dragging_minimap && !dragging_selection) { - emit_signal("symbol_validate", get_word_at_pos(get_local_mouse_position())); - } else { - set_highlighted_word(String()); - } - } + if (!k->is_pressed()) { + return; } - if (!k->is_pressed()) { + // If a modifier has been pressed, and nothing else, return. + if (k->get_keycode() == KEY_CTRL || k->get_keycode() == KEY_ALT || k->get_keycode() == KEY_SHIFT || k->get_keycode() == KEY_META) { return; } - if (completion_active) { - if (readonly) { - return; - } + _reset_caret_blink_timer(); - bool valid = true; - if (k->get_command() || k->get_metakey()) { - valid = false; - } + // 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()); - if (valid) { - if (!k->get_alt()) { - if (k->get_keycode() == KEY_UP) { - if (completion_index > 0) { - completion_index--; - } else { - completion_index = completion_options.size() - 1; - } - completion_current = completion_options[completion_index]; - update(); + selection.selecting_text = false; - accept_event(); - return; - } + // Check and handle all built in shortcuts. - if (k->get_keycode() == KEY_DOWN) { - if (completion_index < completion_options.size() - 1) { - completion_index++; - } else { - completion_index = 0; - } - completion_current = completion_options[completion_index]; - update(); + // NEWLINES. + 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; + } - accept_event(); - return; - } + // BACKSPACE AND DELETE. + if (k->is_action("ui_text_backspace_all_to_left", true)) { + _do_backspace(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace_word", true)) { + _do_backspace(true); + accept_event(); + return; + } + if (k->is_action("ui_text_backspace", true)) { + _do_backspace(); + accept_event(); + return; + } + if (k->is_action("ui_text_delete_all_to_right", true)) { + _delete(false, true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete_word", true)) { + _delete(true); + accept_event(); + return; + } + if (k->is_action("ui_text_delete", true)) { + _delete(); + accept_event(); + return; + } - if (k->get_keycode() == KEY_PAGEUP) { - completion_index -= get_theme_constant("completion_lines"); - if (completion_index < 0) { - completion_index = 0; - } - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } + // SCROLLING. + if (k->is_action("ui_text_scroll_up", true)) { + _scroll_lines_up(); + accept_event(); + return; + } + if (k->is_action("ui_text_scroll_down", true)) { + _scroll_lines_down(); + accept_event(); + return; + } - if (k->get_keycode() == KEY_PAGEDOWN) { - 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; - } + // SELECT ALL, SELECT WORD UNDER CARET, CUT, COPY, PASTE. - if (k->get_keycode() == KEY_HOME && completion_index > 0) { - completion_index = 0; - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } + if (k->is_action("ui_text_select_all", true)) { + select_all(); + accept_event(); + return; + } + if (k->is_action("ui_text_select_word_under_caret", true)) { + select_word_under_caret(); + accept_event(); + return; + } + if (k->is_action("ui_cut", true)) { + cut(); + accept_event(); + return; + } + if (k->is_action("ui_copy", true)) { + copy(); + accept_event(); + return; + } + if (k->is_action("ui_paste", true)) { + paste(); + accept_event(); + return; + } - if (k->get_keycode() == KEY_END && completion_index < completion_options.size() - 1) { - completion_index = completion_options.size() - 1; - completion_current = completion_options[completion_index]; - update(); - accept_event(); - return; - } + // UNDO/REDO. + if (k->is_action("ui_undo", true)) { + undo(); + accept_event(); + return; + } + if (k->is_action("ui_redo", true)) { + redo(); + accept_event(); + return; + } - if (k->get_keycode() == KEY_KP_ENTER || k->get_keycode() == KEY_ENTER || k->get_keycode() == KEY_TAB) { - _confirm_completion(); - accept_event(); - return; - } + // MISC. + if (k->is_action("ui_menu", true)) { + if (context_menu_enabled) { + _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(); + } + accept_event(); + return; + } + if (k->is_action("ui_text_toggle_insert_mode", true)) { + set_overtype_mode_enabled(!overtype_mode); + accept_event(); + return; + } + if (k->is_action("ui_swap_input_direction", true)) { + _swap_current_input_direction(); + accept_event(); + return; + } - if (k->get_keycode() == KEY_BACKSPACE) { - _reset_caret_blink_timer(); + // CARET MOVEMENT - backspace_at_cursor(); - _update_completion_candidates(); - accept_event(); - return; - } + 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); - if (k->get_keycode() == KEY_SHIFT) { - accept_event(); - return; - } - } + // CARET MOVEMENT - LEFT, RIGHT. + if (k->is_action("ui_text_caret_word_left", true)) { + _move_caret_left(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_left", true)) { + _move_caret_left(shift_pressed, false); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_word_right", true)) { + _move_caret_right(shift_pressed, true); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_right", true)) { + _move_caret_right(shift_pressed, false); + accept_event(); + return; + } - if (k->get_unicode() > 32) { - _reset_caret_blink_timer(); + // CARET MOVEMENT - UP, DOWN. + if (k->is_action("ui_text_caret_up", true)) { + _move_caret_up(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_down", true)) { + _move_caret_down(shift_pressed); + accept_event(); + return; + } - const char32_t chr[2] = { (char32_t)k->get_unicode(), 0 }; - if (auto_brace_completion_enabled && _is_pair_symbol(chr[0])) { - _consume_pair_symbol(chr[0]); - } else { - // Remove the old character if in insert mode. - if (insert_mode) { - begin_complex_operation(); + // CARET MOVEMENT - DOCUMENT START/END. + if (k->is_action("ui_text_caret_document_start", true)) { // && shift_pressed) { + _move_caret_document_start(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_document_end", true)) { // && shift_pressed) { + _move_caret_document_end(shift_pressed); + accept_event(); + return; + } - // 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); - } - } + // CARET MOVEMENT - LINE START/END. + if (k->is_action("ui_text_caret_line_start", true)) { + _move_caret_to_line_start(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_line_end", true)) { + _move_caret_to_line_end(shift_pressed); + accept_event(); + return; + } + + // CARET MOVEMENT - PAGE UP/DOWN. + if (k->is_action("ui_text_caret_page_up", true)) { + _move_caret_page_up(shift_pressed); + accept_event(); + return; + } + if (k->is_action("ui_text_caret_page_down", true)) { + _move_caret_page_down(shift_pressed); + accept_event(); + return; + } - _insert_text_at_cursor(chr); + // 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; + } + } +} - if (insert_mode) { - end_complex_operation(); - } - } - _update_completion_candidates(); - accept_event(); +/* 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(); +} - return; - } - } +void TextEdit::_new_line(bool p_split_current_line, bool p_above) { + if (!editable) { + return; + } - _cancel_completion(); - } - - /* TEST CONTROL FIRST! */ - - // Some remaps for duplicate functions. - if (k->get_command() && !k->get_shift() && !k->get_alt() && !k->get_metakey() && k->get_keycode() == KEY_INSERT) { - k->set_keycode(KEY_C); - } - if (!k->get_command() && k->get_shift() && !k->get_alt() && !k->get_metakey() && k->get_keycode() == KEY_INSERT) { - k->set_keycode(KEY_V); - k->set_command(true); - k->set_shift(false); - } -#ifdef APPLE_STYLE_KEYS - if (k->get_control() && !k->get_shift() && !k->get_alt() && !k->get_command()) { - uint32_t remap_key = KEY_UNKNOWN; - switch (k->get_keycode()) { - case KEY_F: { - remap_key = KEY_RIGHT; - } break; - case KEY_B: { - remap_key = KEY_LEFT; - } break; - case KEY_P: { - remap_key = KEY_UP; - } break; - case KEY_N: { - remap_key = KEY_DOWN; - } break; - case KEY_D: { - remap_key = KEY_DELETE; - } break; - case KEY_H: { - remap_key = KEY_BACKSPACE; - } break; - } + begin_complex_operation(); - if (remap_key != KEY_UNKNOWN) { - k->set_keycode(remap_key); - k->set_control(false); + 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()); } -#endif + } - _reset_caret_blink_timer(); + insert_text_at_caret("\n"); - // Save here for insert mode, just in case it is cleared in the following section. - bool had_selection = selection.active; + if (first_line) { + set_caret_line(0); + } - // Stuff to do when selection is active. - if (!readonly && selection.active) { - bool clear = false; - bool unselect = false; - bool dobreak = false; + end_complex_operation(); +} - switch (k->get_keycode()) { - case KEY_TAB: { - if (k->get_shift()) { - indent_left(); - } else { - indent_right(); - } - dobreak = true; - accept_event(); - } break; - case KEY_X: - case KEY_C: - // Special keys often used with control, wait. - clear = (!k->get_command() || k->get_shift() || k->get_alt()); - break; - case KEY_DELETE: - if (!k->get_shift()) { - accept_event(); - clear = true; - dobreak = true; - } else if (k->get_command() || k->get_alt()) { - dobreak = true; - } - break; - case KEY_BACKSPACE: - accept_event(); - clear = true; - dobreak = true; - break; - case KEY_LEFT: - case KEY_RIGHT: - case KEY_UP: - case KEY_DOWN: - case KEY_PAGEUP: - case KEY_PAGEDOWN: - case KEY_HOME: - case KEY_END: - // Ignore arrows if any modifiers are held (shift = selecting, others may be used for editor hotkeys). - if (k->get_command() || k->get_shift() || k->get_alt()) { - break; - } - unselect = true; - break; +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 { + deselect(); + } - default: - if (k->get_unicode() >= 32 && !k->get_command() && !k->get_alt() && !k->get_metakey()) { - clear = true; - } - if (auto_brace_completion_enabled && _is_pair_left_symbol(k->get_unicode())) { - clear = false; - } - } + if (p_move_by_word) { + int cc = caret.column; - if (unselect) { - selection.active = false; - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; - update(); - } - if (clear) { - if (!dobreak) { - begin_complex_operation(); + 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; } - selection.active = false; - update(); - _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - cursor_set_line(selection.from_line, true, false); - cursor_set_column(selection.from_column); - update(); } - if (dobreak) { - return; + set_caret_column(cc); + } + } 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 { + 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())); } } + } - selection.selecting_text = false; + if (p_select) { + _post_shift_selection(); + } +} - bool keycode_handled = true; +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 { + deselect(); + } - // Special keycode test. + if (p_move_by_word) { + int cc = caret.column; - switch (k->get_keycode()) { - case KEY_KP_ENTER: - case KEY_ENTER: { - if (readonly) { + 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())); + } + } + } - String ins = "\n"; + if (p_select) { + _post_shift_selection(); + } +} - // 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++; +void TextEdit::_move_caret_up(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - if (space_count == indent_size) { - if (indent_using_spaces) { - ins += space_indent; - } else { - ins += "\t"; - } - space_count = 0; - } - } else { - break; - } - } + 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 { + 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 (is_folded(cursor.line)) { - unfold_line(cursor.line); - } + if (p_select) { + _post_shift_selection(); + } +} - bool brace_indent = false; - - // No need to indent if we are going upwards. - if (auto_indent && !(k->get_command() && k->get_shift())) { - // 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; - } +void TextEdit::_move_caret_down(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - if (indent_char_found && is_line_comment(i)) { - should_indent = true; - break; - } else if (indent_char_found && !_is_whitespace(c)) { - should_indent = false; - indent_char_found = false; - } - } + 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 (!is_line_comment(cursor.line) && should_indent) { - if (indent_using_spaces) { - ins += space_indent; - } else { - ins += "\t"; - } + if (p_select) { + _post_shift_selection(); + } +} - // 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]) && !k->get_command()) { - brace_indent = true; - ins += "\n" + ins.substr(1, ins.length() - 2); - } - } - } - } - begin_complex_operation(); - bool first_line = false; - if (k->get_command()) { - if (k->get_shift()) { - if (cursor.line > 0) { - cursor_set_line(cursor.line - 1); - cursor_set_column(text[cursor.line].length()); - } else { - cursor_set_column(0); - first_line = true; - } - } else { - cursor_set_column(text[cursor.line].length()); - } - } +void TextEdit::_move_caret_to_line_start(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - insert_text_at_cursor(ins); + // 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 (first_line) { - cursor_set_line(0); - } else if (brace_indent) { - cursor_set_line(cursor.line - 1); - cursor_set_column(text[cursor.line].length()); - } - end_complex_operation(); - } break; - case KEY_ESCAPE: { - if (completion_hint != "") { - completion_hint = ""; - update(); - } else { - keycode_handled = false; - } - } break; - case KEY_TAB: { - if (k->get_command()) { - break; // Avoid tab when command. - } + if (get_caret_column() == current_line_whitespace_len) { + set_caret_column(0); + } else { + set_caret_column(current_line_whitespace_len); + } + } else { + set_caret_column(row_start_col); + } - if (readonly) { - break; - } + if (p_select) { + _post_shift_selection(); + } +} - if (is_selection_active()) { - if (k->get_shift()) { - indent_left(); - } else { - indent_right(); - } - } else { - if (k->get_shift()) { - // Simple unindent. - int cc = cursor.column; - const String &line = text[cursor.line]; +void TextEdit::_move_caret_to_line_end(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - int left = _find_first_non_whitespace_column_of_line(line); - cc = MIN(cc, left); + // 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); + } - while (cc < indent_size && cc < left && line[cc] == ' ') { - cc++; - } + if (p_select) { + _post_shift_selection(); + } +} - 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(); - } - } 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::_move_caret_page_up(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - } break; - case KEY_BACKSPACE: { - if (readonly) { - break; - } + 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); -#ifdef APPLE_STYLE_KEYS - if (k->get_alt() && cursor.column > 1) { -#else - if (k->get_alt()) { - keycode_handled = false; - break; - } else if (k->get_command() && cursor.column > 1) { -#endif - int line = cursor.line; - int column = cursor.column; - - // Check if we are removing a single whitespace, if so remove it and the next char type, - // else we just remove the whitespace. - bool only_whitespace = false; - if (_is_whitespace(text[line][column - 1]) && _is_whitespace(text[line][column - 2])) { - only_whitespace = true; - } else if (_is_whitespace(text[line][column - 1])) { - // Remove the single whitespace. - column--; - } + if (p_select) { + _post_shift_selection(); + } +} - // Check if its a text char. - bool only_char = (_is_text_char(text[line][column - 1]) && !only_whitespace); +void TextEdit::_move_caret_page_down(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - // If its not whitespace or char then symbol. - bool only_symbols = !(only_whitespace || only_char); + 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); - while (column > 0) { - bool is_whitespace = _is_whitespace(text[line][column - 1]); - bool is_text_char = _is_text_char(text[line][column - 1]); + if (p_select) { + _post_shift_selection(); + } +} - if (only_whitespace && !is_whitespace) { - break; - } else if (only_char && !is_text_char) { - break; - } else if (only_symbols && (is_whitespace || is_text_char)) { - break; - } - column--; - } +void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { + if (!editable) { + return; + } - _remove_text(line, column, cursor.line, cursor.column); + if (has_selection() || (!p_all_to_left && !p_word)) { + backspace(); + return; + } - cursor_set_line(line); - cursor_set_column(column); + 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; + } -#ifdef APPLE_STYLE_KEYS - } else if (k->get_command()) { - int cursor_current_column = cursor.column; - cursor.column = 0; - _remove_text(cursor.line, 0, cursor.line, cursor_current_column); -#endif - } else { - if (cursor.line > 0 && is_line_hidden(cursor.line - 1)) { - unfold_line(cursor.line - 1); - } - backspace_at_cursor(); - } + if (p_word) { + int line = caret.line; + int column = caret.column; - } break; - case KEY_KP_4: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; + 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; } - case KEY_LEFT: { - if (k->get_shift()) { - _pre_shift_selection(); -#ifdef APPLE_STYLE_KEYS - } else { -#else - } else if (!k->get_alt()) { -#endif - deselect(); - } + } -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - // Start at first column (it's slightly faster that way) and look for the first non-whitespace character. - int new_cursor_pos = 0; - for (int i = 0; i < text[cursor.line].length(); ++i) { - if (!_is_whitespace(text[cursor.line][i])) { - new_cursor_pos = i; - break; - } - } - if (new_cursor_pos == cursor.column) { - // We're already at the first text character, so move to the very beginning of the line. - cursor_set_column(0); - } else { - // We're somewhere to the right of the first text character; move to the first one. - cursor_set_column(new_cursor_pos); - } - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - keycode_handled = false; - break; - } else if (k->get_command()) { -#endif - int cc = cursor.column; + _remove_text(line, column, caret.line, caret.column); - if (cc == 0 && cursor.line > 0) { - cursor_set_line(cursor.line - 1); - cursor_set_column(text[cursor.line].length()); - } else { - bool prev_char = false; + set_caret_line(line, false); + set_caret_column(column); + return; + } +} - while (cc > 0) { - bool ischar = _is_text_char(text[cursor.line][cc - 1]); +void TextEdit::_delete(bool p_word, bool p_all_to_right) { + if (!editable) { + return; + } - if (prev_char && !ischar) { - break; - } + if (has_selection()) { + delete_selection(); + return; + } + int curline_len = text[caret.line].length(); - prev_char = ischar; - cc--; - } - cursor_set_column(cc); - } + if (caret.line == text.size() - 1 && caret.column == curline_len) { + return; // Last line, last column: Nothing to do. + } - } else 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 { - cursor_set_column(cursor_get_column() - 1); - } + int next_line = caret.column < curline_len ? caret.line : caret.line + 1; + int next_column; - if (k->get_shift()) { - _post_shift_selection(); - } + 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; - } break; - case KEY_KP_6: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; + 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; } - case KEY_RIGHT: { - if (k->get_shift()) { - _pre_shift_selection(); -#ifdef APPLE_STYLE_KEYS - } else { -#else - } else if (!k->get_alt()) { -#endif - deselect(); - } - -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - cursor_set_column(text[cursor.line].length()); - } else if (k->get_alt()) { -#else - if (k->get_alt()) { - keycode_handled = false; - break; - } else if (k->get_command()) { -#endif - 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 { - bool prev_char = false; + } - while (cc < text[cursor.line].length()) { - bool ischar = _is_text_char(text[cursor.line][cc]); + 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; + } + } - if (prev_char && !ischar) { - break; - } - prev_char = ischar; - cc++; - } - cursor_set_column(cc); - } + _remove_text(caret.line, caret.column, next_line, next_column); + update(); +} - } else 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 { - cursor_set_column(cursor_get_column() + 1); - } +void TextEdit::_move_caret_document_start(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } - if (k->get_shift()) { - _post_shift_selection(); - } + set_caret_line(0); + set_caret_column(0); - } break; - case KEY_KP_8: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; - } - case KEY_UP: { - if (k->get_alt()) { - keycode_handled = false; - break; - } -#ifndef APPLE_STYLE_KEYS - if (k->get_command()) { -#else - if (k->get_command() && k->get_alt()) { -#endif - _scroll_lines_up(); - break; - } + if (p_select) { + _post_shift_selection(); + } +} - if (k->get_shift()) { - _pre_shift_selection(); - } +void TextEdit::_move_caret_document_end(bool p_select) { + if (p_select) { + _pre_shift_selection(); + } else { + deselect(); + } -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - cursor_set_line(0); - } else -#endif - { - 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); - } - } - } + set_caret_line(get_last_unhidden_line(), true, false, 9999); + set_caret_column(text[caret.line].length()); - if (k->get_shift()) { - _post_shift_selection(); - } - _cancel_code_hint(); + if (p_select) { + _post_shift_selection(); + } +} - } break; - case KEY_KP_2: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; - } - case KEY_DOWN: { - if (k->get_alt()) { - keycode_handled = false; - break; - } -#ifndef APPLE_STYLE_KEYS - if (k->get_command()) { -#else - if (k->get_command() && k->get_alt()) { -#endif - _scroll_lines_down(); - break; - } +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")); - if (k->get_shift()) { - _pre_shift_selection(); - } + /* Search */ + search_result_color = get_theme_color(SNAME("search_result_color")); + search_result_border_color = get_theme_color(SNAME("search_result_border_color")); -#ifdef APPLE_STYLE_KEYS - if (k->get_command()) { - cursor_set_line(get_last_unhidden_line(), true, false, 9999); - } else -#endif - { - 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); - } - } + /* Caret */ + caret_color = get_theme_color(SNAME("caret_color")); + caret_background_color = get_theme_color(SNAME("caret_background_color")); - if (k->get_shift()) { - _post_shift_selection(); - } - _cancel_code_hint(); + /* Selection */ + font_selected_color = get_theme_color(SNAME("font_selected_color")); + selection_color = get_theme_color(SNAME("selection_color")); - } break; - case KEY_DELETE: { - if (readonly) { - break; - } + /* Visual. */ + style_normal = get_theme_stylebox(SNAME("normal")); + style_focus = get_theme_stylebox(SNAME("focus")); + style_readonly = get_theme_stylebox(SNAME("read_only")); - if (k->get_shift() && !k->get_command() && !k->get_alt() && is_shortcut_keys_enabled()) { - cut(); - break; - } + tab_icon = get_theme_icon(SNAME("tab")); + space_icon = get_theme_icon(SNAME("space")); - int curline_len = text[cursor.line].length(); + 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")); - if (cursor.line == text.size() - 1 && cursor.column == curline_len) { - break; // Nothing to do. - } + outline_size = get_theme_constant(SNAME("outline_size")); + outline_color = get_theme_color(SNAME("font_outline_color")); - int next_line = cursor.column < curline_len ? cursor.line : cursor.line + 1; - int next_column; + line_spacing = get_theme_constant(SNAME("line_spacing")); -#ifdef APPLE_STYLE_KEYS - if (k->get_alt() && cursor.column < curline_len - 1) { -#else - if (k->get_alt()) { - keycode_handled = false; - break; - } else if (k->get_command() && cursor.column < curline_len - 1) { -#endif + 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")); - int line = cursor.line; - int column = cursor.column; - - // Check if we are removing a single whitespace, if so remove it and the next char type, - // else we just remove the whitespace. - bool only_whitespace = false; - if (_is_whitespace(text[line][column]) && _is_whitespace(text[line][column + 1])) { - only_whitespace = true; - } else if (_is_whitespace(text[line][column])) { - // Remove the single whitespace. - column++; - } - - // Check if its a text char. - bool only_char = (_is_text_char(text[line][column]) && !only_whitespace); - - // If its not whitespace or char then symbol. - bool only_symbols = !(only_whitespace || only_char); + /* 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(); - while (column < curline_len) { - bool is_whitespace = _is_whitespace(text[line][column]); - bool is_text_char = _is_text_char(text[line][column]); + /* Syntax highlighting. */ + if (syntax_highlighter.is_valid()) { + syntax_highlighter->set_text_edit(this); + } +} - if (only_whitespace && !is_whitespace) { - break; - } else if (only_char && !is_text_char) { - break; - } else if (only_symbols && (is_whitespace || is_text_char)) { - break; - } - column++; - } +/* General overrides. */ +Size2 TextEdit::get_minimum_size() const { + return style_normal->get_minimum_size(); +} - next_line = line; - next_column = column; -#ifdef APPLE_STYLE_KEYS - } else if (k->get_command()) { - next_column = curline_len; - next_line = cursor.line; -#endif - } else { - next_column = cursor.column < curline_len ? (cursor.column + 1) : 0; - } +bool TextEdit::is_text_field() const { + return true; +} - _remove_text(cursor.line, cursor.column, next_line, next_column); - update(); +Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { + Point2i pos = get_line_column_at_pos(p_pos); + int row = pos.y; - } break; - case KEY_KP_7: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; + 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; } - case KEY_HOME: { -#ifdef APPLE_STYLE_KEYS - if (k->get_shift()) - _pre_shift_selection(); - - cursor_set_line(0); - - if (k->get_shift()) - _post_shift_selection(); - else if (k->get_command() || k->get_control()) - deselect(); -#else - if (k->get_shift()) { - _pre_shift_selection(); - } - if (k->get_command()) { - cursor_set_line(0); - cursor_set_column(0); - } else { - // 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 seq 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 (k->get_shift()) { - _post_shift_selection(); - } else if (k->get_command() || k->get_control()) { - deselect(); - } - _cancel_completion(); - completion_hint = ""; -#endif - } break; - case KEY_KP_1: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; + 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; } - [[fallthrough]]; } - case KEY_END: { -#ifdef APPLE_STYLE_KEYS - if (k->get_shift()) - _pre_shift_selection(); - - cursor_set_line(get_last_unhidden_line(), true, false, 9999); - - if (k->get_shift()) - _post_shift_selection(); - else if (k->get_command() || k->get_control()) - deselect(); -#else - if (k->get_shift()) { - _pre_shift_selection(); - } + left_margin += gutters[i].width; + } + return CURSOR_ARROW; + } - if (k->get_command()) { - cursor_set_line(get_last_unhidden_line(), true, false, 9999); - } + 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(); +} - // 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); - } +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; - if (k->get_shift()) { - _post_shift_selection(); - } else if (k->get_command() || k->get_control()) { - deselect(); - } + 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); - _cancel_completion(); - completion_hint = ""; -#endif - } break; - case KEY_KP_9: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; - } - case KEY_PAGEUP: { - if (k->get_shift()) { - _pre_shift_selection(); - } + return tt; + } - 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); + return Control::get_tooltip(p_pos); +} - if (k->get_shift()) { - _post_shift_selection(); - } +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; +} - _cancel_completion(); - completion_hint = ""; +/* Text */ +// Text properties. +bool TextEdit::has_ime_text() const { + return !ime_text.is_empty(); +} - } break; - case KEY_KP_3: { - if (k->get_unicode() != 0) { - keycode_handled = false; - break; - } - [[fallthrough]]; - } - case KEY_PAGEDOWN: { - if (k->get_shift()) { - _pre_shift_selection(); - } +void TextEdit::set_editable(const bool p_editable) { + if (editable == p_editable) { + return; + } - 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); + editable = p_editable; - if (k->get_shift()) { - _post_shift_selection(); - } + update(); +} - _cancel_completion(); - completion_hint = ""; +bool TextEdit::is_editable() const { + return editable; +} - } break; - case KEY_A: { -#ifndef APPLE_STYLE_KEYS - if (!k->get_control() || k->get_shift() || k->get_alt()) { - keycode_handled = false; - break; - } - if (is_shortcut_keys_enabled()) { - select_all(); - } -#else - if ((!k->get_command() && !k->get_control())) { - keycode_handled = false; - break; - } - if (!k->get_shift() && k->get_command() && is_shortcut_keys_enabled()) - select_all(); - else if (k->get_control()) { - if (k->get_shift()) - _pre_shift_selection(); - - 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++; - } +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; + } + text.set_direction_and_language(dir, (language != "") ? language : TranslationServer::get_singleton()->get_tool_locale()); + text.invalidate_all(); - if (cursor_get_column() == current_line_whitespace_len) - cursor_set_column(0); - else - cursor_set_column(current_line_whitespace_len); + 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(); + } +} - if (k->get_shift()) - _post_shift_selection(); - else if (k->get_command() || k->get_control()) - deselect(); - } - } break; - case KEY_E: { - if (!k->get_control() || k->get_command() || k->get_alt()) { - keycode_handled = false; - break; - } +Control::TextDirection TextEdit::get_text_direction() const { + return text_direction; +} - if (k->get_shift()) - _pre_shift_selection(); +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 (k->get_command()) - cursor_set_line(text.size() - 1, true, false); - cursor_set_column(text[cursor.line].length()); +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]; +} - if (k->get_shift()) - _post_shift_selection(); - else if (k->get_command() || k->get_control()) - deselect(); +void TextEdit::clear_opentype_features() { + opentype_features.clear(); + text.set_font_features(opentype_features); + text.invalidate_all(); + update(); +} - _cancel_completion(); - completion_hint = ""; -#endif - } break; - case KEY_X: { - if (readonly) { - break; - } - if (!k->get_command() || k->get_shift() || k->get_alt()) { - keycode_handled = false; - break; - } - if (is_shortcut_keys_enabled()) { - cut(); - } +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(); + } +} - } break; - case KEY_C: { - if (!k->get_command() || k->get_shift() || k->get_alt()) { - keycode_handled = false; - break; - } +String TextEdit::get_language() const { + return language; +} - if (is_shortcut_keys_enabled()) { - copy(); - } +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(); + } +} - } break; - case KEY_Z: { - if (readonly) { - break; - } +Control::StructuredTextParser TextEdit::get_structured_text_bidi_override() const { + return st_parser; +} - if (!k->get_command()) { - keycode_handled = false; - break; - } +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(); +} - if (is_shortcut_keys_enabled()) { - if (k->get_shift()) { - redo(); - } else { - undo(); - } - } - } break; - case KEY_Y: { - if (readonly) { - break; - } +Array TextEdit::get_structured_text_bidi_override_options() const { + return st_args; +} - if (!k->get_command()) { - keycode_handled = false; - break; - } +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(); +} - if (is_shortcut_keys_enabled()) { - redo(); - } - } break; - case KEY_V: { - if (readonly) { - break; - } - if (!k->get_command() || k->get_shift() || k->get_alt()) { - keycode_handled = false; - break; - } +int TextEdit::get_tab_size() const { + return text.get_tab_size(); +} - if (is_shortcut_keys_enabled()) { - paste(); - } +// User controls +void TextEdit::set_overtype_mode_enabled(const bool p_enabled) { + overtype_mode = p_enabled; + update(); +} - } break; - case KEY_SPACE: { -#ifdef OSX_ENABLED - if (completion_enabled && k->get_metakey()) { // cmd-space is spotlight shortcut in OSX -#else - if (completion_enabled && k->get_command()) { -#endif +bool TextEdit::is_overtype_mode_enabled() const { + return overtype_mode; +} - query_code_comple(); - keycode_handled = true; - } else { - keycode_handled = false; - } +void TextEdit::set_context_menu_enabled(bool p_enable) { + context_menu_enabled = p_enable; +} - } break; +bool TextEdit::is_context_menu_enabled() const { + return context_menu_enabled; +} - case KEY_MENU: { - if (context_menu_enabled) { - menu->set_position(get_global_transform().xform(_get_cursor_pixel_pos())); - menu->set_size(Vector2(1, 1)); - // menu->set_scale(get_global_transform().get_scale()); - menu->popup(); - menu->grab_focus(); - } - } break; +void TextEdit::set_shortcut_keys_enabled(bool p_enabled) { + shortcut_keys_enabled = p_enabled; +} - default: { - keycode_handled = false; - } break; - } +bool TextEdit::is_shortcut_keys_enabled() const { + return shortcut_keys_enabled; +} - if (keycode_handled) { - accept_event(); - } +void TextEdit::set_virtual_keyboard_enabled(bool p_enable) { + virtual_keyboard_enabled = p_enable; +} - if (k->get_keycode() == KEY_INSERT) { - set_insert_mode(!insert_mode); - accept_event(); - return; - } +bool TextEdit::is_virtual_keyboard_enabled() const { + return virtual_keyboard_enabled; +} - if (!keycode_handled && (!k->get_command() || (k->get_command() && k->get_alt()))) { // For German keyboards. +// Text manipulation +void TextEdit::clear() { + setting_text = true; + _clear(); + setting_text = false; + emit_signal(SNAME("text_set")); +} - if (k->get_unicode() >= 32) { - if (readonly) { - return; - } +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; +} - // Remove the old character if in insert mode and no selection. - if (insert_mode && !had_selection) { - begin_complex_operation(); +void TextEdit::set_text(const String &p_text) { + setting_text = true; + if (!undo_enabled) { + _clear(); + insert_text_at_caret(p_text); + } - // 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); - } - } + if (undo_enabled) { + set_caret_line(0); + set_caret_column(0); - const char32_t chr[2] = { (char32_t)k->get_unicode(), 0 }; + 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_caret(p_text); + end_complex_operation(); + selection.active = false; + } - if (completion_hint != "" && k->get_unicode() == ')') { - completion_hint = ""; - } - if (auto_brace_completion_enabled && _is_pair_symbol(chr[0])) { - _consume_pair_symbol(chr[0]); - } else { - _insert_text_at_cursor(chr); - } + set_caret_line(0); + set_caret_column(0); - if (insert_mode && !had_selection) { - end_complex_operation(); - } + update(); + setting_text = false; + emit_signal(SNAME("text_set")); +} - if (selection.active != had_selection) { - end_complex_operation(); - } - accept_event(); - } +String TextEdit::get_text() const { + String longthing; + int len = text.size(); + for (int i = 0; i < len; i++) { + longthing += text[i]; + if (i != len - 1) { + longthing += "\n"; } - - return; } + return longthing; } -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; - } - - if (scrolling) { - target_v_scroll = (target_v_scroll - p_delta); - } else { - target_v_scroll = (get_v_scroll() - p_delta); - } - - if (smooth_scroll_enabled) { - if (target_v_scroll <= 0) { - target_v_scroll = 0; - } - 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); - } - } else { - set_v_scroll(target_v_scroll); - } +int TextEdit::get_line_count() const { + return text.size(); } -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::set_line(int p_line, const String &p_new_text) { + if (p_line < 0 || p_line >= text.size()) { + return; } - - if (scrolling) { - target_v_scroll = (target_v_scroll + p_delta); - } else { - target_v_scroll = (get_v_scroll() + p_delta); + _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 (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 (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); - } - } else { - set_v_scroll(target_v_scroll); + if (has_selection() && p_line == selection.to_line && selection.to_column > text[p_line].length()) { + selection.to_column = text[p_line].length(); } } -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; +String TextEdit::get_line(int p_line) const { + if (p_line < 0 || p_line >= text.size()) { + return ""; } - - selection.selecting_mode = SelectionMode::SELECTION_MODE_SHIFT; + return text[p_line]; } -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(); - } +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); - selection.selecting_text = true; + return text.get_line_width(p_line, p_wrap_index); } -void TextEdit::_scroll_lines_up() { - scrolling = false; - minimap_clicked = false; - - // Adjust the vertical scroll. - set_v_scroll(get_v_scroll() - 1); - - // 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_visible_line(); - int last_vis_wrap = get_last_visible_line_wrap_index(); - - 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); +int TextEdit::get_line_height() const { + int height = font->get_height(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)); } } + return height + line_spacing; } -void TextEdit::_scroll_lines_down() { - scrolling = false; - minimap_clicked = false; - - // Adjust the vertical scroll. - set_v_scroll(get_v_scroll() + 1); - - // 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; +int TextEdit::get_indent_level(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); - 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); + 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; } } + return tab_count * text.get_tab_size() + whitespace_count; } -/**** TEXT EDIT CORE API ****/ +int TextEdit::get_first_non_whitespace_column(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); -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); + int col = 0; + while (col < text[p_line].length() && _is_whitespace(text[p_line][col])) { + col++; + } + return col; +} - /* STEP 1: Remove \r from source text and separate in substrings. */ +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()); - Vector<String> substrings = p_text.replace("\r", "").split("\n"); + 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); +} - // Is this just a new empty line? - bool shift_first_line = p_char == 0 && p_text.replace("\r", "") == "\n"; +void TextEdit::insert_line_at(int p_at, const String &p_text) { + ERR_FAIL_INDEX(p_at, text.size()); - /* 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(' ')); + _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; + } + } +} - /* STEP 3: Separate dest string in pre and post text. */ - - 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::insert_text_at_caret(const String &p_text) { + delete_selection(); - for (int j = 0; j < substrings.size(); j++) { - // Insert the substrings. + int new_column, new_line; + _insert_text(caret.line, caret.column, p_text, &new_line, &new_column); + _update_scrollbars(); - if (j == 0) { - text.set(p_line, preinsert_text + substrings[j]); - } else { - text.insert(p_line + j, substrings[j]); - } + set_caret_line(new_line, false); + set_caret_column(new_column); + update(); +} - if (j == substrings.size() - 1) { - text.set(p_line + j, text[p_line + j] + postinsert_text); - } - } +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); - if (shift_first_line) { - text.move_gutters(p_line, p_line + 1); - text.set_hidden(p_line + 1, text.is_hidden(p_line)); + _remove_text(p_from_line, p_from_column, p_to_line, p_to_column); +} - text.set_hidden(p_line, false); +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; } - text.set_line_wrap_amount(p_line, -1); - - r_end_line = p_line + substrings.size() - 1; - r_end_column = text[r_end_line].length() - postinsert_text.length(); - - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); + int last_line; + for (last_line = text.size() - 1; last_line > 0; last_line--) { + if (!_is_line_hidden(last_line)) { + break; } - text_changed_dirty = true; } - emit_signal("lines_edited_from", p_line, r_end_line); + return last_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'. - - String ret; +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)); - 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(); + if (!_is_hiding_enabled()) { + return ABS(p_visible_amount); + } - if (i > p_from_line) { - ret += "\n"; + 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 { + 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; + } } - ret += text[i].substr(begin, end - begin); } - - return ret; + return num_total; } -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 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()); +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)); - for (int i = p_from_line; i < p_to_line; i++) { - text.remove(p_from_line + 1); + if (!_is_hiding_enabled() && get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) { + return Point2i(ABS(p_visible_amount), 0); } - text.set(p_from_line, pre_text + post_text); - text.set_line_wrap_amount(p_from_line, -1); - - if (!text_changed_dirty && !setting_text) { - if (is_inside_tree()) { - MessageQueue::get_singleton()->push_call(this, "_text_changed_emit"); + 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; + } } - text_changed_dirty = true; + 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); } - emit_signal("lines_edited_from", p_to_line, p_from_line); + wrap_index = MAX(wrap_index, 0); + return Point2i(num_total, wrap_index); } -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(); +// Overridable actions +void TextEdit::handle_unicode_input(const uint32_t p_unicode) { + if (GDVIRTUAL_CALL(_handle_unicode_input, p_unicode)) { + return; } + _handle_unicode_input_internal(p_unicode); +} - if (undo_enabled) { - _clear_redo(); +void TextEdit::backspace() { + if (GDVIRTUAL_CALL(_backspace)) { + return; } + _backspace_internal(); +} - int retline, retchar; - _base_insert_text(p_line, p_char, p_text, retline, retchar); - if (r_end_line) { - *r_end_line = retline; +void TextEdit::cut() { + if (GDVIRTUAL_CALL(_cut)) { + return; } - if (r_end_char) { - *r_end_char = retchar; + _cut_internal(); +} + +void TextEdit::copy() { + if (GDVIRTUAL_CALL(_copy)) { + return; } + _copy_internal(); +} - if (!undo_enabled) { +void TextEdit::paste() { + if (GDVIRTUAL_CALL(_paste)) { return; } + _paste_internal(); +} - /* 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; +// Context menu. +PopupMenu *TextEdit::get_menu() const { + const_cast<TextEdit *>(this)->_generate_context_menu(); + 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::is_menu_visible() const { + return menu && menu->is_visible(); +} - 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. +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)); + } + } } - // Merge current op. +} - current_op.text += p_text; - current_op.to_column = retchar; - current_op.to_line = retline; - current_op.version = op.version; +/* Versioning */ +void TextEdit::begin_complex_operation() { + _push_current_op(); + next_operation_is_complex = 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(); +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; + return; } - String text; - if (undo_enabled) { - _clear_redo(); - text = _base_get_text(p_from_line, p_from_column, p_to_line, p_to_column); + 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(); +} - _base_remove_text(p_from_line, p_from_column, p_to_line, p_to_column); +bool TextEdit::has_redo() const { + return undo_stack_pos != nullptr; +} - if (!undo_enabled) { +void TextEdit::undo() { + if (!editable) { return; } - /* 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; + _push_current_op(); - // 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. + if (undo_stack_pos == nullptr) { + if (!undo_stack.size()) { + return; // Nothing to undo. + } + + 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(); } - // 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. + + deselect(); + + 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); } - op.prev_version = get_version(); - _push_current_op(); - current_op = op; -} + 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; + } + } + } -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); - cursor_set_column(new_column); - + 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(); } -int TextEdit::get_char_count() { - int totalsize = 0; +void TextEdit::redo() { + if (!editable) { + return; + } + _push_current_op(); - for (int i = 0; i < text.size(); i++) { - if (i > 0) { - totalsize++; // Include \n. + 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; + } } - totalsize += text[i].length(); } - return totalsize; // Omit last \n. + _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(); } -Size2 TextEdit::get_minimum_size() const { - return cache.style_normal->get_minimum_size(); +void TextEdit::clear_undo_history() { + saved_version = 0; + current_op.type = TextOperation::TYPE_NONE; + undo_stack_pos = nullptr; + undo_stack.clear(); } -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; - } - return control_height; +bool TextEdit::is_insert_text_operation() const { + return (current_op.type == TextOperation::TYPE_INSERT); } -void TextEdit::_generate_context_menu() { - // Reorganize context menu. - menu->clear(); - if (!readonly) { - menu->add_item(RTR("Cut"), MENU_CUT, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_X : 0); - } - menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_C : 0); - if (!readonly) { - menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_V : 0); - } - menu->add_separator(); - if (is_selecting_enabled()) { - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_A : 0); - } - if (!readonly) { - menu->add_item(RTR("Clear"), MENU_CLEAR); - menu->add_separator(); - menu->add_item(RTR("Undo"), MENU_UNDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_Z : 0); - menu->add_item(RTR("Redo"), MENU_REDO, is_shortcut_keys_enabled() ? KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z : 0); - } +void TextEdit::tag_saved_version() { + saved_version = get_version(); } -int TextEdit::get_visible_rows() const { - return _get_control_height() / get_row_height(); +uint32_t TextEdit::get_version() const { + return current_op.version; } -int TextEdit::_get_minimap_visible_rows() const { - return _get_control_height() / (minimap_char_size.y + minimap_line_spacing); +uint32_t TextEdit::get_saved_version() const { + return saved_version; } -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(); - } - - 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); - } - } - return total_rows; +/* Search */ +void TextEdit::set_search_text(const String &p_search_text) { + search_text = p_search_text; } -void TextEdit::_update_wrap_at() { - wrap_at = get_size().width - cache.style_normal->get_minimum_size().width - gutters_width - gutter_padding - cache.minimap_width - wrap_right_offset; - update_cursor_wrap_offset(); - text.clear_wrap_cache(); +void TextEdit::set_search_flags(uint32_t p_flags) { + search_flags = p_flags; +} - for (int i = 0; i < text.size(); i++) { - // Update all values that wrap. - if (!line_wraps(i)) { - continue; - } - Vector<String> rows = get_wrap_rows_text(i); - text.set_line_wrap_amount(i, rows.size() - 1); +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); } -} + 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)); -void TextEdit::adjust_viewport_to_cursor() { - // Make sure cursor is visible on the screen. - scrolling = false; - minimap_clicked = false; + // Search through the whole document, but start by current line. - int cur_line = cursor.line; - int cur_wrap = get_cursor_wrap_index(); + int line = p_from_line; + int pos = -1; - int first_vis_line = get_first_visible_line(); - int first_vis_wrap = cursor.wrap_ofs; - int last_vis_line = get_last_visible_line(); - int last_vis_wrap = get_last_visible_line_wrap_index(); + for (int i = 0; i < text.size() + 1; i++) { + if (line < 0) { + line = text.size() - 1; + } + if (line == text.size()) { + line = 0; + } - 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); - } + String text_line = text[line]; + int from_column = 0; + if (line == p_from_line) { + if (i == text.size()) { + // Wrapped. - 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; - } - visible_width -= 20; // Give it a little more space. + if (p_search_flags & SEARCH_BACKWARDS) { + from_column = text_line.length(); + } else { + from_column = 0; + } - if (!is_wrap_enabled()) { - // Adjust x offset. - int cursor_x = get_column_x_offset(cursor.column, text[cursor.line]); + } else { + from_column = p_from_column; + } - if (cursor_x > (cursor.x_ofs + visible_width)) { - cursor.x_ofs = cursor_x - visible_width + 1; + } else { + if (p_search_flags & SEARCH_BACKWARDS) { + from_column = text_line.length() - 1; + } else { + from_column = 0; + } } - if (cursor_x < cursor.x_ofs) { - cursor.x_ofs = cursor_x; - } - } else { - cursor.x_ofs = 0; - } - h_scroll->set_value(cursor.x_ofs); + pos = -1; - update(); -} + int pos_from = (p_search_flags & SEARCH_BACKWARDS) ? text_line.length() : 0; + int last_pos = -1; -void TextEdit::center_viewport_to_cursor() { - // Move viewport so the cursor is in the center of the screen. - scrolling = false; - minimap_clicked = false; + 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(); + } + } - if (is_line_hidden(cursor.line)) { - unfold_line(cursor.line); - } + bool is_match = true; - 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; - } - visible_width -= 20; // Give it a little more space. + 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 (is_wrap_enabled()) { - // Center x offset. - int cursor_x = get_column_x_offset_for_line(cursor.column, cursor.line); + if (pos_from == -1) { + pos = -1; + } - if (cursor_x > (cursor.x_ofs + visible_width)) { - cursor.x_ofs = cursor_x - visible_width + 1; - } + if (is_match || last_pos == -1 || pos == -1) { + break; + } - if (cursor_x < cursor.x_ofs) { - cursor.x_ofs = cursor_x; + pos_from = (p_search_flags & SEARCH_BACKWARDS) ? pos - 1 : pos + 1; + pos = -1; } - } else { - cursor.x_ofs = 0; - } - h_scroll->set_value(cursor.x_ofs); - update(); -} + if (pos != -1) { + break; + } -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; + if (p_search_flags & SEARCH_BACKWARDS) { + line--; + } else { + line++; + } } - set_line_as_first_visible(cursor.line_ofs, cursor.wrap_ofs); + return (pos == -1) ? Point2i(-1, -1) : Point2i(pos, line); } -bool TextEdit::line_wraps(int line) const { - ERR_FAIL_INDEX_V(line, text.size(), 0); - if (!is_wrap_enabled()) { - return false; +/* 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; } - return text.get_line_width(line) > wrap_at; + return mp; } -int TextEdit::times_line_wraps(int line) const { - ERR_FAIL_INDEX_V(line, text.size(), 0); - if (!line_wraps(line)) { - return 0; +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]; + } + } + } + } - int wrap_amount = text.get_line_wrap_amount(line); - if (wrap_amount == -1) { - // Update the value. - Vector<String> rows = get_wrap_rows_text(line); - wrap_amount = rows.size() - 1; - text.set_line_wrap_amount(line, wrap_amount); + return s.substr(beg, end - beg); } - return wrap_amount; + return String(); } -Vector<String> TextEdit::get_wrap_rows_text(int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), Vector<String>()); +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; - Vector<String> lines; - if (!line_wraps(p_line)) { - lines.push_back(text[p_line]); - return lines; + 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; } - int px = 0; int col = 0; - String line_text = text[p_line]; - String wrap_substring = ""; - int word_px = 0; - String word_str = ""; - int cur_wrap_index = 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; + } + } - int tab_offset_px = get_indent_level(p_line) * cache.font->get_char_size(' ').width; - if (tab_offset_px >= wrap_at) { - tab_offset_px = 0; + 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); } - while (col < line_text.length()) { - char32_t c = line_text[col]; - int w = text.get_char_width(c, line_text[col + 1], px + word_px); + return Point2i(col, row); +} - int indent_ofs = (cur_wrap_index != 0 ? tab_offset_px : 0); +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(); - if (indent_ofs + word_px + w > wrap_at) { - // Not enough space to add this char; start next line. - wrap_substring += word_str; - lines.push_back(wrap_substring); - cur_wrap_index++; - wrap_substring = ""; - px = 0; + // 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); - word_str = ""; - word_str += c; - word_px = w; - } else { - word_str += c; - word_px += w; - if (c == ' ') { - // End of a word; add this word to the substring. - wrap_substring += word_str; - px += word_px; - word_str = ""; - word_px = 0; - } + // 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)); - if (indent_ofs + px + word_px > wrap_at) { - // This word will be moved to the next line. - lines.push_back(wrap_substring); - // Reset for next wrap. - cur_wrap_index++; - wrap_substring = ""; - px = 0; - } + // 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; + } + + 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; } - col++; } - // Line ends before hit wrap_at; add this word to the substring. - wrap_substring += word_str; - lines.push_back(wrap_substring); - // Update cache. - text.set_line_wrap_amount(p_line, lines.size() - 1); + if (row < 0) { + row = 0; + } - return lines; + 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::cursor_set_column(int p_col, bool p_adjust_viewport) { - if (p_col < 0) { - p_col = 0; - } +bool TextEdit::is_caret_blink_enabled() const { + return caret_blink_enabled; +} - cursor.column = p_col; - if (cursor.column > get_line(cursor.line).length()) { - cursor.column = get_line(cursor.line).length(); - } +float TextEdit::get_caret_blink_speed() const { + return caret_blink_timer->get_wait_time(); +} - cursor.last_fit_x = get_column_x_offset_for_line(cursor.column, cursor.line); +void TextEdit::set_caret_blink_speed(const float p_speed) { + ERR_FAIL_COND(p_speed <= 0); + caret_blink_timer->set_wait_time(p_speed); +} - if (p_adjust_viewport) { - adjust_viewport_to_cursor(); - } +void TextEdit::set_move_caret_on_right_click_enabled(const bool p_enable) { + move_caret_on_right_click = p_enable; +} - if (!cursor_changed_dirty) { - if (is_inside_tree()) { - MessageQueue::get_singleton()->push_call(this, "_cursor_changed_emit"); - } - cursor_changed_dirty = true; - } +bool TextEdit::is_move_caret_on_right_click_enabled() const { + return move_caret_on_right_click; +} + +void TextEdit::set_caret_mid_grapheme_enabled(const bool p_enabled) { + caret_mid_grapheme_enabled = p_enabled; +} + +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(); @@ -4280,76 +3449,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) { @@ -4359,465 +3539,549 @@ void TextEdit::set_selection_mode(SelectionMode p_mode, int p_line, int p_column selection.selecting_line = p_line; } if (p_column >= 0) { - ERR_FAIL_INDEX(p_line, text[selection.selecting_line].length()); + ERR_FAIL_INDEX(p_column, text[selection.selecting_line].length()); selection.selecting_column = p_column; } } -int TextEdit::get_selection_line() const { - return selection.selecting_line; -}; +TextEdit::SelectionMode TextEdit::get_selection_mode() const { + return selection.selecting_mode; +} -int TextEdit::get_selection_column() const { - return selection.selecting_column; -}; +void TextEdit::select_all() { + if (!selecting_enabled) { + return; + } -void TextEdit::_v_scroll_input() { - scrolling = false; - minimap_clicked = false; + 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(); } -void TextEdit::_scroll_moved(double p_to_val) { - if (updating_scrolls) { +void TextEdit::select_word_under_caret() { + if (!selecting_enabled) { return; } - if (h_scroll->is_visible_in_tree()) { - cursor.x_ofs = h_scroll->get_value(); + if (text.size() == 1 && text[0].length() == 0) { + return; } - 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 (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; } - update(); -} -int TextEdit::get_row_height() const { - return cache.font->get_height() + cache.line_spacing; + 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; + } + } + + select(caret.line, begin, caret.line, end); + /* Move the caret to the end of the word for easier editing. */ + set_caret_column(end, false); } -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); +void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { + if (!selecting_enabled) { + return; + } - if (line_wraps(p_line)) { - int line_wrap_amount = times_line_wraps(p_line); - int wrap_offset_px = get_indent_level(p_line) * cache.font->get_char_size(' ').width; - if (wrap_offset_px >= wrap_at) { - wrap_offset_px = 0; - } - if (p_wrap_index > line_wrap_amount) { - p_wrap_index = line_wrap_amount; - } - if (p_wrap_index > 0) { - p_px -= wrap_offset_px; - } else { - p_wrap_index = 0; - } - Vector<String> rows = get_wrap_rows_text(p_line); - int c_pos = get_char_pos_for(p_px, rows[p_wrap_index]); - for (int i = 0; i < p_wrap_index; i++) { - String s = rows[i]; - c_pos += s.length(); - } + 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; + } - return c_pos; - } else { - return get_char_pos_for(p_px, text[p_line]); + 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 TextEdit::get_column_x_offset_for_line(int p_char, int p_line) const { - ERR_FAIL_INDEX_V(p_line, text.size(), 0); + selection.from_line = p_from_line; + selection.from_column = p_from_column; + selection.to_line = p_to_line; + selection.to_column = p_to_column; - if (line_wraps(p_line)) { - int n_char = p_char; - int col = 0; - Vector<String> rows = get_wrap_rows_text(p_line); - int wrap_index = 0; - for (int i = 0; i < rows.size(); i++) { - wrap_index = i; - String s = rows[wrap_index]; - col += s.length(); - if (col > p_char) { - break; - } - n_char -= s.length(); - } - int px = get_column_x_offset(n_char, rows[wrap_index]); + selection.active = true; - int wrap_offset_px = get_indent_level(p_line) * cache.font->get_char_size(' ').width; - if (wrap_offset_px >= wrap_at) { - wrap_offset_px = 0; - } - if (wrap_index != 0) { - px += wrap_offset_px; - } + if (selection.from_line == selection.to_line) { + if (selection.from_column == selection.to_column) { + selection.active = false; - return px; + } 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 { - return get_column_x_offset(p_char, text[p_line]); + selection.shiftclick_left = true; } -} -int TextEdit::get_char_pos_for(int p_px, String p_str) const { - int px = 0; - int c = 0; + update(); +} - while (c < p_str.length()) { - int w = text.get_char_width(p_str[c], p_str[c + 1], px); +bool TextEdit::has_selection() const { + return selection.active; +} - if (p_px < (px + w / 2)) { - break; - } - px += w; - c++; +String TextEdit::get_selected_text() const { + if (!selection.active) { + return ""; } - return c; + return _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); } -int TextEdit::get_column_x_offset(int p_char, String p_str) const { - int px = 0; +int TextEdit::get_selection_line() const { + return selection.selecting_line; +} - for (int i = 0; i < p_char; i++) { - if (i >= p_str.length()) { - break; - } +int TextEdit::get_selection_column() const { + return selection.selecting_column; +} - px += text.get_char_width(p_str[i], p_str[i + 1], px); - } +int TextEdit::get_selection_from_line() const { + ERR_FAIL_COND_V(!selection.active, -1); + return selection.from_line; +} + +int TextEdit::get_selection_from_column() const { + ERR_FAIL_COND_V(!selection.active, -1); + return selection.from_column; +} - return px; +int TextEdit::get_selection_to_line() const { + ERR_FAIL_COND_V(!selection.active, -1); + return selection.to_line; } -void TextEdit::insert_text_at_cursor(const String &p_text) { - if (selection.active) { - cursor_set_line(selection.from_line); - cursor_set_column(selection.from_column); +int TextEdit::get_selection_to_column() const { + ERR_FAIL_COND_V(!selection.active, -1); + return selection.to_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::deselect() { + selection.active = false; + update(); +} + +void TextEdit::delete_selection() { + if (!has_selection()) { + return; } - _insert_text_at_cursor(p_text); + 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(); } -Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { - if (highlighted_word != String()) { - return CURSOR_POINTING_HAND; +/* 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); } +} - if ((completion_active && completion_rect.has_point(p_pos))) { - return CURSOR_ARROW; +TextEdit::LineWrappingMode TextEdit::get_line_wrapping_mode() const { + return line_wrapping_mode; +} + +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; } + return text.get_line_wrap_amount(p_line) > 0; +} - int row, col; - _get_mouse_pos(p_pos, row, col); +int TextEdit::get_line_wrap_count(int p_line) const { + ERR_FAIL_INDEX_V(p_line, text.size(), 0); - int left_margin = cache.style_normal->get_margin(MARGIN_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 (!is_line_wrapped(p_line)) { + return 0; + } - 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; + return text.get_line_wrap_amount(p_line); +} + +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; } - return CURSOR_ARROW; } + return wrap_index; +} - int xmargin_end = get_size().width - cache.style_normal->get_margin(MARGIN_RIGHT); - if (draw_minimap && p_pos.x > xmargin_end - minimap_width && p_pos.x <= xmargin_end) { - return CURSOR_ARROW; +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; } - // 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; - } + 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 get_default_cursor_shape(); + return lines; } -void TextEdit::set_text(String p_text) { - setting_text = true; - if (!undo_enabled) { - _clear(); - _insert_text_at_cursor(p_text); +/* 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; +} + +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(); +} + +bool TextEdit::is_scroll_past_end_of_file_enabled() const { + return scroll_past_end_of_file_enabled; +} + +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 (undo_enabled) { - cursor_set_line(0); - cursor_set_column(0); +double TextEdit::get_v_scroll() const { + return v_scroll->get_value(); +} - 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; +void TextEdit::set_h_scroll(int p_scroll) { + if (p_scroll < 0) { + p_scroll = 0; } + h_scroll->set_value(p_scroll); +} - cursor_set_line(0); - cursor_set_column(0); +int TextEdit::get_h_scroll() const { + return h_scroll->get_value(); +} - update(); - setting_text = false; -}; +void TextEdit::set_v_scroll_speed(float p_speed) { + v_scroll_speed = p_speed; +} -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"; +float TextEdit::get_v_scroll_speed() const { + return v_scroll_speed; +} + +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); + + if (get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE && !_is_hiding_enabled()) { + return p_line; + } + + // 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); } } + new_line_scroll_pos += p_wrap_index; + return new_line_scroll_pos; +} - return longthing; -}; +// 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)); +} -String TextEdit::get_text_for_lookup_completion() { - int row, col; - _get_mouse_pos(get_local_mouse_position(), row, col); +int TextEdit::get_first_visible_line() const { + return CLAMP(caret.line_ofs, 0, text.size() - 1); +} - 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]; - } +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)); - if (i != len - 1) { - longthing += "\n"; - } - } + 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; - return longthing; + set_v_scroll(get_scroll_pos_for_line(first_line, next_line.y)); } -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]; - } +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)); - if (i != len - 1) { - longthing += "\n"; - } - } + 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; - return longthing; -}; + set_v_scroll(get_scroll_pos_for_line(first_line, next_line.y) + _get_visible_lines_offset()); +} -String TextEdit::get_line(int line) const { - if (line < 0 || line >= text.size()) { - return ""; - } +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; +} - return text[line]; -}; +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; +} -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; +int TextEdit::get_visible_line_count() const { + return _get_control_height() / get_line_height(); } -void TextEdit::clear() { - setting_text = true; - _clear(); - setting_text = false; -}; +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(); + } -void TextEdit::set_readonly(bool p_readonly) { - if (readonly == p_readonly) { - return; + 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; +} - readonly = p_readonly; - _generate_context_menu(); +// Auto adjust +void TextEdit::adjust_viewport_to_caret() { + // Make sure Caret is visible on the screen. + scrolling = false; + minimap_clicked = false; - // Reorganize context menu. - menu->clear(); + 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 (!readonly) { - menu->add_item(RTR("Undo"), MENU_UNDO, KEY_MASK_CMD | KEY_Z); - menu->add_item(RTR("Redo"), MENU_REDO, KEY_MASK_CMD | KEY_MASK_SHIFT | KEY_Z); + 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); } - if (!readonly) { - menu->add_separator(); - menu->add_item(RTR("Cut"), MENU_CUT, KEY_MASK_CMD | KEY_X); + 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. - menu->add_item(RTR("Copy"), MENU_COPY, KEY_MASK_CMD | KEY_C); + if (get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) { + // Adjust x offset. + Vector2i caret_pos; - if (!readonly) { - menu->add_item(RTR("Paste"), MENU_PASTE, KEY_MASK_CMD | KEY_V); - } + // 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); + } - menu->add_separator(); - menu->add_item(RTR("Select All"), MENU_SELECT_ALL, KEY_MASK_CMD | KEY_A); + // 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 (!readonly) { - menu->add_item(RTR("Clear"), MENU_CLEAR); + 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(); } -bool TextEdit::is_readonly() const { - return readonly; -} +void TextEdit::center_viewport_to_caret() { + // Move viewport so the caret is in the center of the screen. + scrolling = false; + minimap_clicked = false; -void TextEdit::set_wrap_enabled(bool p_wrap_enabled) { - wrap_enabled = p_wrap_enabled; -} + 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. -bool TextEdit::is_wrap_enabled() const { - return wrap_enabled; -} + if (get_line_wrapping_mode() != LineWrappingMode::LINE_WRAPPING_NONE) { + // Center x offset. -void TextEdit::set_max_chars(int p_max_chars) { - max_chars = p_max_chars; -} + Vector2i caret_pos; -int TextEdit::get_max_chars() const { - return max_chars; -} + // 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); + } -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(); + // 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; } - } -} -void TextEdit::_toggle_draw_caret() { - draw_caret = !draw_caret; - if (is_visible_in_tree() && has_focus() && window_has_focus) { - update(); + 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); -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.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_color_selected = get_theme_color("font_color_selected"); - cache.font_color_readonly = get_theme_color("font_color_readonly"); - cache.selection_color = get_theme_color("selection_color"); - cache.mark_color = get_theme_color("mark_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.row_height = cache.font->get_height() + cache.line_spacing; - cache.tab_icon = get_theme_icon("tab"); - cache.space_icon = get_theme_icon("space"); - cache.folded_eol_icon = get_theme_icon("GuiEllipsis", "EditorIcons"); - text.set_font(cache.font); - text.clear_width_cache(); + update(); +} - if (syntax_highlighter.is_valid()) { - syntax_highlighter->set_text_edit(this); +/* 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()); @@ -4828,7 +4092,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(); } @@ -4840,7 +4104,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(); } @@ -4880,6 +4144,10 @@ 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()); gutters.write[p_gutter].draw = p_draw; @@ -4912,6 +4180,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); @@ -4947,7 +4248,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); @@ -4967,7 +4268,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); @@ -4985,747 +4286,843 @@ bool TextEdit::is_line_gutter_clickable(int p_line, int p_gutter) const { return text.is_line_gutter_clickable(p_line, p_gutter); } -void TextEdit::add_keyword(const String &p_keyword) { - keywords.insert(p_keyword); +// Line style +void TextEdit::set_line_background_color(int p_line, const Color &p_color) { + ERR_FAIL_INDEX(p_line, text.size()); + text.set_line_background_color(p_line, p_color); + update(); } -void TextEdit::clear_keywords() { - keywords.clear(); +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::set_auto_indent(bool p_auto_indent) { - auto_indent = p_auto_indent; +/* 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(); } -void TextEdit::cut() { - if (!selection.active) { - String clipboard = text[cursor.line]; - DisplayServer::get_singleton()->clipboard_set(clipboard); - cursor_set_line(cursor.line); - cursor_set_column(0); - - 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); - } +Ref<SyntaxHighlighter> TextEdit::get_syntax_highlighter() const { + return syntax_highlighter; +} - update(); - cut_copy_line = clipboard; +/* Visual. */ +void TextEdit::set_highlight_current_line(bool p_enabled) { + highlight_current_line = p_enabled; + update(); +} - } 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::is_highlight_current_line_enabled() const { + return highlight_current_line; +} - _remove_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); - cursor_set_line(selection.from_line); // Set afterwards else it causes the view to be offset. - cursor_set_column(selection.from_column); +void TextEdit::set_highlight_all_occurrences(const bool p_enabled) { + highlight_all_occurrences = p_enabled; + update(); +} - selection.active = false; - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; - update(); - cut_copy_line = ""; - } +bool TextEdit::is_highlight_all_occurrences_enabled() const { + return highlight_all_occurrences; } -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; +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); } - } 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 = ""; + text.set_draw_control_chars(draw_control_chars); + text.invalidate_all(); + update(); } } -void TextEdit::paste() { - String clipboard = DisplayServer::get_singleton()->clipboard_get(); - - 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); - cursor_set_column(selection.from_column); - - } else if (!cut_copy_line.empty() && cut_copy_line == clipboard) { - cursor_set_column(0); - String ins = "\n"; - clipboard += ins; - } - - _insert_text_at_cursor(clipboard); - end_complex_operation(); +bool TextEdit::get_draw_control_chars() const { + return draw_control_chars; +} +void TextEdit::set_draw_tabs(bool p_draw) { + draw_tabs = p_draw; update(); } -void TextEdit::select_all() { - if (!selecting_enabled) { - return; - } +bool TextEdit::is_drawing_tabs() const { + return draw_tabs; +} - 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); +void TextEdit::set_draw_spaces(bool p_draw) { + draw_spaces = p_draw; update(); } -void TextEdit::deselect() { - selection.active = false; - update(); +bool TextEdit::is_drawing_spaces() const { + return draw_spaces; } -void TextEdit::select(int p_from_line, int p_from_column, int p_to_line, int p_to_column) { - if (!selecting_enabled) { - return; - } +void TextEdit::_bind_methods() { + /*Internal. */ - 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("_text_changed_emit"), &TextEdit::_text_changed_emit); - 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; - } + /* Text */ + // Text properties + ClassDB::bind_method(D_METHOD("has_ime_text"), &TextEdit::has_ime_text); - 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("set_editable", "enable"), &TextEdit::set_editable); + ClassDB::bind_method(D_METHOD("is_editable"), &TextEdit::is_editable); - selection.active = true; + 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); - if (selection.from_line == selection.to_line) { - if (selection.from_column == selection.to_column) { - selection.active = false; + 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); - } 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("set_language", "language"), &TextEdit::set_language); + ClassDB::bind_method(D_METHOD("get_language"), &TextEdit::get_language); - update(); -} + 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); -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("set_tab_size", "size"), &TextEdit::set_tab_size); + ClassDB::bind_method(D_METHOD("get_tab_size"), &TextEdit::get_tab_size); -bool TextEdit::is_selection_active() const { - return selection.active; -} + // 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 TextEdit::get_selection_from_line() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.from_line; -} + 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); -int TextEdit::get_selection_from_column() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.from_column; -} + 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); -int TextEdit::get_selection_to_line() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.to_line; -} + 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); -int TextEdit::get_selection_to_column() const { - ERR_FAIL_COND_V(!selection.active, -1); - return selection.to_column; -} + // Text manipulation + ClassDB::bind_method(D_METHOD("clear"), &TextEdit::clear); -String TextEdit::get_selection_text() const { - if (!selection.active) { - return ""; - } + 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); - return _base_get_text(selection.from_line, selection.from_column, selection.to_line, selection.to_column); -} + ClassDB::bind_method(D_METHOD("set_line", "line", "new_text"), &TextEdit::set_line); + ClassDB::bind_method(D_METHOD("get_line", "line"), &TextEdit::get_line); -String TextEdit::get_word_under_cursor() const { - int prev_cc = cursor.column; - while (prev_cc > 0) { - bool is_char = _is_text_char(text[cursor.line][prev_cc - 1]); - if (!is_char) { - break; - } - --prev_cc; - } + 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); - int next_cc = cursor.column; - while (next_cc < text[cursor.line].length()) { - bool is_char = _is_text_char(text[cursor.line][next_cc]); - if (!is_char) { - break; - } - ++next_cc; - } - if (prev_cc == cursor.column || next_cc == cursor.column) { - return ""; - } - return text[cursor.line].substr(prev_cc, next_cc - prev_cc); -} + 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); -void TextEdit::set_search_text(const String &p_search_text) { - search_text = p_search_text; -} + ClassDB::bind_method(D_METHOD("swap_lines", "from_line", "to_line"), &TextEdit::swap_lines); -void TextEdit::set_search_flags(uint32_t p_flags) { - search_flags = p_flags; -} + 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); -void TextEdit::set_current_search_result(int line, int col) { - search_result_line = line; - search_result_col = col; - update(); -} + ClassDB::bind_method(D_METHOD("remove_text", "from_line", "from_column", "to_line", "to_column"), &TextEdit::remove_text); -void TextEdit::set_highlight_all_occurrences(const bool p_enabled) { - highlight_all_occurrences = p_enabled; - update(); -} + 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_highlight_all_occurrences_enabled() const { - return highlight_all_occurrences; -} + // Overridable actions + ClassDB::bind_method(D_METHOD("backspace"), &TextEdit::backspace); -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("cut"), &TextEdit::cut); + ClassDB::bind_method(D_METHOD("copy"), &TextEdit::copy); + ClassDB::bind_method(D_METHOD("paste"), &TextEdit::paste); - if (p_key.length() > 0 && p_search.length() > 0) { - if (p_from_column < 0 || p_from_column > p_search.length()) { - p_from_column = 0; - } + GDVIRTUAL_BIND(_handle_unicode_input, "unicode_char") + GDVIRTUAL_BIND(_backspace) + GDVIRTUAL_BIND(_cut) + GDVIRTUAL_BIND(_copy) + GDVIRTUAL_BIND(_paste) - 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); - } + // 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); - // Whole words only. - if (col != -1 && p_search_flags & SEARCH_WHOLE_WORDS) { - p_from_column = col; + /* 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); - 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; - } - } + 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); - p_from_column += 1; - } - } - return col; -} + ClassDB::bind_method(D_METHOD("tag_saved_version"), &TextEdit::tag_saved_version); -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("get_version"), &TextEdit::get_version); + ClassDB::bind_method(D_METHOD("get_saved_version"), &TextEdit::get_saved_version); - } else { - return Dictionary(); - } -} + /* Search */ + BIND_ENUM_CONSTANT(SEARCH_MATCH_CASE); + BIND_ENUM_CONSTANT(SEARCH_WHOLE_WORDS); + BIND_ENUM_CONSTANT(SEARCH_BACKWARDS); -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_search_text", "search_text"), &TextEdit::set_search_text); + ClassDB::bind_method(D_METHOD("set_search_flags", "flags"), &TextEdit::set_search_flags); - // Search through the whole document, but start by current line. + ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_colum"), &TextEdit::search); - int line = p_from_line; - int pos = -1; + /* Tooltip */ + ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "object", "callback", "data"), &TextEdit::set_tooltip_request_func); - for (int i = 0; i < text.size() + 1; i++) { - if (line < 0) { - line = text.size() - 1; - } - if (line == text.size()) { - line = 0; - } + /* Mouse */ + ClassDB::bind_method(D_METHOD("get_local_mouse_pos"), &TextEdit::get_local_mouse_pos); - String text_line = text[line]; - int from_column = 0; - if (line == p_from_line) { - if (i == text.size()) { - // Wrapped. + ClassDB::bind_method(D_METHOD("get_word_at_pos", "position"), &TextEdit::get_word_at_pos); - if (p_search_flags & SEARCH_BACKWARDS) { - from_column = text_line.length(); - } else { - 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); - } else { - from_column = p_from_column; - } + ClassDB::bind_method(D_METHOD("is_dragging_cursor"), &TextEdit::is_dragging_cursor); - } else { - if (p_search_flags & SEARCH_BACKWARDS) { - from_column = text_line.length() - 1; - } else { - from_column = 0; - } - } + /* Caret. */ + BIND_ENUM_CONSTANT(CARET_TYPE_LINE); + BIND_ENUM_CONSTANT(CARET_TYPE_BLOCK); - pos = -1; + // internal. + ClassDB::bind_method(D_METHOD("_emit_caret_changed"), &TextEdit::_emit_caret_changed); - int pos_from = (p_search_flags & SEARCH_BACKWARDS) ? text_line.length() : 0; - int last_pos = -1; + 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); - 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_caret_blink_enabled", "enable"), &TextEdit::set_caret_blink_enabled); + ClassDB::bind_method(D_METHOD("is_caret_blink_enabled"), &TextEdit::is_caret_blink_enabled); - bool is_match = true; + 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); - 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("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); - if (pos_from == -1) { - pos = -1; - } + 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); - if (is_match || last_pos == -1 || pos == -1) { - break; - } + 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); - pos_from = (p_search_flags & SEARCH_BACKWARDS) ? pos - 1 : pos + 1; - pos = -1; - } + 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); - if (pos != -1) { - break; - } + 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) { - line--; - } else { - line++; - } - } + ClassDB::bind_method(D_METHOD("get_caret_wrap_index"), &TextEdit::get_caret_wrap_index); - if (pos == -1) { - r_line = -1; - r_column = -1; - return false; - } + ClassDB::bind_method(D_METHOD("get_word_under_caret"), &TextEdit::get_word_under_caret); + + /* 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); - r_line = line; - r_column = pos; + 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); - return true; + 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); + + 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); + + 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); + + ClassDB::bind_method(D_METHOD("has_selection"), &TextEdit::has_selection); + + ClassDB::bind_method(D_METHOD("get_selected_text"), &TextEdit::get_selected_text); + + ClassDB::bind_method(D_METHOD("get_selection_line"), &TextEdit::get_selection_line); + ClassDB::bind_method(D_METHOD("get_selection_column"), &TextEdit::get_selection_column); + + 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("deselect"), &TextEdit::deselect); + ClassDB::bind_method(D_METHOD("delete_selection"), &TextEdit::delete_selection); + + /* 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. } -void TextEdit::_cursor_changed_emit() { - emit_signal("cursor_changed"); - cursor_changed_dirty = false; +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 false; } -void TextEdit::_text_changed_emit() { - emit_signal("text_changed"); - text_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::set_line_as_marked(int p_line, bool p_marked) { - ERR_FAIL_INDEX(p_line, text.size()); - text.set_marked(p_line, p_marked); - update(); +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)) - (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 = (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); + + 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, &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) { @@ -5761,1107 +5158,791 @@ void TextEdit::_clear_redo() { } } -void TextEdit::undo() { - _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); - cursor_set_column(undo_stack_pos->get().to_column); - _cancel_code_hint(); - } else { - cursor_set_line(undo_stack_pos->get().from_line); - cursor_set_column(undo_stack_pos->get().from_column); + p_from_column += 1; + } } - update(); + return col; } -void TextEdit::redo() { - _push_current_op(); +/* 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); - if (undo_stack_pos == nullptr) { - return; // Nothing to do. + 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; } - - 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; - } - } - } - - _update_scrollbars(); - cursor_set_line(undo_stack_pos->get().to_line); - 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; -} - -bool TextEdit::is_indent_using_spaces() const { - return indent_using_spaces; -} - -void TextEdit::set_indent_size(const int p_size) { - ERR_FAIL_COND_MSG(p_size <= 0, "Indend size must be greater than 0."); - indent_size = p_size; - text.set_indent_size(p_size); - - space_indent = ""; - for (int i = 0; i < p_size; i++) { - space_indent += " "; +void TextEdit::_toggle_draw_caret() { + draw_caret = !draw_caret; + if (is_visible_in_tree() && has_focus() && window_has_focus) { + update(); } - - update(); } -int TextEdit::get_indent_size() { - return indent_size; -} +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_draw_tabs(bool p_draw) { - draw_tabs = p_draw; - update(); -} + 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; + } + } -bool TextEdit::is_drawing_tabs() const { - return draw_tabs; + 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; + } } -void TextEdit::set_draw_spaces(bool p_draw) { - draw_spaces = p_draw; +/* 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; - 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_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() + 1, 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_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() + 1, 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(true) + vmin.x + gutters_width + gutter_padding; - _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); + if (draw_minimap) { + total_width += minimap_width; + } - // 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]; + updating_scrolls = true; - 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); + 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()); + + } 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.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); +// Minimap +void TextEdit::_update_minimap_click() { + Point2 mp = get_local_mouse_pos(); - return tt; + 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; } + minimap_clicked = true; + dragging_minimap = true; - return Control::get_tooltip(p_pos); -} + int row = get_minimap_line_at_pos(Point2i(mp.x, mp.y)); -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; -} - -void TextEdit::set_line(int line, String new_text) { - if (line < 0 || line >= text.size()) { + 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; } - _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; - } + 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); } } -void TextEdit::set_show_line_length_guidelines(bool p_show) { - line_length_guidelines = p_show; - update(); -} - -void TextEdit::set_line_length_guideline_soft_column(int p_column) { - line_length_guideline_soft_col = p_column; - update(); -} - -void TextEdit::set_line_length_guideline_hard_column(int p_column) { - line_length_guideline_hard_col = p_column; - update(); -} - -void TextEdit::set_draw_minimap(bool p_draw) { - draw_minimap = p_draw; - update(); -} - -bool TextEdit::is_drawing_minimap() const { - return draw_minimap; -} - -void TextEdit::set_minimap_width(int p_minimap_width) { - minimap_width = p_minimap_width; - update(); -} - -int TextEdit::get_minimap_width() const { - return minimap_width; -} - -void TextEdit::set_hiding_enabled(bool p_enabled) { - if (!p_enabled) { - unhide_all_lines(); +void TextEdit::_update_minimap_drag() { + if (!can_drag_minimap) { + return; } - hiding_enabled = p_enabled; - update(); -} -bool TextEdit::is_hiding_enabled() const { - return hiding_enabled; -} - -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(); +/* 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::_text_changed_emit() { + emit_signal(SNAME("text_changed")); + text_changed_dirty = false; } -void TextEdit::set_virtual_keyboard_enabled(bool p_enable) { - virtual_keyboard_enabled = p_enable; -} +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(); + } -void TextEdit::set_selecting_enabled(bool p_enabled) { - selecting_enabled = p_enabled; + if (undo_enabled) { + _clear_redo(); + } - if (!selecting_enabled) { - deselect(); + 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; } - _generate_context_menu(); -} + if (!undo_enabled) { + return; + } -bool TextEdit::is_selecting_enabled() const { - return selecting_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; -bool TextEdit::is_shortcut_keys_enabled() const { - return shortcut_keys_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; -bool TextEdit::is_virtual_keyboard_enabled() const { - return virtual_keyboard_enabled; -} + 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. -PopupMenu *TextEdit::get_menu() const { - return menu; + current_op.text += p_text; + current_op.to_column = retchar; + current_op.to_line = retline; + current_op.version = op.version; } -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"), &TextEdit::_update_wrap_at); - - 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); +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(); + } - /* - ClassDB::bind_method(D_METHOD("delete_char"),&TextEdit::delete_char); - ClassDB::bind_method(D_METHOD("delete_line"),&TextEdit::delete_line); -*/ + String text; + if (undo_enabled) { + _clear_redo(); + text = _base_get_text(p_from_line, p_from_column, p_to_line, p_to_column); + } - 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); + _base_remove_text(p_from_line, p_from_column, p_to_line, p_to_column); - 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("set_line", "line", "new_text"), &TextEdit::set_line); + if (!undo_enabled) { + return; + } - 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)); + /* 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("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); + // 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("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); + op.prev_version = get_version(); + _push_current_op(); + current_op = op; +} - 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); +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("set_readonly", "enable"), &TextEdit::set_readonly); - ClassDB::bind_method(D_METHOD("is_readonly"), &TextEdit::is_readonly); + /* STEP 1: Remove \r from source text and separate in substrings. */ - 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); + Vector<String> substrings = p_text.replace("\r", "").split("\n"); - ClassDB::bind_method(D_METHOD("cut"), &TextEdit::cut); - ClassDB::bind_method(D_METHOD("copy"), &TextEdit::copy); - ClassDB::bind_method(D_METHOD("paste"), &TextEdit::paste); + // Is this just a new empty line? + bool shift_first_line = p_char == 0 && p_text.replace("\r", "") == "\n"; - 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); + /* 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("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 3: Separate dest string in pre and post text. */ - 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); + 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_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); + for (int j = 0; j < substrings.size(); j++) { + // Insert the substrings. - 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); + 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("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); + 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)); + } + } - 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); + if (shift_first_line) { + text.move_gutters(p_line, p_line + 1); + text.set_hidden(p_line + 1, text.is_hidden(p_line)); - 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); + text.set_hidden(p_line, false); + } - /* Gutters. */ - BIND_ENUM_CONSTANT(GUTTER_TYPE_STRING); - BIND_ENUM_CONSTANT(GUTTER_TPYE_ICON); - BIND_ENUM_CONSTANT(GUTTER_TPYE_CUSTOM); + text.invalidate_cache(p_line); - 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); + r_end_line = p_line + substrings.size() - 1; + r_end_column = text[r_end_line].length() - postinsert_text.length(); - // 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); + 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("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); + 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("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); +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'. - ClassDB::bind_method(D_METHOD("menu_option", "option"), &TextEdit::menu_option); - ClassDB::bind_method(D_METHOD("get_menu"), &TextEdit::get_menu); + String ret; - 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); + 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_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); - 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"); + if (i > p_from_line) { + ret += "\n"; + } + ret += text[i].substr(begin, end - begin); + } - 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"); + return ret; +} - 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"); +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_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"); + 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()); - 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"))); + 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)); - 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_MAX); + text.invalidate_cache(p_from_line); - 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() { - setting_row = false; - draw_tabs = false; - draw_spaces = false; - override_selected_font_color = false; - draw_caret = true; - max_chars = 0; clear(); - wrap_enabled = false; - wrap_at = 0; - wrap_right_offset = 10; set_focus_mode(FOCUS_ALL); _update_caches(); - cache.row_height = 1; - cache.line_spacing = 1; set_default_cursor_shape(CURSOR_IBEAM); - indent_size = 4; - text.set_indent_size(indent_size); + text.set_tab_size(text.get_tab_size()); text.clear(); h_scroll = memnew(HScrollBar); @@ -6870,31 +5951,23 @@ TextEdit::TextEdit() { add_child(h_scroll); add_child(v_scroll); - updating_scrolls = false; - selection.active = false; - 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)); - cursor_changed_dirty = false; - text_changed_dirty = false; - - selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; - selection.selecting_line = 0; - selection.selecting_column = 0; - selection.selecting_text = false; - selection.active = false; - - block_caret = false; - caret_blink_enabled = false; + /* Caret. */ caret_blink_timer = memnew(Timer); add_child(caret_blink_timer); 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); - right_click_moves_caret = true; + set_caret_blink_enabled(false); + + /* Selection. */ + 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)); idle_detect = memnew(Timer); add_child(idle_detect); @@ -6902,72 +5975,7 @@ TextEdit::TextEdit() { 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)); - - current_op.type = TextOperation::TYPE_NONE; - undo_enabled = true; undo_stack_max_size = GLOBAL_GET("gui/common/text_edit_undo_stack_max_size"); - undo_stack_pos = nullptr; - setting_text = false; - last_dblclk = 0; - current_op.version = 0; - version = 0; - saved_version = 0; - completion_enabled = false; - completion_active = false; - completion_line_ofs = 0; - tooltip_obj = nullptr; - line_length_guidelines = false; - line_length_guideline_soft_col = 80; - line_length_guideline_hard_col = 100; - hiding_enabled = false; - next_operation_is_complex = false; - scroll_past_end_of_file_enabled = false; - auto_brace_completion_enabled = false; - brace_matching_enabled = false; - highlight_all_occurrences = false; - highlight_current_line = false; - indent_using_spaces = false; - space_indent = " "; - auto_indent = false; - insert_mode = false; - window_has_focus = true; - select_identifiers_enabled = false; - smooth_scroll_enabled = false; - scrolling = false; - minimap_clicked = false; - dragging_minimap = false; - can_drag_minimap = false; - minimap_scroll_ratio = 0; - minimap_scroll_click_pos = 0; - dragging_selection = false; - target_v_scroll = 0; - v_scroll_speed = 80; - draw_minimap = false; - minimap_width = 80; - minimap_char_size = Point2(1, 2); - minimap_line_spacing = 1; - - selecting_enabled = true; - context_menu_enabled = true; - shortcut_keys_enabled = true; - menu = memnew(PopupMenu); - add_child(menu); - readonly = true; // Initialise to opposite first, so we get past the early-out in set_readonly. - set_readonly(false); - menu->connect("id_pressed", callable_mp(this, &TextEdit::menu_option)); - first_draw = true; -} - -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 5cfa70bc55..ced03e19d0 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,17 +36,19 @@ #include "scene/gui/scroll_bar.h" #include "scene/main/timer.h" #include "scene/resources/syntax_highlighter.h" +#include "scene/resources/text_paragraph.h" 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, @@ -55,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; @@ -67,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: @@ -87,47 +138,67 @@ private: struct Line { Vector<Gutter> gutters; - int32_t width_cache; - bool marked; - bool hidden; - int32_t wrap_amount_cache; String data; + Vector<Vector2i> bidi_override; + Ref<TextParagraph> data_buf; + + Color background_color = Color(0, 0, 0, 0); + bool hidden = false; + Line() { - width_cache = 0; - marked = false; - hidden = false; - wrap_amount_cache = 0; + data_buf.instantiate(); } }; private: + bool is_dirty = false; + mutable Vector<Line> text; Ref<Font> font; - int indent_size = 4; - int gutter_count = 0; + int font_size = -1; - void _update_line_cache(int p_line) const; + Dictionary opentype_features; + String language; + TextServer::Direction direction = TextServer::DIRECTION_AUTO; + bool draw_control_chars = false; + + int width = -1; + + int tab_size = 4; + int gutter_count = 0; 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); - int get_line_width(int p_line) const; + 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, 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, int p_wrap_index = -1) const; int get_max_width(bool p_exclude_hidden = false) const; - int get_char_width(char32_t c, char32_t next_c, int px) const; - void set_line_wrap_amount(int p_line, int p_wrap_amount) const; + + void set_width(float p_width); int get_line_wrap_amount(int p_line) const; - void set(int p_line, const String &p_text); - void set_marked(int p_line, bool p_marked) { text.write[p_line].marked = p_marked; } - bool is_marked(int p_line) const { return text[p_line].marked; } + + Vector<Vector2i> get_line_wrap_ranges(int p_line) const; + 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; } bool is_hidden(int p_line) const { return text[p_line].hidden; } - void insert(int p_at, const String &p_text); + void insert(int p_at, const String &p_text, const Vector<Vector2i> &p_bidi_override); void remove(int p_at); int size() const { return text.size(); } void clear(); - void clear_width_cache(); - void clear_wrap_cache(); - _FORCE_INLINE_ const String &operator[](int p_line) const { return text[p_line].data; } + + void invalidate_cache(int p_line, int p_column = -1, const String &p_ime_text = String(), const Vector<Vector2i> &p_bidi_override = Vector<Vector2i>()); + void invalidate_all(); + void invalidate_all_lines(); + + _FORCE_INLINE_ const String &operator[](int p_line) const; /* Gutters. */ void add_gutter(int p_at); @@ -140,7 +211,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; } @@ -148,53 +219,54 @@ private: void set_line_gutter_clickable(int p_line, int p_gutter, bool p_clickable) { text.write[p_line].gutters.write[p_gutter].clickable = p_clickable; } bool is_line_gutter_clickable(int p_line, int p_gutter) const { return text[p_line].gutters[p_gutter].clickable; } + + /* Line style. */ + void set_line_background_color(int p_line, const Color &p_color) { text.write[p_line].background_color = p_color; } + const Color get_line_background_color(int p_line) const { return text[p_line].background_color; } }; - struct Cursor { - int last_fit_x; - int line, column; ///< cursor - int x_ofs, line_ofs, wrap_ofs; - Cursor() { - last_fit_x = 0; - line = 0; - column = 0; ///< cursor - x_ofs = 0; - line_ofs = 0; - wrap_ofs = 0; - } - } cursor; + /* Text */ + Text text; - struct Selection { - SelectionMode selecting_mode; - int selecting_line, selecting_column; - int selected_word_beg, selected_word_end, selected_word_origin; - bool selecting_text; - - bool active; - - int from_line, from_column; - int to_line, to_column; - - bool shiftclick_left; - Selection() { - selecting_mode = SelectionMode::SELECTION_MODE_NONE; - selecting_line = 0; - selecting_column = 0; - selected_word_beg = 0; - selected_word_end = 0; - selected_word_origin = 0; - selecting_text = false; - active = false; - from_line = 0; - from_column = 0; - to_line = 0; - to_column = 0; - shiftclick_left = false; - } - } selection; + bool setting_text = false; - Map<int, Dictionary> syntax_highlighting_cache; + // Text properties. + String ime_text = ""; + Point2 ime_selection; + + /* Initialise to opposite first, so we get past the early-out in set_editable. */ + bool editable = false; + + TextDirection text_direction = TEXT_DIRECTION_AUTO; + TextDirection input_direction = TEXT_DIRECTION_LTR; + + 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, @@ -202,570 +274,614 @@ private: TYPE_REMOVE }; - Type type; - int from_line, from_column; - int to_line, to_column; + Type type = TYPE_NONE; + int from_line = 0; + int from_column = 0; + int to_line = 0; + int to_column = 0; String text; - uint32_t prev_version; - uint32_t version; - bool chain_forward; - bool chain_backward; - TextOperation() { - type = TYPE_NONE; - from_line = 0; - from_column = 0; - to_line = 0; - to_column = 0; - prev_version = 0; - version = 0; - chain_forward = false; - chain_backward = false; - } + uint32_t prev_version = 0; + uint32_t version = 0; + bool chain_forward = false; + bool chain_backward = false; }; - String ime_text; - Point2 ime_selection; + bool undo_enabled = true; + int undo_stack_max_size = 50; - TextOperation current_op; + bool next_operation_is_complex = false; + TextOperation current_op; List<TextOperation> undo_stack; - List<TextOperation>::Element *undo_stack_pos; - int undo_stack_max_size; + List<TextOperation>::Element *undo_stack_pos = nullptr; - 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; - List<ScriptCodeCompletionOption> completion_sources; - Vector<ScriptCodeCompletionOption> completion_options; - bool completion_active; - bool completion_forced; - ScriptCodeCompletionOption completion_current; - String completion_base; - int completion_index; - Rect2i completion_rect; - int completion_line_ofs; - String completion_hint; - int completion_hint_offset; - - bool setting_text; - - // data - Text text; + int _get_column_pos_of_word(const String &p_key, const String &p_search, uint32_t p_search_flags, int p_from_column) const; - uint32_t version; - uint32_t saved_version; + /* Tooltip. */ + Object *tooltip_obj = nullptr; + StringName tooltip_func; + Variant tooltip_ud; - int max_chars; - bool readonly; - bool indent_using_spaces; - int indent_size; - String space_indent; + /* Mouse */ + int _get_char_pos_for_line(int p_px, int p_line, int p_wrap_index = 0) const; - Timer *caret_blink_timer; - bool caret_blink_enabled; - bool draw_caret; - bool window_has_focus; - bool block_caret; - bool right_click_moves_caret; - - bool wrap_enabled; - int wrap_at; - int wrap_right_offset; - - bool first_draw; - bool setting_row; - bool draw_tabs; - bool draw_spaces; - bool override_selected_font_color; - bool cursor_changed_dirty; - bool text_changed_dirty; - bool undo_enabled; - bool line_length_guidelines; - int line_length_guideline_soft_col; - int line_length_guideline_hard_col; - bool hiding_enabled; - bool draw_minimap; - int minimap_width; - Point2 minimap_char_size; - int minimap_line_spacing; - - bool highlight_all_occurrences; - bool scroll_past_end_of_file_enabled; - bool auto_brace_completion_enabled; - bool brace_matching_enabled; - bool highlight_current_line; - bool auto_indent; - String cut_copy_line; - bool insert_mode; - bool select_identifiers_enabled; - - bool smooth_scroll_enabled; - bool scrolling; - bool dragging_selection; - bool dragging_minimap; - bool can_drag_minimap; - bool minimap_clicked; - double minimap_scroll_ratio; - double minimap_scroll_click_pos; - float target_v_scroll; - float v_scroll_speed; - - String highlighted_word; - - uint64_t last_dblclk; + /* 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; - Timer *idle_detect; - Timer *click_select_held; - HScrollBar *h_scroll; - VScrollBar *v_scroll; - bool updating_scrolls; + bool setting_caret_line = false; + bool caret_pos_dirty = false; - Object *tooltip_obj; - StringName tooltip_func; - Variant tooltip_ud; + Color caret_color = Color(1, 1, 1); + Color caret_background_color = Color(0, 0, 0); - bool next_operation_is_complex; + CaretType caret_type = CaretType::CARET_TYPE_LINE; - bool callhint_below; - Vector2 callhint_offset; + bool draw_caret = true; - String search_text; - uint32_t search_flags; - int search_result_line; - int search_result_col; + bool caret_blink_enabled = false; + Timer *caret_blink_timer; - bool selecting_enabled; + bool move_caret_on_right_click = true; - bool context_menu_enabled; - bool shortcut_keys_enabled; - bool virtual_keyboard_enabled = true; + bool caret_mid_grapheme_enabled = false; - void _generate_context_menu(); + void _emit_caret_changed(); - int get_visible_rows() const; - int get_total_visible_rows() const; + void _reset_caret_blink_timer(); + void _toggle_draw_caret(); - int _get_minimap_visible_rows() const; + int _get_column_x_offset_for_line(int p_char, int p_line) const; - void update_cursor_wrap_offset(); - void _update_wrap_at(); - 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(); + /* 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 active = false; + + int from_line = 0; + int from_column = 0; + int to_line = 0; + int to_column = 0; + + bool shiftclick_left = false; + } 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_visible_line() const; - int get_last_visible_line_wrap_index() const; - double get_visible_rows_offset() const; - double get_v_scroll_offset() const; - - 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; - int get_char_pos_for(int p_px, String p_str) const; - int get_column_x_offset(int p_char, String p_str) const; - - void adjust_viewport_to_cursor(); - double get_scroll_line_diff() const; - void _scroll_moved(double); - void _update_scrollbars(); - void _v_scroll_input(); + bool selecting_enabled = true; + + Color font_selected_color = Color(1, 1, 1); + Color selection_color = Color(1, 1, 1); + bool override_selected_font_color = false; + + bool dragging_selection = false; + + Timer *click_select_held; + uint64_t last_dblclk = 0; + Vector2 last_dblclk_pos; void _click_selection_held(); void _update_selection_mode_pointer(); void _update_selection_mode_word(); void _update_selection_mode_line(); - 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(); + /* 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); + + 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 _update_scrollbars(); + int _get_control_height() const; + + void _v_scroll_input(); + void _scroll_moved(double p_to_val); + + double _get_visible_lines_offset() const; + double _get_v_scroll_offset() const; + + void _scroll_up(real_t p_delta); + void _scroll_down(real_t p_delta); + 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; - void _reset_caret_blink_timer(); - void _toggle_draw_caret(); + int minimap_width = 80; + Point2 minimap_char_size = Point2(1, 2); + int minimap_line_spacing = 1; - void _update_caches(); - void _cursor_changed_emit(); - void _text_changed_emit(); + // minimap scroll + bool minimap_clicked = false; + bool dragging_minimap = false; + bool can_drag_minimap = false; - void _push_current_op(); + double minimap_scroll_ratio = 0.0; + double minimap_scroll_click_pos = 0.0; - /* super internal api, undo/redo builds on it */ + void _update_minimap_click(); + void _update_minimap_drag(); - 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); + /* Gutters. */ + Vector<GutterInfo> gutters; + int gutters_width = 0; + int gutter_padding = 0; - int _get_column_pos_of_word(const String &p_key, const String &p_search, uint32_t p_search_flags, int p_from_column); + void _update_gutter_width(); - Dictionary _search_bind(const String &p_key, uint32_t p_search_flags, int p_from_line, int p_from_column) const; + /* Syntax highlighting. */ + Ref<SyntaxHighlighter> syntax_highlighter; + Map<int, Dictionary> syntax_highlighting_cache; - PopupMenu *menu; + Dictionary _get_line_syntax_highlighting(int p_line); - void _clear(); - void _cancel_completion(); - void _cancel_code_hint(); - void _confirm_completion(); - void _update_completion_candidates(); + /* Visual. */ + Ref<StyleBox> style_normal; + Ref<StyleBox> style_focus; + Ref<StyleBox> style_readonly; - int _calculate_spaces_till_next_left_indent(int column); - int _calculate_spaces_till_next_right_indent(int column); + Ref<Texture2D> tab_icon; + Ref<Texture2D> space_icon; -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; - 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_color_selected; - Color font_color_readonly; - Color selection_color; - Color mark_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 row_height; - int line_spacing; - int minimap_width; - Cache() { - row_height = 0; - line_spacing = 0; - minimap_width = 0; - } - } cache; + Ref<Font> font; + int font_size = 16; + Color font_color = Color(1, 1, 1); + Color font_readonly_color = Color(1, 1, 1); - virtual String get_tooltip(const Point2 &p_pos) const override; + int outline_size = 0; + Color outline_color = Color(1, 1, 1); + + int line_spacing = 1; + + 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); + virtual void gui_input(const Ref<InputEvent> &p_gui_input) override; static void _bind_methods(); -public: - /* Syntax Highlighting. */ - Ref<SyntaxHighlighter> get_syntax_highlighter(); - void set_syntax_highlighter(Ref<SyntaxHighlighter> p_syntax_highlighter); + bool _set(const StringName &p_name, const Variant &p_value); + bool _get(const StringName &p_name, Variant &r_ret) const; + void _get_property_list(List<PropertyInfo> *p_list) const; - /* Gutters. */ - void add_gutter(int p_at = -1); - void remove_gutter(int p_gutter); - int get_gutter_count() const; + /* Internal API for CodeEdit, pending public API. */ + // brace matching + bool highlight_matching_braces_enabled = false; + Color brace_mismatch_color; - void set_gutter_name(int p_gutter, const String &p_name); - String get_gutter_name(int p_gutter) const; + // Line hiding. + Color code_folding_color = Color(1, 1, 1); + Ref<Texture2D> folded_eol_icon; - void set_gutter_type(int p_gutter, GutterType p_type); - GutterType get_gutter_type(int p_gutter) const; + bool hiding_enabled = false; - void set_gutter_width(int p_gutter, int p_width); - int get_gutter_width(int p_gutter) const; + void _set_hiding_enabled(bool p_enabled); + bool _is_hiding_enabled() const; - void set_gutter_draw(int p_gutter, bool p_draw); - bool is_gutter_drawn(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_gutter_clickable(int p_gutter, bool p_clickable); - bool is_gutter_clickable(int p_gutter) const; + void _unhide_all_lines(); - void set_gutter_overwritable(int p_gutter, bool p_overwritable); - bool is_gutter_overwritable(int p_gutter) const; + // Symbol lookup. + String lookup_symbol_word; + void _set_symbol_lookup_word(const String &p_symbol); - void set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback); + /* Text manipulation */ - // 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; + // Overridable actions + virtual void _handle_unicode_input_internal(const uint32_t p_unicode); + virtual void _backspace_internal(); - 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; + virtual void _cut_internal(); + virtual void _copy_internal(); + virtual void _paste_internal(); - 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; + GDVIRTUAL1(_handle_unicode_input, int) + GDVIRTUAL0(_backspace) + GDVIRTUAL0(_cut) + GDVIRTUAL0(_copy) + GDVIRTUAL0(_paste) - 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); +public: + /* General overrides. */ + 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 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; + /* Text */ + // Text properties. + bool has_ime_text() const; - enum MenuItems { - MENU_CUT, - MENU_COPY, - MENU_PASTE, - MENU_CLEAR, - MENU_SELECT_ALL, - MENU_UNDO, - MENU_REDO, - MENU_MAX + void set_editable(const bool p_editable); + bool is_editable() const; - }; + void set_text_direction(TextDirection p_text_direction); + TextDirection get_text_direction() const; - enum SearchFlags { - SEARCH_MATCH_CASE = 1, - SEARCH_WHOLE_WORDS = 2, - SEARCH_BACKWARDS = 4 - }; + void set_opentype_feature(const String &p_name, int p_value); + int get_opentype_feature(const String &p_name) const; + void clear_opentype_features(); - virtual CursorShape get_cursor_shape(const Point2 &p_pos = Point2i()) const override; + void set_language(const String &p_language); + String get_language() const; - 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 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 delete_char(); - //void delete_line(); + void set_tab_size(const int p_size); + int get_tab_size() const; - void begin_complex_operation(); - void end_complex_operation(); + // 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; - bool is_insert_text_operation(); + // Text manipulation + void clear(); - 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_text(const String &p_text); + String get_text() const; int get_line_count() const; - void set_line_as_marked(int p_line, bool p_marked); - - 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; - 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_left(); - void indent_right(); + 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; + int get_indent_level(int p_line) const; - bool is_line_comment(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 center_viewport_to_cursor(); - - 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 cursor_get_column() const; - int cursor_get_line() const; - Vector2i _get_cursor_pixel_pos(); - - bool cursor_get_blink_enabled() const; - void cursor_set_blink_enabled(const bool p_enabled); - - float cursor_get_blink_speed() const; - void cursor_set_blink_speed(const float p_speed); - - void cursor_set_block_mode(const bool p_enable); - bool cursor_is_block_mode() const; - - void set_right_click_moves_caret(bool p_enable); - bool is_right_click_moving_caret() const; + int get_first_non_whitespace_column(int p_line) const; - 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; + void swap_lines(int p_from_line, int p_to_line); - void set_readonly(bool p_readonly); - bool is_readonly() const; + void insert_line_at(int p_at, const String &p_text); + void insert_text_at_caret(const String &p_text); - void set_max_chars(int p_max_chars); - int get_max_chars() const; + void remove_text(int p_from_line, int p_from_column, int p_to_line, int p_to_column); - void set_wrap_enabled(bool p_wrap_enabled); - bool is_wrap_enabled() const; + 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; - void clear(); + // Overridable actions + void handle_unicode_input(const uint32_t p_unicode); + void backspace(); void cut(); void copy(); void paste(); - void select_all(); - 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); + // Context menu. + PopupMenu *get_menu() const; + bool is_menu_visible() const; + void menu_option(int p_option); + + /* Versioning */ + void begin_complex_operation(); + void end_complex_operation(); + + bool has_undo() const; + bool has_redo() const; + void undo(); + void redo(); + void clear_undo_history(); + + bool is_insert_text_operation() const; + + void tag_saved_version(); + + uint32_t get_version() const; + uint32_t get_saved_version() const; + + /* Search */ 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); - void set_highlight_all_occurrences(const bool p_enabled); - bool is_highlight_all_occurrences_enabled() const; - bool is_selection_active() const; + 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 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); + + bool has_selection() const; + + String get_selected_text() const; + + int get_selection_line() const; + int get_selection_column() 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_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; - 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); + // 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 4187d77083..8659ea06a2 100644 --- a/scene/gui/texture_button.cpp +++ b/scene/gui/texture_button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -29,7 +29,9 @@ /*************************************************************************/ #include "texture_button.h" + #include "core/typedefs.h" + #include <stdlib.h> Size2 TextureButton::get_minimum_size() const { @@ -247,8 +249,8 @@ void TextureButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_disabled_texture", "texture"), &TextureButton::set_disabled_texture); ClassDB::bind_method(D_METHOD("set_focused_texture", "texture"), &TextureButton::set_focused_texture); ClassDB::bind_method(D_METHOD("set_click_mask", "mask"), &TextureButton::set_click_mask); - ClassDB::bind_method(D_METHOD("set_expand", "p_expand"), &TextureButton::set_expand); - ClassDB::bind_method(D_METHOD("set_stretch_mode", "p_mode"), &TextureButton::set_stretch_mode); + ClassDB::bind_method(D_METHOD("set_expand", "expand"), &TextureButton::set_expand); + ClassDB::bind_method(D_METHOD("set_stretch_mode", "mode"), &TextureButton::set_stretch_mode); ClassDB::bind_method(D_METHOD("set_flip_h", "enable"), &TextureButton::set_flip_h); ClassDB::bind_method(D_METHOD("is_flipped_h"), &TextureButton::is_flipped_h); ClassDB::bind_method(D_METHOD("set_flip_v", "enable"), &TextureButton::set_flip_v); @@ -293,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) { @@ -308,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 { @@ -375,13 +380,4 @@ bool TextureButton::is_flipped_v() const { return vflip; } -TextureButton::TextureButton() { - expand = false; - stretch_mode = STRETCH_SCALE; - hflip = false; - vflip = false; - - _texture_region = Rect2(); - _position_rect = Rect2(); - _tile = false; -} +TextureButton::TextureButton() {} diff --git a/scene/gui/texture_button.h b/scene/gui/texture_button.h index 6f7ee65ae4..8361f3c341 100644 --- a/scene/gui/texture_button.h +++ b/scene/gui/texture_button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -54,15 +54,15 @@ private: Ref<Texture2D> disabled; Ref<Texture2D> focused; Ref<BitMap> click_mask; - bool expand; - StretchMode stretch_mode; + bool expand = false; + StretchMode stretch_mode = STRETCH_SCALE; Rect2 _texture_region; Rect2 _position_rect; - bool _tile; + bool _tile = false; - bool hflip; - bool vflip; + bool hflip = false; + bool vflip = false; protected: virtual Size2 get_minimum_size() const override; diff --git a/scene/gui/texture_progress.cpp b/scene/gui/texture_progress_bar.cpp index e0d98d1c22..5e5dec3579 100644 --- a/scene/gui/texture_progress.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -1,12 +1,12 @@ /*************************************************************************/ -/* texture_progress.cpp */ +/* texture_progress_bar.cpp */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -28,21 +28,21 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#include "texture_progress.h" +#include "texture_progress_bar.h" #include "core/config/engine.h" -void TextureProgress::set_under_texture(const Ref<Texture2D> &p_texture) { +void TextureProgressBar::set_under_texture(const Ref<Texture2D> &p_texture) { under = p_texture; update(); minimum_size_changed(); } -Ref<Texture2D> TextureProgress::get_under_texture() const { +Ref<Texture2D> TextureProgressBar::get_under_texture() const { return under; } -void TextureProgress::set_over_texture(const Ref<Texture2D> &p_texture) { +void TextureProgressBar::set_over_texture(const Ref<Texture2D> &p_texture) { over = p_texture; update(); if (under.is_null()) { @@ -50,35 +50,35 @@ void TextureProgress::set_over_texture(const Ref<Texture2D> &p_texture) { } } -Ref<Texture2D> TextureProgress::get_over_texture() const { +Ref<Texture2D> TextureProgressBar::get_over_texture() const { return over; } -void TextureProgress::set_stretch_margin(Margin p_margin, int p_size) { - ERR_FAIL_INDEX((int)p_margin, 4); - stretch_margin[p_margin] = p_size; +void TextureProgressBar::set_stretch_margin(Side p_side, int p_size) { + ERR_FAIL_INDEX((int)p_side, 4); + stretch_margin[p_side] = p_size; update(); minimum_size_changed(); } -int TextureProgress::get_stretch_margin(Margin p_margin) const { - ERR_FAIL_INDEX_V((int)p_margin, 4, 0); - return stretch_margin[p_margin]; +int TextureProgressBar::get_stretch_margin(Side p_side) const { + ERR_FAIL_INDEX_V((int)p_side, 4, 0); + return stretch_margin[p_side]; } -void TextureProgress::set_nine_patch_stretch(bool p_stretch) { +void TextureProgressBar::set_nine_patch_stretch(bool p_stretch) { nine_patch_stretch = p_stretch; update(); minimum_size_changed(); } -bool TextureProgress::get_nine_patch_stretch() const { +bool TextureProgressBar::get_nine_patch_stretch() const { return nine_patch_stretch; } -Size2 TextureProgress::get_minimum_size() const { +Size2 TextureProgressBar::get_minimum_size() const { if (nine_patch_stretch) { - return Size2(stretch_margin[MARGIN_LEFT] + stretch_margin[MARGIN_RIGHT], stretch_margin[MARGIN_TOP] + stretch_margin[MARGIN_BOTTOM]); + return Size2(stretch_margin[SIDE_LEFT] + stretch_margin[SIDE_RIGHT], stretch_margin[SIDE_TOP] + stretch_margin[SIDE_BOTTOM]); } else if (under.is_valid()) { return under->get_size(); } else if (over.is_valid()) { @@ -90,44 +90,44 @@ Size2 TextureProgress::get_minimum_size() const { return Size2(1, 1); } -void TextureProgress::set_progress_texture(const Ref<Texture2D> &p_texture) { +void TextureProgressBar::set_progress_texture(const Ref<Texture2D> &p_texture) { progress = p_texture; update(); minimum_size_changed(); } -Ref<Texture2D> TextureProgress::get_progress_texture() const { +Ref<Texture2D> TextureProgressBar::get_progress_texture() const { return progress; } -void TextureProgress::set_tint_under(const Color &p_tint) { +void TextureProgressBar::set_tint_under(const Color &p_tint) { tint_under = p_tint; update(); } -Color TextureProgress::get_tint_under() const { +Color TextureProgressBar::get_tint_under() const { return tint_under; } -void TextureProgress::set_tint_progress(const Color &p_tint) { +void TextureProgressBar::set_tint_progress(const Color &p_tint) { tint_progress = p_tint; update(); } -Color TextureProgress::get_tint_progress() const { +Color TextureProgressBar::get_tint_progress() const { return tint_progress; } -void TextureProgress::set_tint_over(const Color &p_tint) { +void TextureProgressBar::set_tint_over(const Color &p_tint) { tint_over = p_tint; update(); } -Color TextureProgress::get_tint_over() const { +Color TextureProgressBar::get_tint_over() const { return tint_over; } -Point2 TextureProgress::unit_val_to_uv(float val) { +Point2 TextureProgressBar::unit_val_to_uv(float val) { if (progress.is_null()) { return Point2(); } @@ -145,9 +145,9 @@ Point2 TextureProgress::unit_val_to_uv(float val) { float angle = (val * Math_TAU) - Math_PI * 0.5; Point2 dir = Vector2(Math::cos(angle), Math::sin(angle)); float t1 = 1.0; - float cp = 0; - float cq = 0; - float cr = 0; + float cp = 0.0; + float cq = 0.0; + float cr = 0.0; float edgeLeft = 0.0; float edgeRight = 1.0; float edgeBottom = 0.0; @@ -191,7 +191,7 @@ Point2 TextureProgress::unit_val_to_uv(float val) { return (p + t1 * dir); } -Point2 TextureProgress::get_relative_center() { +Point2 TextureProgressBar::get_relative_center() { if (progress.is_null()) { return Point2(); } @@ -204,10 +204,10 @@ Point2 TextureProgress::get_relative_center() { return p; } -void TextureProgress::draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, FillMode p_mode, double p_ratio, const Color &p_modulate) { +void TextureProgressBar::draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, FillMode p_mode, double p_ratio, const Color &p_modulate) { Vector2 texture_size = p_texture->get_size(); - Vector2 topleft = Vector2(stretch_margin[MARGIN_LEFT], stretch_margin[MARGIN_TOP]); - Vector2 bottomright = Vector2(stretch_margin[MARGIN_RIGHT], stretch_margin[MARGIN_BOTTOM]); + Vector2 topleft = Vector2(stretch_margin[SIDE_LEFT], stretch_margin[SIDE_TOP]); + Vector2 bottomright = Vector2(stretch_margin[SIDE_RIGHT], stretch_margin[SIDE_BOTTOM]); Rect2 src_rect = Rect2(Point2(), texture_size); Rect2 dst_rect = Rect2(Point2(), get_size()); @@ -221,43 +221,87 @@ void TextureProgress::draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, 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); + + // 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 (mode) { + switch (p_mode) { case FILL_LEFT_TO_RIGHT: { src_rect.size.x = width_texture; dst_rect.size.x = width_filled; @@ -287,16 +331,32 @@ void TextureProgress::draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, 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; } } @@ -306,23 +366,38 @@ void TextureProgress::draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, RS::get_singleton()->canvas_item_add_nine_patch(ci, dst_rect, src_rect, p_texture->get_rid(), topleft, bottomright, RS::NINE_PATCH_STRETCH, RS::NINE_PATCH_STRETCH, true, p_modulate); } -void TextureProgress::_notification(int p_what) { +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(); @@ -353,7 +428,7 @@ void TextureProgress::_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); + draw_texture_rect(progress, region, false, tint_progress); } else if (val != 0) { Array pts; float direction = mode == FILL_COUNTER_CLOCKWISE ? -1 : 1; @@ -416,29 +491,46 @@ void TextureProgress::_notification(int p_what) { 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); } 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); } } 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; } } -void TextureProgress::set_fill_mode(int p_fill) { - ERR_FAIL_INDEX(p_fill, 9); +void TextureProgressBar::set_fill_mode(int p_fill) { + ERR_FAIL_INDEX(p_fill, FILL_MODE_MAX); mode = (FillMode)p_fill; update(); } -int TextureProgress::get_fill_mode() { +int TextureProgressBar::get_fill_mode() { return mode; } -void TextureProgress::set_radial_initial_angle(float p_angle) { +void TextureProgressBar::set_radial_initial_angle(float p_angle) { while (p_angle > 360) { p_angle -= 360; } @@ -449,70 +541,70 @@ void TextureProgress::set_radial_initial_angle(float p_angle) { update(); } -float TextureProgress::get_radial_initial_angle() { +float TextureProgressBar::get_radial_initial_angle() { return rad_init_angle; } -void TextureProgress::set_fill_degrees(float p_angle) { +void TextureProgressBar::set_fill_degrees(float p_angle) { rad_max_degrees = CLAMP(p_angle, 0, 360); update(); } -float TextureProgress::get_fill_degrees() { +float TextureProgressBar::get_fill_degrees() { return rad_max_degrees; } -void TextureProgress::set_radial_center_offset(const Point2 &p_off) { +void TextureProgressBar::set_radial_center_offset(const Point2 &p_off) { rad_center_off = p_off; update(); } -Point2 TextureProgress::get_radial_center_offset() { +Point2 TextureProgressBar::get_radial_center_offset() { return rad_center_off; } -void TextureProgress::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_under_texture", "tex"), &TextureProgress::set_under_texture); - ClassDB::bind_method(D_METHOD("get_under_texture"), &TextureProgress::get_under_texture); +void TextureProgressBar::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_under_texture", "tex"), &TextureProgressBar::set_under_texture); + ClassDB::bind_method(D_METHOD("get_under_texture"), &TextureProgressBar::get_under_texture); - ClassDB::bind_method(D_METHOD("set_progress_texture", "tex"), &TextureProgress::set_progress_texture); - ClassDB::bind_method(D_METHOD("get_progress_texture"), &TextureProgress::get_progress_texture); + ClassDB::bind_method(D_METHOD("set_progress_texture", "tex"), &TextureProgressBar::set_progress_texture); + ClassDB::bind_method(D_METHOD("get_progress_texture"), &TextureProgressBar::get_progress_texture); - ClassDB::bind_method(D_METHOD("set_over_texture", "tex"), &TextureProgress::set_over_texture); - ClassDB::bind_method(D_METHOD("get_over_texture"), &TextureProgress::get_over_texture); + ClassDB::bind_method(D_METHOD("set_over_texture", "tex"), &TextureProgressBar::set_over_texture); + ClassDB::bind_method(D_METHOD("get_over_texture"), &TextureProgressBar::get_over_texture); - ClassDB::bind_method(D_METHOD("set_fill_mode", "mode"), &TextureProgress::set_fill_mode); - ClassDB::bind_method(D_METHOD("get_fill_mode"), &TextureProgress::get_fill_mode); + ClassDB::bind_method(D_METHOD("set_fill_mode", "mode"), &TextureProgressBar::set_fill_mode); + ClassDB::bind_method(D_METHOD("get_fill_mode"), &TextureProgressBar::get_fill_mode); - ClassDB::bind_method(D_METHOD("set_tint_under", "tint"), &TextureProgress::set_tint_under); - ClassDB::bind_method(D_METHOD("get_tint_under"), &TextureProgress::get_tint_under); + ClassDB::bind_method(D_METHOD("set_tint_under", "tint"), &TextureProgressBar::set_tint_under); + ClassDB::bind_method(D_METHOD("get_tint_under"), &TextureProgressBar::get_tint_under); - ClassDB::bind_method(D_METHOD("set_tint_progress", "tint"), &TextureProgress::set_tint_progress); - ClassDB::bind_method(D_METHOD("get_tint_progress"), &TextureProgress::get_tint_progress); + ClassDB::bind_method(D_METHOD("set_tint_progress", "tint"), &TextureProgressBar::set_tint_progress); + ClassDB::bind_method(D_METHOD("get_tint_progress"), &TextureProgressBar::get_tint_progress); - ClassDB::bind_method(D_METHOD("set_tint_over", "tint"), &TextureProgress::set_tint_over); - ClassDB::bind_method(D_METHOD("get_tint_over"), &TextureProgress::get_tint_over); + 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_radial_initial_angle", "mode"), &TextureProgress::set_radial_initial_angle); - ClassDB::bind_method(D_METHOD("get_radial_initial_angle"), &TextureProgress::get_radial_initial_angle); + 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); - ClassDB::bind_method(D_METHOD("set_radial_center_offset", "mode"), &TextureProgress::set_radial_center_offset); - ClassDB::bind_method(D_METHOD("get_radial_center_offset"), &TextureProgress::get_radial_center_offset); + ClassDB::bind_method(D_METHOD("set_radial_center_offset", "mode"), &TextureProgressBar::set_radial_center_offset); + ClassDB::bind_method(D_METHOD("get_radial_center_offset"), &TextureProgressBar::get_radial_center_offset); - ClassDB::bind_method(D_METHOD("set_fill_degrees", "mode"), &TextureProgress::set_fill_degrees); - ClassDB::bind_method(D_METHOD("get_fill_degrees"), &TextureProgress::get_fill_degrees); + ClassDB::bind_method(D_METHOD("set_fill_degrees", "mode"), &TextureProgressBar::set_fill_degrees); + ClassDB::bind_method(D_METHOD("get_fill_degrees"), &TextureProgressBar::get_fill_degrees); - ClassDB::bind_method(D_METHOD("set_stretch_margin", "margin", "value"), &TextureProgress::set_stretch_margin); - ClassDB::bind_method(D_METHOD("get_stretch_margin", "margin"), &TextureProgress::get_stretch_margin); + ClassDB::bind_method(D_METHOD("set_stretch_margin", "margin", "value"), &TextureProgressBar::set_stretch_margin); + ClassDB::bind_method(D_METHOD("get_stretch_margin", "margin"), &TextureProgressBar::get_stretch_margin); - ClassDB::bind_method(D_METHOD("set_nine_patch_stretch", "stretch"), &TextureProgress::set_nine_patch_stretch); - ClassDB::bind_method(D_METHOD("get_nine_patch_stretch"), &TextureProgress::get_nine_patch_stretch); + ClassDB::bind_method(D_METHOD("set_nine_patch_stretch", "stretch"), &TextureProgressBar::set_nine_patch_stretch); + ClassDB::bind_method(D_METHOD("get_nine_patch_stretch"), &TextureProgressBar::get_nine_patch_stretch); ADD_GROUP("Textures", "texture_"); 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::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"); @@ -523,10 +615,10 @@ void TextureProgress::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "radial_center_offset"), "set_radial_center_offset", "get_radial_center_offset"); ADD_GROUP("Stretch", "stretch_"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "nine_patch_stretch"), "set_nine_patch_stretch", "get_nine_patch_stretch"); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_left", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", MARGIN_LEFT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_top", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", MARGIN_TOP); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_right", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", MARGIN_RIGHT); - ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_bottom", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", MARGIN_BOTTOM); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_left", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_top", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_right", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "stretch_margin_bottom", PROPERTY_HINT_RANGE, "0,16384,1"), "set_stretch_margin", "get_stretch_margin", SIDE_BOTTOM); BIND_ENUM_CONSTANT(FILL_LEFT_TO_RIGHT); BIND_ENUM_CONSTANT(FILL_RIGHT_TO_LEFT); @@ -539,18 +631,6 @@ void TextureProgress::_bind_methods() { BIND_ENUM_CONSTANT(FILL_CLOCKWISE_AND_COUNTER_CLOCKWISE); } -TextureProgress::TextureProgress() { - mode = FILL_LEFT_TO_RIGHT; - rad_init_angle = 0; - rad_center_off = Point2(); - rad_max_degrees = 360; +TextureProgressBar::TextureProgressBar() { set_mouse_filter(MOUSE_FILTER_PASS); - - nine_patch_stretch = false; - stretch_margin[MARGIN_LEFT] = 0; - stretch_margin[MARGIN_RIGHT] = 0; - stretch_margin[MARGIN_BOTTOM] = 0; - stretch_margin[MARGIN_TOP] = 0; - - tint_under = tint_progress = tint_over = Color(1, 1, 1); } diff --git a/scene/gui/texture_progress.h b/scene/gui/texture_progress_bar.h index 5e29fca21f..d147c43a26 100644 --- a/scene/gui/texture_progress.h +++ b/scene/gui/texture_progress_bar.h @@ -1,12 +1,12 @@ /*************************************************************************/ -/* texture_progress.h */ +/* texture_progress_bar.h */ /*************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -28,13 +28,13 @@ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -#ifndef TEXTURE_PROGRESS_H -#define TEXTURE_PROGRESS_H +#ifndef TEXTURE_PROGRESS_BAR_H +#define TEXTURE_PROGRESS_BAR_H #include "scene/gui/range.h" -class TextureProgress : public Range { - GDCLASS(TextureProgress, Range); +class TextureProgressBar : public Range { + GDCLASS(TextureProgressBar, Range); Ref<Texture2D> under; Ref<Texture2D> progress; @@ -54,7 +54,8 @@ 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); @@ -78,8 +79,8 @@ public: void set_over_texture(const Ref<Texture2D> &p_texture); Ref<Texture2D> get_over_texture() const; - void set_stretch_margin(Margin p_margin, int p_size); - int get_stretch_margin(Margin p_margin) const; + void set_stretch_margin(Side p_side, int p_size); + int get_stretch_margin(Side p_side) const; void set_nine_patch_stretch(bool p_stretch); bool get_nine_patch_stretch() const; @@ -95,22 +96,24 @@ public: Size2 get_minimum_size() const override; - TextureProgress(); + TextureProgressBar(); private: - FillMode mode; - float rad_init_angle; - float rad_max_degrees; + FillMode mode = FILL_LEFT_TO_RIGHT; + float rad_init_angle = 0.0; + float rad_max_degrees = 360.0; Point2 rad_center_off; - bool nine_patch_stretch; - int stretch_margin[4]; - Color tint_under, tint_progress, tint_over; + bool nine_patch_stretch = false; + int stretch_margin[4] = {}; + Color tint_under = Color(1, 1, 1); + Color tint_progress = Color(1, 1, 1); + Color tint_over = Color(1, 1, 1); Point2 unit_val_to_uv(float val); Point2 get_relative_center(); void draw_nine_patch_stretched(const Ref<Texture2D> &p_texture, FillMode p_mode, double p_ratio, const Color &p_modulate); }; -VARIANT_ENUM_CAST(TextureProgress::FillMode); +VARIANT_ENUM_CAST(TextureProgressBar::FillMode); -#endif // TEXTURE_PROGRESS_H +#endif // TEXTURE_PROGRESS_BAR_H diff --git a/scene/gui/texture_rect.cpp b/scene/gui/texture_rect.cpp index 58e7249284..1cba88e06f 100644 --- a/scene/gui/texture_rect.cpp +++ b/scene/gui/texture_rect.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -217,11 +217,7 @@ bool TextureRect::is_flipped_v() const { } TextureRect::TextureRect() { - expand = false; - hflip = false; - vflip = false; set_mouse_filter(MOUSE_FILTER_PASS); - stretch_mode = STRETCH_SCALE_ON_EXPAND; } TextureRect::~TextureRect() { diff --git a/scene/gui/texture_rect.h b/scene/gui/texture_rect.h index e39545f679..0f93d5732f 100644 --- a/scene/gui/texture_rect.h +++ b/scene/gui/texture_rect.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -49,11 +49,11 @@ public: }; private: - bool expand; - bool hflip; - bool vflip; + bool expand = false; + bool hflip = false; + bool vflip = false; Ref<Texture2D> texture; - StretchMode stretch_mode; + StretchMode stretch_mode = STRETCH_SCALE_ON_EXPAND; void _texture_changed(); diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index bcb375d786..3e46e2749b 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,6 +36,7 @@ #include "core/os/keyboard.h" #include "core/os/os.h" #include "core/string/print_string.h" +#include "core/string/translation.h" #include "scene/main/window.h" #include "box_container.h" @@ -46,36 +47,6 @@ #include <limits.h> -void TreeItem::move_to_top() { - if (!parent || parent->children == this) { - return; //already on top - } - TreeItem *prev = get_prev(); - prev->next = next; - next = parent->children; - parent->children = this; -} - -void TreeItem::move_to_bottom() { - if (!parent || !next) { - return; - } - - TreeItem *prev = get_prev(); - TreeItem *last = next; - while (last->next) { - last = last->next; - } - - if (prev) { - prev->next = next; - } else { - parent->children = next; - } - last->next = this; - next = nullptr; -} - Size2 TreeItem::Cell::get_icon_size() const { if (icon.is_null()) { return Size2(); @@ -117,6 +88,59 @@ void TreeItem::_cell_deselected(int p_cell) { tree->item_deselected(p_cell, this); } +void TreeItem::_change_tree(Tree *p_tree) { + if (p_tree == tree) { + return; + } + + TreeItem *c = first_child; + while (c) { + c->_change_tree(p_tree); + c = c->next; + } + + if (tree) { + if (tree->root == this) { + tree->root = nullptr; + } + + if (tree->popup_edited_item == this) { + tree->popup_edited_item = nullptr; + tree->pressing_for_editor = false; + } + + if (tree->cache.hover_item == this) { + tree->cache.hover_item = nullptr; + } + + if (tree->selected_item == this) { + tree->selected_item = nullptr; + } + + if (tree->drop_mode_over == this) { + tree->drop_mode_over = 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; + } + + tree->update(); + } + + tree = p_tree; + + if (tree) { + tree->update(); + cells.resize(tree->columns.size()); + } +} + /* cell mode */ void TreeItem::set_cell_mode(int p_column, TreeCellMode p_mode) { ERR_FAIL_INDEX(p_column, cells.size()); @@ -129,6 +153,7 @@ void TreeItem::set_cell_mode(int p_column, TreeCellMode p_mode) { c.checked = false; c.icon = Ref<Texture2D>(); c.text = ""; + c.dirty = true; c.icon_max_w = 0; _changed_notify(p_column); } @@ -142,6 +167,18 @@ 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); +} + +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); } @@ -150,9 +187,15 @@ 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; + cells.write[p_column].dirty = true; if (cells[p_column].mode == TreeItem::CELL_MODE_RANGE) { Vector<String> strings = p_text.split(","); @@ -160,7 +203,7 @@ void TreeItem::set_text(int p_column, String p_text) { cells.write[p_column].max = INT_MIN; for (int i = 0; i < strings.size(); i++) { int value = i; - if (!strings[i].get_slicec(':', 1).empty()) { + if (!strings[i].get_slicec(':', 1).is_empty()) { value = strings[i].get_slicec(':', 1).to_int(); } cells.write[p_column].min = MIN(cells[p_column].min, value); @@ -176,6 +219,87 @@ String TreeItem::get_text(int p_column) const { return cells[p_column].text; } +void TreeItem::set_text_direction(int p_column, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_column, cells.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (cells[p_column].text_direction != p_text_direction) { + cells.write[p_column].text_direction = p_text_direction; + cells.write[p_column].dirty = true; + _changed_notify(p_column); + } +} + +Control::TextDirection TreeItem::get_text_direction(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Control::TEXT_DIRECTION_INHERITED); + return cells[p_column].text_direction; +} + +void TreeItem::clear_opentype_features(int p_column) { + ERR_FAIL_INDEX(p_column, cells.size()); + cells.write[p_column].opentype_features.clear(); + cells.write[p_column].dirty = true; + _changed_notify(p_column); +} + +void TreeItem::set_opentype_feature(int p_column, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_column, cells.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!cells[p_column].opentype_features.has(tag) || (int)cells[p_column].opentype_features[tag] != p_value) { + cells.write[p_column].opentype_features[tag] = p_value; + cells.write[p_column].dirty = true; + _changed_notify(p_column); + } +} + +int TreeItem::get_opentype_feature(int p_column, const String &p_name) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!cells[p_column].opentype_features.has(tag)) { + return -1; + } + return cells[p_column].opentype_features[tag]; +} + +void TreeItem::set_structured_text_bidi_override(int p_column, Control::StructuredTextParser p_parser) { + ERR_FAIL_INDEX(p_column, cells.size()); + if (cells[p_column].st_parser != p_parser) { + cells.write[p_column].st_parser = p_parser; + cells.write[p_column].dirty = true; + _changed_notify(p_column); + } +} + +Control::StructuredTextParser TreeItem::get_structured_text_bidi_override(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Control::STRUCTURED_TEXT_NONE); + return cells[p_column].st_parser; +} + +void TreeItem::set_structured_text_bidi_override_options(int p_column, Array p_args) { + ERR_FAIL_INDEX(p_column, cells.size()); + cells.write[p_column].st_args = p_args; + cells.write[p_column].dirty = true; + _changed_notify(p_column); +} + +Array TreeItem::get_structured_text_bidi_override_options(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), Array()); + return cells[p_column].st_args; +} + +void TreeItem::set_language(int p_column, const String &p_language) { + ERR_FAIL_INDEX(p_column, cells.size()); + if (cells[p_column].language != p_language) { + cells.write[p_column].language = p_language; + cells.write[p_column].dirty = true; + _changed_notify(p_column); + } +} + +String TreeItem::get_language(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), ""); + return cells[p_column].language; +} + void TreeItem::set_suffix(int p_column, String p_suffix) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].suffix = p_suffix; @@ -236,7 +360,7 @@ int TreeItem::get_icon_max_width(int p_column) const { void TreeItem::set_range(int p_column, double p_value) { ERR_FAIL_INDEX(p_column, cells.size()); if (cells[p_column].step > 0) { - p_value = Math::stepify(p_value, cells[p_column].step); + p_value = Math::snapped(p_value, cells[p_column].step); } if (p_value < cells[p_column].min) { p_value = cells[p_column].min; @@ -246,6 +370,7 @@ void TreeItem::set_range(int p_column, double p_value) { } cells.write[p_column].val = p_value; + cells.write[p_column].dirty = true; _changed_notify(p_column); } @@ -308,7 +433,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); } @@ -318,13 +443,21 @@ 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() { return collapsed; } +void TreeItem::uncollapse_tree() { + TreeItem *t = this; + while (t) { + t->set_collapsed(false); + t = t->parent; + } +} + void TreeItem::set_custom_minimum_height(int p_height) { custom_min_height = p_height; _changed_notify(); @@ -334,29 +467,84 @@ int TreeItem::get_custom_minimum_height() const { return custom_min_height; } -TreeItem *TreeItem::get_next() { +/* Item manipulation */ + +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; + TreeItem *c = first_child; + int idx = 0; + + while (c) { + if (idx++ == p_idx) { + c->prev = ti; + ti->next = c; + break; + } + l_prev = c; + c = c->next; + } + + if (l_prev) { + l_prev->next = ti; + ti->prev = l_prev; + if (!children_cache.is_empty()) { + if (ti->next) { + children_cache.insert(p_idx, ti); + } else { + children_cache.append(ti); + } + } + } else { + first_child = ti; + if (!children_cache.is_empty()) { + children_cache.insert(0, ti); + } + } + + ti->parent = this; + + return ti; +} + +Tree *TreeItem::get_tree() const { + return tree; +} + +TreeItem *TreeItem::get_next() const { return next; } TreeItem *TreeItem::get_prev() { - if (!parent || parent->children == this) { - return nullptr; + if (prev) { + return prev; } - TreeItem *prev = parent->children; - while (prev && prev->next != this) { - prev = prev->next; + if (!parent || parent->first_child == this) { + return nullptr; + } + // This is an edge case + TreeItem *l_prev = parent->first_child; + while (l_prev && l_prev->next != this) { + l_prev = l_prev->next; } + prev = l_prev; + return prev; } -TreeItem *TreeItem::get_parent() { +TreeItem *TreeItem::get_parent() const { return parent; } -TreeItem *TreeItem::get_children() { - return children; +TreeItem *TreeItem::get_first_child() const { + return first_child; } TreeItem *TreeItem::get_prev_visible(bool p_wrap) { @@ -382,10 +570,10 @@ TreeItem *TreeItem::get_prev_visible(bool p_wrap) { } } else { current = prev; - while (!current->collapsed && current->children) { + while (!current->collapsed && current->first_child) { //go to the very end - current = current->children; + current = current->first_child; while (current->next) { current = current->next; } @@ -398,8 +586,8 @@ TreeItem *TreeItem::get_prev_visible(bool p_wrap) { TreeItem *TreeItem::get_next_visible(bool p_wrap) { TreeItem *current = this; - if (!current->collapsed && current->children) { - current = current->children; + if (!current->collapsed && current->first_child) { + current = current->first_child; } else if (current->next) { current = current->next; @@ -422,24 +610,128 @@ TreeItem *TreeItem::get_next_visible(bool p_wrap) { return current; } -void TreeItem::remove_child(TreeItem *p_item) { +TreeItem *TreeItem::get_child(int p_idx) { + _create_children_cache(); + ERR_FAIL_INDEX_V(p_idx, children_cache.size(), nullptr); + return children_cache.get(p_idx); +} + +int TreeItem::get_child_count() { + _create_children_cache(); + return children_cache.size(); +} + +Array TreeItem::get_children() { + int size = get_child_count(); + Array arr; + arr.resize(size); + for (int i = 0; i < size; i++) { + arr[i] = children_cache[i]; + } + + return arr; +} + +int TreeItem::get_index() { + int idx = 0; + TreeItem *c = this; + + while (c) { + c = c->get_prev(); + idx++; + } + return idx - 1; +} + +void TreeItem::move_before(TreeItem *p_item) { ERR_FAIL_NULL(p_item); - TreeItem **c = &children; + ERR_FAIL_COND(is_root); + ERR_FAIL_COND(!p_item->parent); + + if (p_item == this) { + return; + } - while (*c) { - if ((*c) == p_item) { - TreeItem *aux = *c; + TreeItem *p = p_item->parent; + while (p) { + ERR_FAIL_COND_MSG(p == this, "Can't move to a descendant"); + p = p->parent; + } - *c = (*c)->next; + Tree *old_tree = tree; + _unlink_from_tree(); + _change_tree(p_item->tree); - aux->parent = nullptr; - return; - } + parent = p_item->parent; - c = &(*c)->next; + TreeItem *item_prev = p_item->get_prev(); + if (item_prev) { + item_prev->next = this; + parent->children_cache.clear(); + } else { + parent->first_child = this; + parent->children_cache.insert(0, this); } - ERR_FAIL(); + prev = item_prev; + next = p_item; + p_item->prev = this; + + if (tree && old_tree == tree) { + tree->update(); + } +} + +void TreeItem::move_after(TreeItem *p_item) { + ERR_FAIL_NULL(p_item); + ERR_FAIL_COND(is_root); + ERR_FAIL_COND(!p_item->parent); + + if (p_item == this) { + return; + } + + TreeItem *p = p_item->parent; + while (p) { + ERR_FAIL_COND_MSG(p == this, "Can't move to a descendant"); + p = p->parent; + } + + Tree *old_tree = tree; + _unlink_from_tree(); + _change_tree(p_item->tree); + + if (p_item->next) { + p_item->next->prev = this; + } + parent = p_item->parent; + prev = p_item; + next = p_item->next; + p_item->next = this; + + if (next) { + parent->children_cache.clear(); + } else { + parent->children_cache.append(this); + } + + if (tree && old_tree == tree) { + tree->update(); + } +} + +void TreeItem::remove_child(TreeItem *p_item) { + ERR_FAIL_NULL(p_item); + ERR_FAIL_COND(p_item->parent != this); + + p_item->_unlink_from_tree(); + p_item->prev = nullptr; + p_item->next = nullptr; + p_item->parent = nullptr; + + if (tree) { + tree->update(); + } } void TreeItem::set_selectable(int p_column, bool p_selectable) { @@ -512,12 +804,6 @@ String TreeItem::get_button_tooltip(int p_column, int p_idx) const { return cells[p_column].buttons[p_idx].tooltip; } -int TreeItem::get_button_id(int p_column, int p_idx) const { - ERR_FAIL_INDEX_V(p_column, cells.size(), -1); - ERR_FAIL_INDEX_V(p_idx, cells[p_column].buttons.size(), -1); - return cells[p_column].buttons[p_idx].id; -} - void TreeItem::erase_button(int p_column, int p_idx) { ERR_FAIL_INDEX(p_column, cells.size()); ERR_FAIL_INDEX(p_idx, cells[p_column].buttons.size()); @@ -599,6 +885,26 @@ 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; +} + +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; +} + +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; @@ -673,6 +979,56 @@ 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()); + + 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; + } + + return 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; @@ -698,7 +1054,7 @@ void recursive_call_aux(TreeItem *p_item, const StringName &p_method, const Vari return; } p_item->call(p_method, p_args, p_argcount, r_error); - TreeItem *c = p_item->get_children(); + TreeItem *c = p_item->get_first_child(); while (c) { recursive_call_aux(c, p_method, p_args, p_argcount, r_error); c = c->get_next(); @@ -714,11 +1070,29 @@ 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); + ClassDB::bind_method(D_METHOD("set_text_direction", "column", "direction"), &TreeItem::set_text_direction); + ClassDB::bind_method(D_METHOD("get_text_direction", "column"), &TreeItem::get_text_direction); + + ClassDB::bind_method(D_METHOD("set_opentype_feature", "column", "tag", "value"), &TreeItem::set_opentype_feature); + ClassDB::bind_method(D_METHOD("get_opentype_feature", "column", "tag"), &TreeItem::get_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_opentype_features", "column"), &TreeItem::clear_opentype_features); + + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override", "column", "parser"), &TreeItem::set_structured_text_bidi_override); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override", "column"), &TreeItem::get_structured_text_bidi_override); + + ClassDB::bind_method(D_METHOD("set_structured_text_bidi_override_options", "column", "args"), &TreeItem::set_structured_text_bidi_override_options); + ClassDB::bind_method(D_METHOD("get_structured_text_bidi_override_options", "column"), &TreeItem::get_structured_text_bidi_override_options); + + ClassDB::bind_method(D_METHOD("set_language", "column", "language"), &TreeItem::set_language); + ClassDB::bind_method(D_METHOD("get_language", "column"), &TreeItem::get_language); + ClassDB::bind_method(D_METHOD("set_suffix", "column", "text"), &TreeItem::set_suffix); ClassDB::bind_method(D_METHOD("get_suffix", "column"), &TreeItem::get_suffix); @@ -747,19 +1121,11 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_collapsed", "enable"), &TreeItem::set_collapsed); ClassDB::bind_method(D_METHOD("is_collapsed"), &TreeItem::is_collapsed); + ClassDB::bind_method(D_METHOD("uncollapse_tree"), &TreeItem::uncollapse_tree); + ClassDB::bind_method(D_METHOD("set_custom_minimum_height", "height"), &TreeItem::set_custom_minimum_height); ClassDB::bind_method(D_METHOD("get_custom_minimum_height"), &TreeItem::get_custom_minimum_height); - ClassDB::bind_method(D_METHOD("get_next"), &TreeItem::get_next); - ClassDB::bind_method(D_METHOD("get_prev"), &TreeItem::get_prev); - ClassDB::bind_method(D_METHOD("get_parent"), &TreeItem::get_parent); - ClassDB::bind_method(D_METHOD("get_children"), &TreeItem::get_children); - - ClassDB::bind_method(D_METHOD("get_next_visible", "wrap"), &TreeItem::get_next_visible, DEFVAL(false)); - ClassDB::bind_method(D_METHOD("get_prev_visible", "wrap"), &TreeItem::get_prev_visible, DEFVAL(false)); - - ClassDB::bind_method(D_METHOD("remove_child", "child"), &TreeItem::_remove_child); - ClassDB::bind_method(D_METHOD("set_selectable", "column", "selectable"), &TreeItem::set_selectable); ClassDB::bind_method(D_METHOD("is_selectable", "column"), &TreeItem::is_selectable); @@ -771,8 +1137,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); @@ -790,19 +1162,38 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_button_disabled", "column", "button_idx", "disabled"), &TreeItem::set_button_disabled); ClassDB::bind_method(D_METHOD("is_button_disabled", "column", "button_idx"), &TreeItem::is_button_disabled); - ClassDB::bind_method(D_METHOD("set_expand_right", "column", "enable"), &TreeItem::set_expand_right); - ClassDB::bind_method(D_METHOD("get_expand_right", "column"), &TreeItem::get_expand_right); - ClassDB::bind_method(D_METHOD("set_tooltip", "column", "tooltip"), &TreeItem::set_tooltip); ClassDB::bind_method(D_METHOD("get_tooltip", "column"), &TreeItem::get_tooltip); ClassDB::bind_method(D_METHOD("set_text_align", "column", "text_align"), &TreeItem::set_text_align); ClassDB::bind_method(D_METHOD("get_text_align", "column"), &TreeItem::get_text_align); - ClassDB::bind_method(D_METHOD("move_to_top"), &TreeItem::move_to_top); - ClassDB::bind_method(D_METHOD("move_to_bottom"), &TreeItem::move_to_bottom); + + ClassDB::bind_method(D_METHOD("set_expand_right", "column", "enable"), &TreeItem::set_expand_right); + ClassDB::bind_method(D_METHOD("get_expand_right", "column"), &TreeItem::get_expand_right); ClassDB::bind_method(D_METHOD("set_disable_folding", "disable"), &TreeItem::set_disable_folding); ClassDB::bind_method(D_METHOD("is_folding_disabled"), &TreeItem::is_folding_disabled); + ClassDB::bind_method(D_METHOD("create_child", "idx"), &TreeItem::create_child, DEFVAL(-1)); + ClassDB::bind_method(D_METHOD("get_tree"), &TreeItem::get_tree); + + ClassDB::bind_method(D_METHOD("get_next"), &TreeItem::get_next); + ClassDB::bind_method(D_METHOD("get_prev"), &TreeItem::get_prev); + ClassDB::bind_method(D_METHOD("get_parent"), &TreeItem::get_parent); + ClassDB::bind_method(D_METHOD("get_first_child"), &TreeItem::get_first_child); + + ClassDB::bind_method(D_METHOD("get_next_visible", "wrap"), &TreeItem::get_next_visible, DEFVAL(false)); + ClassDB::bind_method(D_METHOD("get_prev_visible", "wrap"), &TreeItem::get_prev_visible, DEFVAL(false)); + + ClassDB::bind_method(D_METHOD("get_child", "idx"), &TreeItem::get_child); + ClassDB::bind_method(D_METHOD("get_child_count"), &TreeItem::get_child_count); + ClassDB::bind_method(D_METHOD("get_children"), &TreeItem::get_children); + ClassDB::bind_method(D_METHOD("get_index"), &TreeItem::get_index); + + ClassDB::bind_method(D_METHOD("move_before", "item"), &TreeItem::_move_before); + ClassDB::bind_method(D_METHOD("move_after", "item"), &TreeItem::_move_after); + + ClassDB::bind_method(D_METHOD("remove_child", "child"), &TreeItem::_remove_child); + { MethodInfo mi; mi.name = "call_recursive"; @@ -827,7 +1218,7 @@ void TreeItem::_bind_methods() { } void TreeItem::clear_children() { - TreeItem *c = children; + TreeItem *c = first_child; while (c) { TreeItem *aux = c; c = c->get_next(); @@ -835,56 +1226,18 @@ void TreeItem::clear_children() { memdelete(aux); } - children = nullptr; + first_child = nullptr; }; TreeItem::TreeItem(Tree *p_tree) { tree = p_tree; - collapsed = false; - disable_folding = false; - custom_min_height = 0; - - parent = nullptr; // parent item - next = nullptr; // next in list - children = nullptr; //child items } TreeItem::~TreeItem() { + _unlink_from_tree(); + prev = nullptr; clear_children(); - - if (parent) { - parent->remove_child(this); - } - - if (tree && 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 && tree->cache.hover_item == this) { - tree->cache.hover_item = nullptr; - } - - if (tree && tree->selected_item == this) { - tree->selected_item = nullptr; - } - - if (tree && tree->drop_mode_over == this) { - tree->drop_mode_over = nullptr; - } - - if (tree && tree->single_select_defer == this) { - tree->single_select_defer = nullptr; - } - - if (tree && tree->edited_item == this) { - tree->edited_item = nullptr; - tree->pressing_for_editor = false; - } + _change_tree(nullptr); } /**********************************************/ @@ -895,47 +1248,65 @@ TreeItem::~TreeItem() { /**********************************************/ void Tree::update_cache() { - cache.font = get_theme_font("font"); - cache.tb_font = get_theme_font("title_button_font"); - 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.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_color_selected = get_theme_color("font_color_selected"); - cache.guide_color = get_theme_color("guide_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.draw_relationship_lines = get_theme_constant("draw_relationship_lines"); - cache.relationship_line_color = get_theme_color("relationship_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"); - - v_scroll->set_custom_step(cache.font->get_height()); + 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(SNAME("arrow_collapsed_mirrored")); + } else { + 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)); } int Tree::compute_item_height(TreeItem *p_item) const { @@ -944,9 +1315,13 @@ int Tree::compute_item_height(TreeItem *p_item) const { } ERR_FAIL_COND_V(cache.font.is_null(), 0); - int height = cache.font->get_height(); + int height = 0; for (int i = 0; i < columns.size(); i++) { + if (p_item->cells[i].dirty) { + const_cast<Tree *>(this)->update_item_cell(p_item, i); + } + height = MAX(height, p_item->cells[i].text_buf->get_size().y); for (int j = 0; j < p_item->cells[i].buttons.size(); j++) { Size2i s; // = cache.button_pressed->get_minimum_size(); s += p_item->cells[i].buttons[j].texture->get_size(); @@ -1001,7 +1376,7 @@ int Tree::get_item_height(TreeItem *p_item) const { if (!p_item->collapsed) { /* if not collapsed, check the children */ - TreeItem *c = p_item->children; + TreeItem *c = p_item->first_child; while (c) { height += get_item_height(c); @@ -1013,39 +1388,56 @@ int Tree::get_item_height(TreeItem *p_item) const { return height; } -void Tree::draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color) { +void Tree::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) { ERR_FAIL_COND(cache.font.is_null()); Rect2i rect = p_rect; - Ref<Font> font = cache.font; - String text = p_cell.text; - if (p_cell.suffix != String()) { - text += " " + p_cell.suffix; - } + Size2 ts = p_cell.text_buf->get_size(); + bool rtl = is_layout_rtl(); int w = 0; if (!p_cell.icon.is_null()) { Size2i bmsize = p_cell.get_icon_size(); - if (p_cell.icon_max_w > 0 && bmsize.width > p_cell.icon_max_w) { bmsize.width = p_cell.icon_max_w; } w += bmsize.width + cache.hseparation; + if (rect.size.width > 0 && (w + ts.width) > rect.size.width) { + ts.width = rect.size.width - w; + } } - w += font->get_string_size(text).width; + w += ts.width; switch (p_cell.text_align) { case TreeItem::ALIGN_LEFT: - break; //do none + if (rtl) { + rect.position.x += MAX(0, (rect.size.width - w)); + } + break; case TreeItem::ALIGN_CENTER: rect.position.x += MAX(0, (rect.size.width - w) / 2); - break; //do none + break; case TreeItem::ALIGN_RIGHT: - rect.position.x += MAX(0, (rect.size.width - w)); - break; //do none + if (!rtl) { + rect.position.x += MAX(0, (rect.size.width - w)); + } + break; } RID ci = get_canvas_item(); + + if (rtl) { + Point2 draw_pos = rect.position; + draw_pos.y += Math::floor((rect.size.y - p_cell.text_buf->get_size().y) / 2.0); + p_cell.text_buf->set_width(MAX(0, rect.size.width)); + if (p_ol_size > 0 && p_ol_color.a > 0) { + p_cell.text_buf->draw_outline(ci, draw_pos, p_ol_size, p_ol_color); + } + p_cell.text_buf->draw(ci, draw_pos, p_color); + rect.position.x += ts.width + cache.hseparation; + rect.size.x -= ts.width + cache.hseparation; + } + if (!p_cell.icon.is_null()) { Size2i bmsize = p_cell.get_icon_size(); @@ -1059,8 +1451,98 @@ void Tree::draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, co rect.size.x -= bmsize.x + cache.hseparation; } - rect.position.y += Math::floor((rect.size.y - font->get_height()) / 2.0) + font->get_ascent(); - font->draw(ci, rect.position, text, p_color, MAX(0, rect.size.width)); + if (!rtl) { + Point2 draw_pos = rect.position; + draw_pos.y += Math::floor((rect.size.y - p_cell.text_buf->get_size().y) / 2.0); + p_cell.text_buf->set_width(MAX(0, rect.size.width)); + if (p_ol_size > 0 && p_ol_color.a > 0) { + p_cell.text_buf->draw_outline(ci, draw_pos, p_ol_size, p_ol_color); + } + p_cell.text_buf->draw(ci, draw_pos, p_color); + } +} + +void Tree::update_column(int p_col) { + columns.write[p_col].text_buf->clear(); + if (columns[p_col].text_direction == Control::TEXT_DIRECTION_INHERITED) { + columns.write[p_col].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } 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()); +} + +void Tree::update_item_cell(TreeItem *p_item, int p_col) { + String valtext; + + p_item->cells.write[p_col].text_buf->clear(); + if (p_item->cells[p_col].mode == TreeItem::CELL_MODE_RANGE) { + if (p_item->cells[p_col].text != "") { + if (!p_item->cells[p_col].editable) { + return; + } + + int option = (int)p_item->cells[p_col].val; + + valtext = RTR("(Other)"); + Vector<String> strings = p_item->cells[p_col].text.split(","); + for (int j = 0; j < strings.size(); j++) { + int value = j; + if (!strings[j].get_slicec(':', 1).is_empty()) { + value = strings[j].get_slicec(':', 1).to_int(); + } + if (option == value) { + valtext = strings[j].get_slicec(':', 0); + break; + } + } + + } else { + valtext = String::num(p_item->cells[p_col].val, Math::range_step_decimals(p_item->cells[p_col].step)); + } + } else { + valtext = p_item->cells[p_col].text; + } + + if (p_item->cells[p_col].suffix != String()) { + valtext += " " + p_item->cells[p_col].suffix; + } + + if (p_item->cells[p_col].text_direction == Control::TEXT_DIRECTION_INHERITED) { + p_item->cells.write[p_col].text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR); + } else { + p_item->cells.write[p_col].text_buf->set_direction((TextServer::Direction)p_item->cells[p_col].text_direction); + } + + 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; +} + +void Tree::update_item_cache(TreeItem *p_item) { + for (int i = 0; i < p_item->cells.size(); i++) { + update_item_cell(p_item, i); + } + + TreeItem *c = p_item->first_child; + while (c) { + update_item_cache(c); + c = c->next; + } } int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 &p_draw_size, TreeItem *p_item) { @@ -1073,6 +1555,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 = cache.rtl; /* Calculate height of the label part */ label_h += cache.vseparation; @@ -1086,9 +1569,6 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 //if (p_item->get_parent()!=root || !hide_root) ERR_FAIL_COND_V(cache.font.is_null(), -1); - Ref<Font> font = cache.font; - - int font_ascent = font->get_ascent(); int ofs = p_pos.x + ((p_item->disable_folding || hide_folding) ? cache.hseparation : cache.item_margin); int skip2 = 0; @@ -1121,6 +1601,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; @@ -1133,12 +1627,20 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (cache.click_type == Cache::CLICK_BUTTON && cache.click_item == p_item && cache.click_column == i && cache.click_index == j && !p_item->cells[i].buttons[j].disabled) { //being pressed - cache.button_pressed->draw(get_canvas_item(), Rect2(o, s)); + Point2 od = o; + if (rtl) { + od.x = get_size().width - od.x - s.x; + } + cache.button_pressed->draw(get_canvas_item(), Rect2(od, s)); } o.y += (label_h - s.height) / 2; o += cache.button_pressed->get_offset(); + if (rtl) { + o.x = get_size().width - o.x - b->get_width(); + } + b->draw(ci, o, p_item->cells[i].buttons[j].disabled ? Color(1, 1, 1, 0.5) : p_item->cells[i].buttons[j].color); w -= s.width + cache.button_margin; bw += s.width + cache.button_margin; @@ -1152,14 +1654,21 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } if (cache.draw_guides) { - RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(cell_rect.position.x, cell_rect.position.y + cell_rect.size.height), cell_rect.position + cell_rect.size, cache.guide_color, 1); + Rect2 r = cell_rect; + if (rtl) { + r.position.x = get_size().width - r.position.x - r.size.x; + } + RenderingServer::get_singleton()->canvas_item_add_line(ci, Point2i(r.position.x, r.position.y + r.size.height), r.position + r.size, cache.guide_color, 1); } if (i == 0) { if (p_item->cells[0].selected && select_mode == SELECT_ROW) { - Rect2i row_rect = Rect2i(Point2i(cache.bg->get_margin(MARGIN_LEFT), item_rect.position.y), Size2i(get_size().width - cache.bg->get_minimum_size().width, item_rect.size.y)); + Rect2i row_rect = Rect2i(Point2i(cache.bg->get_margin(SIDE_LEFT), item_rect.position.y), Size2i(get_size().width - cache.bg->get_minimum_size().width, item_rect.size.y)); //Rect2 r = Rect2i(row_rect.pos,row_rect.size); - //r.grow(cache.selected->get_margin(MARGIN_LEFT)); + //r.grow(cache.selected->get_margin(SIDE_LEFT)); + if (rtl) { + row_rect.position.x = get_size().width - row_rect.position.x - row_rect.size.x; + } if (has_focus()) { cache.selected_focus->draw(ci, row_rect); } else { @@ -1168,19 +1677,15 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } } - if ((select_mode == SELECT_ROW && selected_item == p_item) || p_item->cells[i].selected) { + if ((select_mode == SELECT_ROW && selected_item == p_item) || p_item->cells[i].selected || !p_item->has_meta("__focus_rect")) { Rect2i r(cell_rect.position, cell_rect.size); - if (p_item->cells[i].text.size() > 0) { - float icon_width = p_item->cells[i].get_icon_size().width; - if (p_item->get_icon_max_width(i) > 0) { - icon_width = p_item->get_icon_max_width(i); - } - r.position.x += icon_width; - r.size.x -= icon_width; - } p_item->set_meta("__focus_rect", Rect2(r.position, r.size)); + if (rtl) { + r.position.x = get_size().width - r.position.x - r.size.x; + } + if (p_item->cells[i].selected) { if (has_focus()) { cache.selected_focus->draw(ci, r); @@ -1199,6 +1704,9 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 r.position.x -= cache.hseparation; r.size.x += cache.hseparation; } + if (rtl) { + r.position.x = get_size().width - r.position.x - r.size.x; + } if (p_item->cells[i].custom_bg_outline) { RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y, r.size.x, 1), p_item->cells[i].bg_color); RenderingServer::get_singleton()->canvas_item_add_rect(ci, Rect2(r.position.x, r.position.y + r.size.y - 1, r.size.x, 1), p_item->cells[i].bg_color); @@ -1209,41 +1717,64 @@ 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_children() != nullptr; - - 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 (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.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_color_selected" : "font_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 = 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) { + const_cast<Tree *>(this)->update_item_cell(p_item, i); + } + + if (rtl) { + item_rect.position.x = get_size().width - item_rect.position.x - item_rect.size.x; + } + Point2i text_pos = item_rect.position; - text_pos.y += Math::floor((item_rect.size.y - font->get_height()) / 2) + font_ascent; + text_pos.y += Math::floor((item_rect.size.y - p_item->cells[i].text_buf->get_size().y) / 2); + int text_width = p_item->cells[i].text_buf->get_size().x; switch (p_item->cells[i].mode) { case TreeItem::CELL_MODE_STRING: { - draw_item_rect(p_item->cells[i], item_rect, col, icon_col); + draw_item_rect(p_item->cells.write[i], item_rect, col, icon_col, outline_size, font_outline_color); } break; 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); @@ -1256,7 +1787,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 item_rect.size.x -= check_w; item_rect.position.x += check_w; - draw_item_rect(p_item->cells[i], item_rect, col, icon_col); + draw_item_rect(p_item->cells.write[i], item_rect, col, icon_col, outline_size, font_outline_color); } break; case TreeItem::CELL_MODE_RANGE: { @@ -1265,29 +1796,22 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 break; } - int option = (int)p_item->cells[i].val; + Ref<Texture2D> downarrow = cache.select_arrow; + int cell_width = item_rect.size.x - downarrow->get_width(); - String s = RTR("(Other)"); - Vector<String> strings = p_item->cells[i].text.split(","); - for (int j = 0; j < strings.size(); j++) { - int value = j; - if (!strings[j].get_slicec(':', 1).empty()) { - value = strings[j].get_slicec(':', 1).to_int(); + p_item->cells.write[i].text_buf->set_width(cell_width); + if (rtl) { + if (outline_size > 0 && font_outline_color.a > 0) { + p_item->cells[i].text_buf->draw_outline(ci, text_pos + Vector2(cell_width - text_width, 0), outline_size, font_outline_color); } - if (option == value) { - s = strings[j].get_slicec(':', 0); - break; + p_item->cells[i].text_buf->draw(ci, text_pos + Vector2(cell_width - text_width, 0), col); + } else { + if (outline_size > 0 && font_outline_color.a > 0) { + p_item->cells[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); } + p_item->cells[i].text_buf->draw(ci, text_pos, col); } - if (p_item->cells[i].suffix != String()) { - s += " " + p_item->cells[i].suffix; - } - - Ref<Texture2D> downarrow = cache.select_arrow; - - font->draw(ci, text_pos, s, col, item_rect.size.x - downarrow->get_width()); - Point2i arrow_pos = item_rect.position; arrow_pos.x += item_rect.size.x - downarrow->get_width(); arrow_pos.y += Math::floor(((item_rect.size.y - downarrow->get_height())) / 2.0); @@ -1296,14 +1820,20 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } else { Ref<Texture2D> updown = cache.updown; - String valtext = String::num(p_item->cells[i].val, Math::range_step_decimals(p_item->cells[i].step)); + int cell_width = item_rect.size.x - updown->get_width(); - if (p_item->cells[i].suffix != String()) { - valtext += " " + p_item->cells[i].suffix; + if (rtl) { + if (outline_size > 0 && font_outline_color.a > 0) { + p_item->cells[i].text_buf->draw_outline(ci, text_pos + Vector2(cell_width - text_width, 0), outline_size, font_outline_color); + } + p_item->cells[i].text_buf->draw(ci, text_pos + Vector2(cell_width - text_width, 0), col); + } else { + if (outline_size > 0 && font_outline_color.a > 0) { + p_item->cells[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + p_item->cells[i].text_buf->draw(ci, text_pos, col); } - font->draw(ci, text_pos, valtext, col, item_rect.size.x - updown->get_width()); - if (!p_item->cells[i].editable) { break; } @@ -1341,7 +1871,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } if (!p_item->cells[i].editable) { - draw_item_rect(p_item->cells[i], item_rect, col, icon_col); + draw_item_rect(p_item->cells.write[i], item_rect, col, icon_col, outline_size, font_outline_color); break; } @@ -1356,7 +1886,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (p_item->cells[i].custom_button) { if (cache.hover_item == p_item && cache.hover_cell == i) { - if (Input::get_singleton()->is_mouse_button_pressed(BUTTON_LEFT)) { + if (Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) { draw_style_box(cache.custom_button_pressed, ir); } else { draw_style_box(cache.custom_button_hover, ir); @@ -1369,7 +1899,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 ir.position += cache.custom_button->get_offset(); } - draw_item_rect(p_item->cells[i], ir, col, icon_col); + draw_item_rect(p_item->cells.write[i], ir, col, icon_col, outline_size, font_outline_color); downarrow->draw(ci, arrow_pos); @@ -1383,6 +1913,9 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } if (select_mode == SELECT_MULTI && selected_item == p_item && selected_col == i) { + if (is_layout_rtl()) { + cell_rect.position.x = get_size().width - cell_rect.position.x - cell_rect.size.x; + } if (has_focus()) { cache.cursor->draw(ci, cell_rect); } else { @@ -1391,7 +1924,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 } } - if (!p_item->disable_folding && !hide_folding && p_item->children) { //has children, draw the guide box + if (!p_item->disable_folding && !hide_folding && p_item->first_child) { //has children, draw the guide box Ref<Texture2D> arrow; @@ -1401,7 +1934,13 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 arrow = cache.arrow; } - arrow->draw(ci, p_pos + p_draw_ofs + Point2i(0, (label_h - arrow->get_height()) / 2) - cache.offset); + Point2 apos = p_pos + p_draw_ofs + Point2i(0, (label_h - arrow->get_height()) / 2) - cache.offset; + + if (rtl) { + apos.x = get_size().width - apos.x - arrow->get_width(); + } + + arrow->draw(ci, apos); } } @@ -1415,48 +1954,91 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 if (!p_item->collapsed) { /* if not collapsed, check the children */ - TreeItem *c = p_item->children; + TreeItem *c = p_item->first_child; - int prev_ofs = children_pos.y - cache.offset.y + p_draw_ofs.y; + int base_ofs = children_pos.y - cache.offset.y + p_draw_ofs.y; + int prev_ofs = base_ofs; + 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 + ((p_item->disable_folding || hide_folding) ? cache.hseparation : 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_children() != 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; + + 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; - float line_width = 1.0; #ifdef TOOLS_ENABLED - line_width *= 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; - if (root_pos.y + line_width >= 0) { - 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), Point2i(parent_pos.x, prev_ofs), cache.relationship_line_color, line_width); - } + int more_prev_ofs = 0; - if (htotal < 0) { - return -1; - } - prev_ofs = root_pos.y; - } + 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 (htotal >= 0) { - int child_h = draw_item(children_pos, p_draw_ofs, p_draw_size, c); + // 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 { + // 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_ofs = root_pos.y + more_prev_ofs; + } if (child_h < 0) { if (cache.draw_relationship_lines == 0) { return -1; // break, stop drawing, no need to anymore - } else { - htotal = -1; - children_pos.y = cache.offset.y + p_draw_size.height; } + + htotal = -1; + children_pos.y = cache.offset.y + p_draw_size.height; } else { htotal += child_h; children_pos.y += child_h; @@ -1478,8 +2060,8 @@ int Tree::_count_selected_items(TreeItem *p_from) const { } } - if (p_from->get_children()) { - count += _count_selected_items(p_from->get_children()); + if (p_from->get_first_child()) { + count += _count_selected_items(p_from->get_first_child()); } if (p_from->get_next()) { @@ -1489,6 +2071,36 @@ int Tree::_count_selected_items(TreeItem *p_from) const { return count; } +bool Tree::_is_branch_selected(TreeItem *p_from) const { + for (int i = 0; i < columns.size(); i++) { + if (p_from->is_selected(i)) { + return true; + } + } + + TreeItem *child_item = p_from->get_first_child(); + while (child_item) { + if (_is_branch_selected(child_item)) { + return true; + } + child_item = child_item->get_next(); + } + + return false; +} + +bool Tree::_is_sibling_branch_selected(TreeItem *p_from) const { + TreeItem *sibling_item = p_from->get_next(); + while (sibling_item) { + if (_is_branch_selected(sibling_item)) { + return true; + } + sibling_item = sibling_item->get_next(); + } + + return false; +} + void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_col, TreeItem *p_prev, bool *r_in_range, bool p_force_deselect) { TreeItem::Cell &selected_cell = p_selected->cells.write[p_col]; @@ -1513,7 +2125,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; } /* @@ -1533,28 +2145,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; } @@ -1567,7 +2179,7 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c *r_in_range = false; } - TreeItem *c = p_current->children; + TreeItem *c = p_current->first_child; while (c) { select_single_item(p_selected, c, p_col, p_prev, r_in_range, p_current->is_collapsed() || p_force_deselect); @@ -1580,7 +2192,7 @@ Rect2 Tree::search_item_rect(TreeItem *p_from, TreeItem *p_item) { } void Tree::_range_click_timeout() { - if (range_item_last && !range_drag_enabled && Input::get_singleton()->is_mouse_button_pressed(BUTTON_LEFT)) { + if (range_item_last && !range_drag_enabled && Input::get_singleton()->is_mouse_button_pressed(MOUSE_BUTTON_LEFT)) { Point2 pos = get_local_mouse_position() - cache.bg->get_offset(); if (show_column_titles) { pos.y -= _get_title_button_height(); @@ -1591,14 +2203,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, 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()) { @@ -1612,7 +2234,7 @@ void Tree::_range_click_timeout() { } if (propagate_mouse_activated) { - emit_signal("item_activated"); + emit_signal(SNAME("item_activated")); propagate_mouse_activated = false; } @@ -1621,7 +2243,7 @@ void Tree::_range_click_timeout() { } } -int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool p_doubleclick, 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); @@ -1634,7 +2256,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool } if (!p_item->disable_folding && !hide_folding && (p_pos.x >= x_ofs && p_pos.x < (x_ofs + cache.item_margin))) { - if (p_item->children) { + if (p_item->first_child) { p_item->set_collapsed(!p_item->is_collapsed()); } @@ -1646,6 +2268,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); @@ -1661,6 +2286,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; } @@ -1672,16 +2298,18 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool return -1; } else if (col == 0) { int margin = x_ofs + cache.item_margin; //-cache.hseparation; - //int lm = cache.bg->get_margin(MARGIN_LEFT); + //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; } - if (!p_item->disable_folding && !hide_folding && !p_item->cells[col].editable && !p_item->cells[col].selectable && p_item->get_children()) { + if (!p_item->disable_folding && !hide_folding && !p_item->cells[col].editable && !p_item->cells[col].selectable && p_item->get_first_child()) { p_item->set_collapsed(!p_item->is_collapsed()); return -1; //collapse/uncollapse because nothing can be done with item } @@ -1691,6 +2319,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; @@ -1709,16 +2347,17 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool cache.click_column = col; cache.click_pos = get_global_mouse_position() - get_global_position(); update(); - //emit_signal("button_pressed"); + //emit_signal(SNAME("button_pressed")); return -1; } + col_width -= w + cache.button_margin; } - if (p_button == BUTTON_LEFT || (p_button == BUTTON_RIGHT && allow_rmb_select)) { + if (p_button == MOUSE_BUTTON_LEFT || (p_button == MOUSE_BUTTON_RIGHT && allow_rmb_select)) { /* process selection */ - if (p_doubleclick && (!c.editable || c.mode == TreeItem::CELL_MODE_CUSTOM || c.mode == TreeItem::CELL_MODE_ICON /*|| c.mode==TreeItem::CELL_MODE_CHECK*/)) { //it's confusing for check + if (p_double_click && (!c.editable || c.mode == TreeItem::CELL_MODE_CUSTOM || c.mode == TreeItem::CELL_MODE_ICON /*|| c.mode==TreeItem::CELL_MODE_CHECK*/)) { //it's confusing for check propagate_mouse_activated = true; @@ -1726,50 +2365,50 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool return -1; } - if (select_mode == SELECT_MULTI && p_mod->get_command() && c.selectable) { - if (!c.selected || p_button == BUTTON_RIGHT) { + 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); - if (p_button == BUTTON_RIGHT) { - emit_signal("item_rmb_selected", get_local_mouse_position()); + emit_signal(SNAME("multi_selected"), p_item, col, true); + if (p_button == MOUSE_BUTTON_RIGHT) { + 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); } } else { if (c.selectable) { - if (select_mode == SELECT_MULTI && p_mod->get_shift() && selected_item && selected_item != p_item) { + if (select_mode == SELECT_MULTI && p_mod->is_shift_pressed() && selected_item && selected_item != p_item) { bool inrange = false; select_single_item(p_item, root, col, selected_item, &inrange); - if (p_button == BUTTON_RIGHT) { - emit_signal("item_rmb_selected", get_local_mouse_position()); + if (p_button == MOUSE_BUTTON_RIGHT) { + emit_signal(SNAME("item_rmb_selected"), get_local_mouse_position()); } } else { int icount = _count_selected_items(root); - if (select_mode == SELECT_MULTI && icount > 1 && p_button != BUTTON_RIGHT) { + if (select_mode == SELECT_MULTI && icount > 1 && p_button != MOUSE_BUTTON_RIGHT) { single_select_defer = p_item; single_select_defer_column = col; } else { - if (p_button != BUTTON_RIGHT || !c.selected) { + if (p_button != MOUSE_BUTTON_RIGHT || !c.selected) { select_single_item(p_item, root, col); } - if (p_button == BUTTON_RIGHT) { - emit_signal("item_rmb_selected", get_local_mouse_position()); + if (p_button == MOUSE_BUTTON_RIGHT) { + 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(); @@ -1813,11 +2452,10 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool case TreeItem::CELL_MODE_RANGE: { if (c.text != "") { //if (x >= (get_column_width(col)-item_h/2)) { - popup_menu->clear(); for (int i = 0; i < c.text.get_slice_count(","); i++) { String s = c.text.get_slicec(',', i); - popup_menu->add_item(s.get_slicec(':', 0), s.get_slicec(':', 1).empty() ? i : s.get_slicec(':', 1).to_int()); + popup_menu->add_item(s.get_slicec(':', 0), s.get_slicec(':', 1).is_empty() ? i : s.get_slicec(':', 1).to_int()); } popup_menu->set_size(Size2(col_width, 0)); @@ -1832,7 +2470,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool /* touching the combo */ bool up = p_pos.y < (item_h / 2); - if (p_button == BUTTON_LEFT) { + if (p_button == MOUSE_BUTTON_LEFT) { if (range_click_timer->get_time_left() == 0) { range_item_last = p_item; range_up_last = up; @@ -1849,13 +2487,13 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool item_edited(col, p_item); - } else if (p_button == BUTTON_RIGHT) { + } else if (p_button == MOUSE_BUTTON_RIGHT) { p_item->set_range(col, (up ? c.max : c.min)); item_edited(col, p_item); - } else if (p_button == BUTTON_WHEEL_UP) { + } else if (p_button == MOUSE_BUTTON_WHEEL_UP) { p_item->set_range(col, c.val + c.step); item_edited(col, p_item); - } else if (p_button == BUTTON_WHEEL_DOWN) { + } else if (p_button == MOUSE_BUTTON_WHEEL_DOWN) { p_item->set_range(col, c.val - c.step); item_edited(col, p_item); } @@ -1884,18 +2522,18 @@ 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) { - item_edited(col, p_item, p_button == BUTTON_LEFT); + item_edited(col, p_item, p_button == MOUSE_BUTTON_LEFT); } click_handled = true; return -1; } break; }; - if (!bring_up_editor || p_button != BUTTON_LEFT) { + if (!bring_up_editor || p_button != MOUSE_BUTTON_LEFT) { return -1; } @@ -1920,10 +2558,10 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool if (!p_item->collapsed) { /* if not collapsed, check the children */ - TreeItem *c = p_item->children; + TreeItem *c = p_item->first_child; while (c) { - int child_h = propagate_mouse_event(new_pos, x_ofs, y_ofs, p_doubleclick, 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 @@ -1935,8 +2573,8 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, bool item_h += child_h; } } - if (p_item == root && p_button == BUTTON_RIGHT) { - emit_signal("empty_rmb", get_local_mouse_position()); + if (p_item == root && p_button == MOUSE_BUTTON_RIGHT) { + emit_signal(SNAME("empty_rmb"), get_local_mouse_position()); } } @@ -1954,10 +2592,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) { @@ -1977,14 +2615,13 @@ void Tree::_text_editor_enter(String p_text) { case TreeItem::CELL_MODE_RANGE: { c.val = p_text.to_float(); if (c.step > 0) { - c.val = Math::stepify(c.val, c.step); + c.val = Math::snapped(c.val, c.step); } if (c.val < c.min) { c.val = c.min; } else if (c.val > c.max) { c.val = c.max; } - //popup_edited_item->edited_signal.call( popup_edited_item_col ); } break; default: { @@ -2027,7 +2664,7 @@ void Tree::popup_select(int p_option) { void Tree::_go_left() { if (selected_col == 0) { - if (selected_item->get_children() != nullptr && !selected_item->is_collapsed()) { + if (selected_item->get_first_child() != nullptr && !selected_item->is_collapsed()) { selected_item->set_collapsed(true); } else { if (columns.size() == 1) { // goto parent with one column @@ -2043,7 +2680,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); } @@ -2055,7 +2692,7 @@ void Tree::_go_left() { void Tree::_go_right() { if (selected_col == (columns.size() - 1)) { - if (selected_item->get_children() != nullptr && selected_item->is_collapsed()) { + if (selected_item->get_first_child() != nullptr && selected_item->is_collapsed()) { selected_item->set_collapsed(false); } else if (selected_item->get_next_visible()) { selected_col = 0; @@ -2064,7 +2701,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); } @@ -2097,7 +2734,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; @@ -2140,7 +2777,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; @@ -2158,10 +2795,12 @@ 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; - bool is_command = k.is_valid() && k->get_command(); + bool is_command = k.is_valid() && k->is_command_pressed(); if (p_event->is_action("ui_right") && p_event->is_pressed()) { if (!cursor_can_exit_tree) { accept_event(); @@ -2170,9 +2809,9 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (!selected_item || select_mode == SELECT_ROW || selected_col > (columns.size() - 1)) { return; } - if (k.is_valid() && k->get_alt()) { + if (k.is_valid() && k->is_alt_pressed()) { selected_item->set_collapsed(false); - TreeItem *next = selected_item->get_children(); + TreeItem *next = selected_item->get_first_child(); while (next && next != selected_item->next) { next->set_collapsed(false); next = next->get_next_visible(); @@ -2189,9 +2828,9 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { return; } - if (k.is_valid() && k->get_alt()) { + if (k.is_valid() && k->is_alt_pressed()) { selected_item->set_collapsed(true); - TreeItem *next = selected_item->get_children(); + TreeItem *next = selected_item->get_first_child(); while (next && next != selected_item->next) { next->set_collapsed(true); next = next->get_next_visible(); @@ -2239,7 +2878,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) { @@ -2277,7 +2916,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) { @@ -2293,7 +2932,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(); } } @@ -2305,10 +2944,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(); @@ -2319,7 +2958,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (!k->is_pressed()) { return; } - if (k->get_command() || (k->get_shift() && k->get_unicode() == 0) || k->get_metakey()) { + if (k->is_command_pressed() || (k->is_shift_pressed() && k->get_unicode() == 0) || k->is_meta_pressed()) { return; } if (!root) { @@ -2350,8 +2989,13 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } Ref<StyleBox> bg = cache.bg; + bool rtl = is_layout_rtl(); - Point2 pos = mm->get_position() - bg->get_offset(); + Point2 pos = mm->get_position(); + if (rtl) { + pos.x = get_size().width - pos.x; + } + pos -= cache.bg->get_offset(); Cache::ClickType old_hover = cache.hover_type; int old_index = cache.hover_index; @@ -2376,6 +3020,9 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (root) { Point2 mpos = mm->get_position(); + if (rtl) { + mpos.x = get_size().width - mpos.x; + } mpos -= cache.bg->get_offset(); mpos.y -= _get_title_button_height(); if (mpos.y >= 0) { @@ -2432,6 +3079,9 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { if (!range_drag_enabled) { Vector2 cpos = mm->get_position(); + if (rtl) { + cpos.x = get_size().width - cpos.x; + } if (cpos.distance_to(pressing_pos) > 2) { range_drag_enabled = true; range_drag_capture_pos = cpos; @@ -2463,9 +3113,15 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { update_cache(); } + bool rtl = is_layout_rtl(); + if (!b->is_pressed()) { - if (b->get_button_index() == BUTTON_LEFT) { - Point2 pos = b->get_position() - cache.bg->get_offset(); + if (b->get_button_index() == MOUSE_BUTTON_LEFT) { + Point2 pos = b->get_position(); + if (rtl) { + pos.x = get_size().width - pos.x; + } + pos -= cache.bg->get_offset(); if (show_column_titles) { pos.y -= _get_title_button_height(); @@ -2475,7 +3131,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; } } @@ -2496,12 +3152,16 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { warp_mouse(range_drag_capture_pos); } else { Rect2 rect = get_selected()->get_meta("__focus_rect"); - if (rect.has_point(Point2(b->get_position().x, b->get_position().y))) { + Point2 mpos = b->get_position(); + if (rtl) { + mpos.x = get_size().width - mpos.x; + } + 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; @@ -2510,7 +3170,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; @@ -2537,17 +3197,21 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } switch (b->get_button_index()) { - case BUTTON_RIGHT: - case BUTTON_LEFT: { + case MOUSE_BUTTON_RIGHT: + case MOUSE_BUTTON_LEFT: { Ref<StyleBox> bg = cache.bg; - Point2 pos = b->get_position() - bg->get_offset(); + Point2 pos = b->get_position(); + if (rtl) { + pos.x = get_size().width - pos.x; + } + pos -= bg->get_offset(); cache.click_type = Cache::CLICK_NONE; if (show_column_titles) { pos.y -= _get_title_button_height(); if (pos.y < 0) { - if (b->get_button_index() == BUTTON_LEFT) { + if (b->get_button_index() == MOUSE_BUTTON_LEFT) { pos.x += cache.offset.x; int len = 0; for (int i = 0; i < columns.size(); i++) { @@ -2564,9 +3228,9 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { break; } } - if (!root || (!root->get_children() && hide_root)) { - if (b->get_button_index() == BUTTON_RIGHT && allow_rmb_select) { - emit_signal("empty_tree_rmb_selected", get_local_mouse_position()); + if (!root || (!root->get_first_child() && hide_root)) { + if (b->get_button_index() == MOUSE_BUTTON_RIGHT && allow_rmb_select) { + emit_signal(SNAME("empty_tree_rmb_selected"), get_local_mouse_position()); } break; } @@ -2575,15 +3239,24 @@ 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_doubleclick(), 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) { pressing_pos = b->get_position(); + if (rtl) { + pressing_pos.x = get_size().width - pressing_pos.x; + } } - if (b->get_button_index() == BUTTON_RIGHT) { + if (b->get_button_index() == MOUSE_BUTTON_RIGHT) { break; } @@ -2600,26 +3273,26 @@ 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); } - if (b->get_button_index() == BUTTON_LEFT) { - if (get_item_at_position(b->get_position()) == nullptr && !b->get_shift() && !b->get_control() && !b->get_command()) { - emit_signal("nothing_selected"); + 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(SNAME("nothing_selected")); } } } if (propagate_mouse_activated) { - emit_signal("item_activated"); + emit_signal(SNAME("item_activated")); propagate_mouse_activated = false; } } break; - case BUTTON_WHEEL_UP: { + case MOUSE_BUTTON_WHEEL_UP: { double prev_value = v_scroll->get_value(); v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() * b->get_factor() / 8); if (v_scroll->get_value() != prev_value) { @@ -2627,7 +3300,7 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } } break; - case BUTTON_WHEEL_DOWN: { + case MOUSE_BUTTON_WHEEL_DOWN: { double prev_value = v_scroll->get_value(); v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() * b->get_factor() / 8); if (v_scroll->get_value() != prev_value) { @@ -2635,6 +3308,8 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { } } break; + default: + break; } } @@ -2644,7 +3319,11 @@ void Tree::_gui_input(Ref<InputEvent> p_event) { v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() * pan_gesture->get_delta().y / 8); double prev_h = h_scroll->get_value(); - h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * pan_gesture->get_delta().x / 8); + if (is_layout_rtl()) { + h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * -pan_gesture->get_delta().x / 8); + } else { + h_scroll->set_value(h_scroll->get_value() + h_scroll->get_page() * pan_gesture->get_delta().x / 8); + } if (v_scroll->get_value() != prev_v || h_scroll->get_value() != prev_h) { accept_event(); @@ -2677,7 +3356,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; @@ -2685,7 +3364,7 @@ bool Tree::edit_selected() { popup_menu->clear(); for (int i = 0; i < c.text.get_slice_count(","); i++) { String s2 = c.text.get_slicec(',', i); - popup_menu->add_item(s2.get_slicec(':', 0), s2.get_slicec(':', 1).empty() ? i : s2.get_slicec(':', 1).to_int()); + popup_menu->add_item(s2.get_slicec(':', 0), s2.get_slicec(':', 1).is_empty() ? i : s2.get_slicec(':', 1).to_int()); } popup_menu->set_size(Size2(rect.size.width, 0)); @@ -2736,13 +3415,17 @@ bool Tree::edit_selected() { return false; } +bool Tree::is_editing() { + return popup_editor->is_visible(); +} + Size2 Tree::get_internal_min_size() const { Size2i size = cache.bg->get_offset(); if (root) { 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; @@ -2760,38 +3443,56 @@ void Tree::update_scrollbars() { 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.bg->get_margin(MARGIN_TOP))); - v_scroll->set_end(Point2(size.width, size.height - cache.bg->get_margin(MARGIN_TOP) - cache.bg->get_margin(MARGIN_BOTTOM))); + v_scroll->set_begin(Point2(size.width - vmin.width, cache.bg->get_margin(SIDE_TOP))); + v_scroll->set_end(Point2(size.width, size.height - cache.bg->get_margin(SIDE_TOP) - cache.bg->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)); - 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; } } int Tree::_get_title_button_height() const { ERR_FAIL_COND_V(cache.font.is_null() || cache.title_button.is_null(), 0); - return show_column_titles ? cache.font->get_height() + cache.title_button->get_minimum_size().height : 0; + int h = 0; + if (show_column_titles) { + for (int i = 0; i < columns.size(); i++) { + h = MAX(h, columns[i].text_buf->get_size().y + cache.title_button->get_minimum_size().height); + } + } + return h; } void Tree::_notification(int p_what) { @@ -2891,46 +3592,66 @@ void Tree::_notification(int p_what) { RID ci = get_canvas_item(); Ref<StyleBox> bg = cache.bg; - Ref<StyleBox> bg_focus = get_theme_stylebox("bg_focus"); + 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())); - if (has_focus()) { - RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, true); - bg_focus->draw(ci, Rect2(Point2(), get_size())); - RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, false); - } int tbh = _get_title_button_height(); 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); } if (show_column_titles) { //title buttons - int ofs2 = cache.bg->get_margin(MARGIN_LEFT); + int ofs2 = cache.bg->get_margin(SIDE_LEFT); for (int i = 0; i < columns.size(); i++) { 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(MARGIN_TOP), get_column_width(i), tbh); + Rect2 tbrect = Rect2(ofs2 - cache.offset.x, bg->get_margin(SIDE_TOP), get_column_width(i), tbh); + if (cache.rtl) { + tbrect.position.x = get_size().width - tbrect.size.x - tbrect.position.x; + } sb->draw(ci, tbrect); ofs2 += tbrect.size.width; //text int clip_w = tbrect.size.width - sb->get_minimum_size().width; - f->draw_halign(ci, tbrect.position + Point2i(sb->get_offset().x, (tbrect.size.height - f->get_height()) / 2 + f->get_ascent()), HALIGN_CENTER, clip_w, columns[i].title, cache.title_button_color); + columns.write[i].text_buf->set_width(clip_w); + + Vector2 text_pos = tbrect.position + Point2i(sb->get_offset().x + (tbrect.size.width - columns[i].text_buf->get_size().x) / 2, (tbrect.size.height - columns[i].text_buf->get_size().y) / 2); + if (outline_size > 0 && font_outline_color.a > 0) { + columns[i].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + columns[i].text_buf->draw(ci, text_pos, cache.title_button_color); } } + + // Draw the background focus outline last, so that it is drawn in front of the section headings. + // 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(SNAME("bg_focus")); + bg_focus->draw(ci, Rect2(Point2(), get_size())); + RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, false); + } } - if (p_what == NOTIFICATION_THEME_CHANGED) { + if (p_what == NOTIFICATION_THEME_CHANGED || p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { update_cache(); + _update_all(); } if (p_what == NOTIFICATION_RESIZED || p_what == NOTIFICATION_TRANSFORM_CHANGED) { @@ -2948,8 +3669,27 @@ void Tree::_notification(int p_what) { } } +void Tree::_update_all() { + for (int i = 0; i < columns.size(); i++) { + update_column(i); + } + if (root) { + update_item_cache(root); + } +} + 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) { @@ -2958,38 +3698,15 @@ TreeItem *Tree::create_item(TreeItem *p_parent, int p_idx) { TreeItem *ti = nullptr; if (p_parent) { - // Append or insert a new item to the given parent. - ti = memnew(TreeItem(this)); - ERR_FAIL_COND_V(!ti, nullptr); - ti->cells.resize(columns.size()); - - TreeItem *prev = nullptr; - TreeItem *c = p_parent->children; - int idx = 0; - - while (c) { - if (idx++ == p_idx) { - ti->next = c; - break; - } - prev = c; - c = c->next; - } - - if (prev) { - prev->next = ti; - } else { - p_parent->children = ti; - } - ti->parent = p_parent; - + ERR_FAIL_COND_V_MSG(p_parent->tree != this, nullptr, "A different tree owns the given parent"); + ti = p_parent->create_child(p_idx); } else { if (!root) { // No root exists, make the given item the new root. ti = memnew(TreeItem(this)); ERR_FAIL_COND_V(!ti, nullptr); ti->cells.resize(columns.size()); - + ti->is_root = true; root = ti; } else { // Root exists, append or insert to root. @@ -3000,18 +3717,18 @@ 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) { if (last->next) { last = last->next; - } else if (last->children) { - last = last->children; + } else if (last->first_child) { + last = last->first_child; } else { break; } @@ -3023,14 +3740,20 @@ TreeItem *Tree::get_last_item() { void Tree::item_edited(int p_column, TreeItem *p_item, bool p_lmb) { edited_item = p_item; edited_col = p_column; + if (p_item != nullptr && p_column >= 0 && p_column < p_item->cells.size()) { + 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")); } } void Tree::item_changed(int p_column, TreeItem *p_item) { + if (p_item != nullptr && p_column >= 0 && p_column < p_item->cells.size()) { + p_item->cells.write[p_column].dirty = true; + } update(); } @@ -3041,9 +3764,12 @@ 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) { + selected_item = p_item; + } } else { select_single_item(p_item, root, p_column); } @@ -3051,6 +3777,14 @@ void Tree::item_selected(int p_column, TreeItem *p_item) { } void Tree::item_deselected(int p_column, TreeItem *p_item) { + if (selected_item == p_item) { + selected_item = nullptr; + + if (selected_col == p_column) { + selected_col = -1; + } + } + if (select_mode == SELECT_MULTI || select_mode == SELECT_SINGLE) { p_item->cells.write[p_column].selected = false; } @@ -3117,13 +3851,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(); } @@ -3134,6 +3868,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; } @@ -3163,8 +3927,8 @@ TreeItem *Tree::get_next_selected(TreeItem *p_item) { if (!p_item) { p_item = root; } else { - if (p_item->children) { - p_item = p_item->children; + if (p_item->first_child) { + p_item = p_item->first_child; } else if (p_item->next) { p_item = p_item->next; @@ -3190,49 +3954,90 @@ 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); + } } - Ref<StyleBox> bg = cache.bg; + return min_width; +} - int expand_area = get_size().width - (bg->get_margin(MARGIN_LEFT) + bg->get_margin(MARGIN_RIGHT)); +int Tree::get_column_width(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), -1); - if (v_scroll->is_visible_in_tree()) { - expand_area -= v_scroll->get_combined_minimum_size().width; - } + int column_width = get_column_minimum_width(p_column); - int expanding_columns = 0; - int expanding_total = 0; + if (columns[p_column].expand) { + int expand_area = get_size().width; - 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++; + Ref<StyleBox> bg = cache.bg; + + 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; + } + + 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; + } + } - ERR_FAIL_COND_V(expanding_columns == 0, -1); // shouldn't happen + 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) { p_item->cells.resize(columns.size()); - TreeItem *c = p_item->get_children(); + TreeItem *c = p_item->get_first_child(); while (c) { propagate_set_columns(c); - c = c->get_next(); + c = c->next; } } @@ -3279,8 +4084,8 @@ int Tree::get_item_offset(TreeItem *p_item) const { ofs += cache.vseparation; } - if (it->children && !it->collapsed) { - it = it->children; + if (it->first_child && !it->collapsed) { + it = it->first_child; } else if (it->next) { it = it->next; @@ -3320,7 +4125,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); } @@ -3338,7 +4143,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); } @@ -3388,7 +4193,11 @@ bool Tree::are_column_titles_visible() const { void Tree::set_column_title(int p_column, const String &p_title) { ERR_FAIL_INDEX(p_column, columns.size()); + if (cache.font.is_null()) { // avoid a strange case that may corrupt stuff + update_cache(); + } columns.write[p_column].title = p_title; + update_column(p_column); update(); } @@ -3397,6 +4206,61 @@ String Tree::get_column_title(int p_column) const { return columns[p_column].title; } +void Tree::set_column_title_direction(int p_column, Control::TextDirection p_text_direction) { + ERR_FAIL_INDEX(p_column, columns.size()); + ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + if (columns[p_column].text_direction != p_text_direction) { + columns.write[p_column].text_direction = p_text_direction; + update_column(p_column); + update(); + } +} + +Control::TextDirection Tree::get_column_title_direction(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), TEXT_DIRECTION_INHERITED); + return columns[p_column].text_direction; +} + +void Tree::clear_column_title_opentype_features(int p_column) { + ERR_FAIL_INDEX(p_column, columns.size()); + columns.write[p_column].opentype_features.clear(); + update_column(p_column); + update(); +} + +void Tree::set_column_title_opentype_feature(int p_column, const String &p_name, int p_value) { + ERR_FAIL_INDEX(p_column, columns.size()); + int32_t tag = TS->name_to_tag(p_name); + if (!columns[p_column].opentype_features.has(tag) || (int)columns[p_column].opentype_features[tag] != p_value) { + columns.write[p_column].opentype_features[tag] = p_value; + update_column(p_column); + update(); + } +} + +int Tree::get_column_title_opentype_feature(int p_column, const String &p_name) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), -1); + int32_t tag = TS->name_to_tag(p_name); + if (!columns[p_column].opentype_features.has(tag)) { + return -1; + } + return columns[p_column].opentype_features[tag]; +} + +void Tree::set_column_title_language(int p_column, const String &p_language) { + ERR_FAIL_INDEX(p_column, columns.size()); + if (columns[p_column].language != p_language) { + columns.write[p_column].language = p_language; + update_column(p_column); + update(); + } +} + +String Tree::get_column_title_language(int p_column) const { + ERR_FAIL_INDEX_V(p_column, columns.size(), ""); + return columns[p_column].language; +} + Point2 Tree::get_scroll() const { Point2 ofs; if (h_scroll->is_visible_in_tree()) { @@ -3426,6 +4290,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. @@ -3544,7 +4426,7 @@ TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_ return nullptr; // do not try children, it's collapsed } - TreeItem *n = p_item->get_children(); + TreeItem *n = p_item->get_first_child(); while (n) { int ch; TreeItem *r = _find_item_at_pos(n, pos, r_column, ch, section); @@ -3562,6 +4444,9 @@ TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_ int Tree::get_column_at_position(const Point2 &p_pos) const { if (root) { Point2 pos = p_pos; + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } pos -= cache.bg->get_offset(); pos.y -= _get_title_button_height(); if (pos.y < 0) { @@ -3589,6 +4474,9 @@ int Tree::get_column_at_position(const Point2 &p_pos) const { int Tree::get_drop_section_at_position(const Point2 &p_pos) const { if (root) { Point2 pos = p_pos; + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } pos -= cache.bg->get_offset(); pos.y -= _get_title_button_height(); if (pos.y < 0) { @@ -3616,6 +4504,9 @@ int Tree::get_drop_section_at_position(const Point2 &p_pos) const { TreeItem *Tree::get_item_at_position(const Point2 &p_pos) const { if (root) { Point2 pos = p_pos; + if (is_layout_rtl()) { + pos.x = get_size().width - pos.x; + } pos -= cache.bg->get_offset(); pos.y -= _get_title_button_height(); if (pos.y < 0) { @@ -3736,10 +4627,6 @@ void Tree::set_cursor_can_exit_tree(bool p_enable) { cursor_can_exit_tree = p_enable; } -bool Tree::can_cursor_exit_tree() const { - return cursor_can_exit_tree; -} - void Tree::set_hide_folding(bool p_hide) { hide_folding = p_hide; update(); @@ -3790,14 +4677,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); @@ -3814,6 +4705,7 @@ void Tree::_bind_methods() { ClassDB::bind_method(D_METHOD("get_edited"), &Tree::get_edited); ClassDB::bind_method(D_METHOD("get_edited_column"), &Tree::get_edited_column); + ClassDB::bind_method(D_METHOD("edit_selected"), &Tree::edit_selected); ClassDB::bind_method(D_METHOD("get_custom_popup_rect"), &Tree::get_custom_popup_rect); ClassDB::bind_method(D_METHOD("get_item_area_rect", "item", "column"), &Tree::_get_item_rect, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_item_at_position", "position"), &Tree::get_item_at_position); @@ -3827,7 +4719,25 @@ void Tree::_bind_methods() { ClassDB::bind_method(D_METHOD("set_column_title", "column", "title"), &Tree::set_column_title); ClassDB::bind_method(D_METHOD("get_column_title", "column"), &Tree::get_column_title); + + ClassDB::bind_method(D_METHOD("set_column_title_direction", "column", "direction"), &Tree::set_column_title_direction); + ClassDB::bind_method(D_METHOD("get_column_title_direction", "column"), &Tree::get_column_title_direction); + + ClassDB::bind_method(D_METHOD("set_column_title_opentype_feature", "column", "tag", "value"), &Tree::set_column_title_opentype_feature); + ClassDB::bind_method(D_METHOD("get_column_title_opentype_feature", "column", "tag"), &Tree::get_column_title_opentype_feature); + ClassDB::bind_method(D_METHOD("clear_column_title_opentype_features", "column"), &Tree::clear_column_title_opentype_features); + + ClassDB::bind_method(D_METHOD("set_column_title_language", "column", "language"), &Tree::set_column_title_language); + ClassDB::bind_method(D_METHOD("get_column_title_language", "column"), &Tree::get_column_title_language); + 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); @@ -3846,8 +4756,10 @@ void Tree::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_rmb_select"), "set_allow_rmb_select", "get_allow_rmb_select"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hide_folding"), "set_hide_folding", "is_folding_hidden"); 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, "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")); @@ -3860,7 +4772,7 @@ void Tree::_bind_methods() { ADD_SIGNAL(MethodInfo("item_custom_button_pressed")); ADD_SIGNAL(MethodInfo("item_double_clicked")); ADD_SIGNAL(MethodInfo("item_collapsed", PropertyInfo(Variant::OBJECT, "item", PROPERTY_HINT_RESOURCE_TYPE, "TreeItem"))); - //ADD_SIGNAL( MethodInfo("item_doubleclicked" ) ); + //ADD_SIGNAL( MethodInfo("item_double_clicked" ) ); ADD_SIGNAL(MethodInfo("button_pressed", PropertyInfo(Variant::OBJECT, "item", PROPERTY_HINT_RESOURCE_TYPE, "TreeItem"), PropertyInfo(Variant::INT, "column"), PropertyInfo(Variant::INT, "id"))); ADD_SIGNAL(MethodInfo("custom_popup_edited", PropertyInfo(Variant::BOOL, "arrow_clicked"))); ADD_SIGNAL(MethodInfo("item_activated")); @@ -3877,19 +4789,8 @@ void Tree::_bind_methods() { } Tree::Tree() { - selected_col = 0; columns.resize(1); - selected_item = nullptr; - edited_item = nullptr; - selected_col = -1; - edited_col = -1; - hide_root = false; - select_mode = SELECT_SINGLE; - root = nullptr; - popup_menu = nullptr; - popup_edited_item = nullptr; - text_editor = nullptr; set_focus_mode(FOCUS_ALL); popup_menu = memnew(PopupMenu); @@ -3903,7 +4804,7 @@ Tree::Tree() { popup_editor_vb = memnew(VBoxContainer); popup_editor->add_child(popup_editor_vb); popup_editor_vb->add_theme_constant_override("separation", 0); - popup_editor_vb->set_anchors_and_margins_preset(PRESET_WIDE); + popup_editor_vb->set_anchors_and_offsets_preset(PRESET_WIDE); text_editor = memnew(LineEdit); popup_editor_vb->add_child(text_editor); text_editor->set_v_size_flags(SIZE_EXPAND_FILL); @@ -3926,57 +4827,18 @@ Tree::Tree() { 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)); set_notify_transform(true); - updating_value_editor = false; - pressed_button = -1; - show_column_titles = false; - - cache.click_type = Cache::CLICK_NONE; - cache.hover_type = Cache::CLICK_NONE; - cache.hover_index = -1; - cache.click_index = -1; - cache.click_id = -1; - cache.click_item = nullptr; - cache.click_column = 0; - cache.hover_cell = -1; - last_keypress = 0; - focus_in_id = 0; - - blocked = 0; - - cursor_can_exit_tree = true; set_mouse_filter(MOUSE_FILTER_STOP); - drag_speed = 0; - drag_touching = false; - drag_touching_deaccel = false; - pressing_for_editor = false; - range_drag_enabled = false; - - hide_folding = false; - - drop_mode_flags = 0; - drop_mode_over = nullptr; - drop_mode_section = 0; - single_select_defer = nullptr; - - scrolling = false; - allow_rmb_select = false; - force_edit_checkbox_only_on_checkbox = false; - set_clip_contents(true); - cache.hover_item = nullptr; - cache.hover_cell = -1; - - allow_reselect = false; - propagate_mouse_activated = false; + update_cache(); } Tree::~Tree() { diff --git a/scene/gui/tree.h b/scene/gui/tree.h index c0910a8fe0..8b7ddc3faf 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -36,6 +36,7 @@ #include "scene/gui/popup_menu.h" #include "scene/gui/scroll_bar.h" #include "scene/gui/slider.h" +#include "scene/resources/text_line.h" class Tree; @@ -44,7 +45,6 @@ class TreeItem : public Object { public: enum TreeCellMode { - CELL_MODE_STRING, ///< just a string CELL_MODE_CHECK, ///< string + check CELL_MODE_RANGE, ///< Contains a range @@ -62,29 +62,40 @@ private: friend class Tree; struct Cell { - TreeCellMode mode; + TreeCellMode mode = TreeItem::CELL_MODE_STRING; Ref<Texture2D> icon; Rect2i icon_region; String text; String suffix; - double min, max, step, val; - int icon_max_w; - bool expr; - bool checked; - bool editable; - bool selected; - bool selectable; - bool custom_color; + Ref<TextLine> text_buf; + Dictionary opentype_features; + String language; + Control::StructuredTextParser st_parser = Control::STRUCTURED_TEXT_DEFAULT; + Array st_args; + Control::TextDirection text_direction = Control::TEXT_DIRECTION_INHERITED; + bool dirty = true; + double min = 0.0; + double max = 100.0; + double step = 1.0; + double val = 0.0; + int icon_max_w = 0; + bool expr = false; + bool checked = false; + bool indeterminate = false; + bool editable = false; + bool selected = false; + bool selectable = true; + bool custom_color = false; Color color; - bool custom_bg_color; - bool custom_bg_outline; + bool custom_bg_color = false; + bool custom_bg_outline = false; Color bg_color; - bool custom_button; - bool expand_right; - Color icon_color; + bool custom_button = false; + bool expand_right = false; + Color icon_color = Color(1, 1, 1); - TextAlign text_align; + TextAlign text_align = ALIGN_LEFT; Variant meta; String tooltip; @@ -93,40 +104,20 @@ private: StringName custom_draw_callback; struct Button { - int id; - bool disabled; + int id = 0; + bool disabled = false; Ref<Texture2D> texture; - Color color; + Color color = Color(1, 1, 1, 1); String tooltip; - Button() { - id = 0; - disabled = false; - color = Color(1, 1, 1, 1); - tooltip = ""; - } }; Vector<Button> buttons; + Ref<Font> custom_font; + int custom_font_size = -1; + Cell() { - custom_draw_obj = ObjectID(); - custom_button = false; - mode = TreeItem::CELL_MODE_STRING; - min = 0; - max = 100; - step = 1; - val = 0; - checked = false; - editable = false; - selected = false; - selectable = true; - custom_color = false; - custom_bg_color = false; - expr = false; - icon_max_w = 0; - text_align = ALIGN_LEFT; - expand_right = false; - icon_color = Color(1, 1, 1); + text_buf.instantiate(); } Size2 get_icon_size() const; @@ -135,14 +126,18 @@ private: Vector<Cell> cells; - bool collapsed; // won't show children - bool disable_folding; - int custom_min_height; + bool collapsed = false; // won't show children + bool disable_folding = false; + int custom_min_height = 0; - TreeItem *parent; // parent item - TreeItem *next; // next in list - TreeItem *children; //child items - Tree *tree; //tree (for reference) + TreeItem *parent = nullptr; // parent item + TreeItem *prev = nullptr; // previous in list + TreeItem *next = nullptr; // next in list + TreeItem *first_child = nullptr; + + Vector<TreeItem *> children_cache; + bool is_root = false; // for tree root + Tree *tree; // tree (for reference) TreeItem(Tree *p_tree); @@ -151,9 +146,40 @@ private: void _cell_selected(int p_cell); void _cell_deselected(int p_cell); + void _change_tree(Tree *p_tree); + + _FORCE_INLINE_ void _create_children_cache() { + if (children_cache.is_empty()) { + TreeItem *c = first_child; + while (c) { + children_cache.append(c); + c = c->next; + } + } + } + + _FORCE_INLINE_ void _unlink_from_tree() { + TreeItem *p = get_prev(); + if (p) { + p->next = next; + } + if (next) { + next->prev = p; + } + if (parent) { + if (!parent->children_cache.is_empty()) { + parent->children_cache.remove(get_index()); + } + if (parent->first_child == this) { + parent->first_child = next; + } + } + } + protected: static void _bind_methods(); - //bind helpers + + // Bind helpers Dictionary _get_range_config(int p_column) { Dictionary d; double min = 0.0, max = 0.0, step = 0.0; @@ -169,6 +195,13 @@ protected: remove_child(Object::cast_to<TreeItem>(p_child)); } + void _move_before(Object *p_item) { + move_before(Object::cast_to<TreeItem>(p_item)); + } + void _move_after(Object *p_item) { + move_after(Object::cast_to<TreeItem>(p_item)); + } + Variant _call_recursive_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error); public: @@ -178,11 +211,29 @@ 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; + void set_text_direction(int p_column, Control::TextDirection p_text_direction); + Control::TextDirection get_text_direction(int p_column) const; + + void set_opentype_feature(int p_column, const String &p_name, int p_value); + int get_opentype_feature(int p_column, const String &p_name) const; + void clear_opentype_features(int p_column); + + void set_structured_text_bidi_override(int p_column, Control::StructuredTextParser p_parser); + Control::StructuredTextParser get_structured_text_bidi_override(int p_column) const; + + void set_structured_text_bidi_override_options(int p_column, Array p_args); + Array get_structured_text_bidi_override_options(int p_column) const; + + void set_language(int p_column, const String &p_language); + String get_language(int p_column) const; + void set_suffix(int p_column, String p_suffix); String get_suffix(int p_column) const; @@ -202,7 +253,6 @@ public: int get_button_count(int p_column) const; String get_button_tooltip(int p_column, int p_idx) const; Ref<Texture2D> get_button(int p_column, int p_idx) const; - int get_button_id(int p_column, int p_idx) const; void erase_button(int p_column, int p_idx); int get_button_by_id(int p_column, int p_id) const; void set_button(int p_column, int p_idx, const Ref<Texture2D> &p_button); @@ -227,19 +277,11 @@ public: void set_collapsed(bool p_collapsed); bool is_collapsed(); + void uncollapse_tree(); + void set_custom_minimum_height(int p_height); int get_custom_minimum_height() const; - TreeItem *get_prev(); - TreeItem *get_next(); - TreeItem *get_parent(); - TreeItem *get_children(); - - TreeItem *get_prev_visible(bool p_wrap = false); - TreeItem *get_next_visible(bool p_wrap = false); - - void remove_child(TreeItem *p_item); - void set_selectable(int p_column, bool p_selectable); bool is_selectable(int p_column) const; @@ -255,6 +297,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; @@ -265,22 +313,45 @@ public: void set_tooltip(int p_column, const String &p_tooltip); String get_tooltip(int p_column) const; - void clear_children(); - void set_text_align(int p_column, TextAlign p_align); TextAlign get_text_align(int p_column) const; void set_expand_right(int p_column, bool p_enable); bool get_expand_right(int p_column) const; - void move_to_top(); - void move_to_bottom(); - 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() const; + + TreeItem *get_prev(); + 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); + + TreeItem *get_child(int p_idx); + int get_child_count(); + Array get_children(); + int get_index(); + + void move_before(TreeItem *p_item); + void move_after(TreeItem *p_item); + + void remove_child(TreeItem *p_item); + void call_recursive(const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error); + void clear_children(); + ~TreeItem(); }; @@ -308,89 +379,95 @@ public: private: friend class TreeItem; - TreeItem *root; - TreeItem *popup_edited_item; - TreeItem *selected_item; - TreeItem *edited_item; + TreeItem *root = nullptr; + TreeItem *popup_edited_item = nullptr; + TreeItem *selected_item = nullptr; + TreeItem *edited_item = nullptr; - TreeItem *drop_mode_over; - int drop_mode_section; + TreeItem *drop_mode_over = nullptr; + int drop_mode_section = 0; - TreeItem *single_select_defer; - int single_select_defer_column; + TreeItem *single_select_defer = nullptr; + int single_select_defer_column = 0; - int pressed_button; - bool pressing_for_editor; + int pressed_button = -1; + bool pressing_for_editor = false; String pressing_for_editor_text; Vector2 pressing_pos; Rect2 pressing_item_rect; - float range_drag_base; - bool range_drag_enabled; + float range_drag_base = 0.0; + bool range_drag_enabled = false; Vector2 range_drag_capture_pos; - bool propagate_mouse_activated; + bool propagate_mouse_activated = false; //TreeItem *cursor_item; //int cursor_column; Rect2 custom_popup_rect; - int edited_col; - int selected_col; - int popup_edited_item_col; - bool hide_root; - SelectMode select_mode; + int edited_col = -1; + int selected_col = -1; + int popup_edited_item_col = -1; + bool hide_root = false; + SelectMode select_mode = SELECT_SINGLE; - int blocked; + int blocked = 0; - int drop_mode_flags; + int drop_mode_flags = 0; struct ColumnInfo { - int min_width; - bool expand; + 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() { - min_width = 1; - expand = true; + text_buf.instantiate(); } }; - bool show_column_titles; + bool show_column_titles = false; VBoxContainer *popup_editor_vb; Popup *popup_editor; - LineEdit *text_editor; + LineEdit *text_editor = nullptr; HSlider *value_editor; - bool updating_value_editor; - uint64_t focus_in_id; - PopupMenu *popup_menu; + bool updating_value_editor = false; + uint64_t focus_in_id = 0; + PopupMenu *popup_menu = nullptr; Vector<ColumnInfo> columns; Timer *range_click_timer; - TreeItem *range_item_last; - bool range_up_last; + TreeItem *range_item_last = nullptr; + bool range_up_last = false; void _range_click_timeout(); int compute_item_height(TreeItem *p_item) const; int get_item_height(TreeItem *p_item) const; + void _update_all(); + void update_column(int p_col); + void update_item_cell(TreeItem *p_item, int p_col); + void update_item_cache(TreeItem *p_item); //void draw_item_text(String p_text,const Ref<Texture2D>& p_icon,int p_icon_max_w,bool p_tool,Rect2i p_rect,const Color& p_color); - void draw_item_rect(const TreeItem::Cell &p_cell, const Rect2i &p_rect, const Color &p_color, const Color &p_icon_color); + 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_doubleclick, 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); @@ -401,6 +478,8 @@ private: struct Cache { Ref<Font> font; Ref<Font> tb_font; + int font_size = 0; + int tb_font_size = 0; Ref<StyleBox> bg; Ref<StyleBox> selected; Ref<StyleBox> selected_focus; @@ -418,27 +497,36 @@ private: Ref<Texture2D> checked; Ref<Texture2D> unchecked; + Ref<Texture2D> indeterminate; Ref<Texture2D> arrow_collapsed; Ref<Texture2D> arrow; Ref<Texture2D> select_arrow; Ref<Texture2D> updown; Color font_color; - Color font_color_selected; + Color font_selected_color; Color guide_color; Color drop_position_color; Color relationship_line_color; + Color parent_hl_line_color; + Color children_hl_line_color; Color custom_button_font_highlight; + Color font_outline_color; - int hseparation; - int vseparation; - int item_margin; - int button_margin; + int hseparation = 0; + int vseparation = 0; + int item_margin = 0; + int button_margin = 0; Point2 offset; - int draw_relationship_lines; - int draw_guides; - int scroll_border; - int scroll_speed; + int draw_relationship_lines = 0; + int relationship_line_width = 0; + int parent_hl_line_width = 0; + int children_hl_line_width = 0; + int parent_hl_line_margin = 0; + int draw_guides = 0; + int scroll_border = 0; + int scroll_speed = 0; + int font_outline_size = 0; enum ClickType { CLICK_NONE, @@ -447,20 +535,22 @@ private: }; - ClickType click_type; - ClickType hover_type; - int click_index; - int click_id; - TreeItem *click_item; - int click_column; - int hover_index; + ClickType click_type = Cache::CLICK_NONE; + ClickType hover_type = Cache::CLICK_NONE; + int click_index = -1; + int click_id = -1; + TreeItem *click_item = nullptr; + int click_column = 0; + int hover_index = -1; Point2 click_pos; - TreeItem *hover_item; - int hover_cell; + TreeItem *hover_item = nullptr; + int hover_cell = -1; Point2i text_editor_position; + bool rtl = false; + } cache; int _get_title_button_height() const; @@ -469,15 +559,18 @@ 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(); Rect2 search_item_rect(TreeItem *p_from, TreeItem *p_item); //Rect2 get_item_rect(TreeItem *p_item); - uint64_t last_keypress; + uint64_t last_keypress = 0; String incr_search; - bool cursor_can_exit_tree; + bool cursor_can_exit_tree = true; void _do_incr_search(const String &p_add); TreeItem *_search_item_text(TreeItem *p_at, const String &p_find, int *r_col, bool p_selectable, bool p_backwards = false); @@ -491,23 +584,25 @@ private: float last_drag_time; float time_since_motion;*/ - float drag_speed; - float drag_from; - float drag_accum; + float drag_speed = 0.0; + float drag_from = 0.0; + float drag_accum = 0.0; Vector2 last_speed; - bool drag_touching; - bool drag_touching_deaccel; - bool click_handled; - bool allow_rmb_select; - bool scrolling; + bool drag_touching = false; + bool drag_touching_deaccel = false; + bool click_handled = false; + bool allow_rmb_select = false; + bool scrolling = false; - bool allow_reselect; + bool allow_reselect = false; - bool force_edit_checkbox_only_on_checkbox; + bool force_edit_checkbox_only_on_checkbox = false; - bool hide_folding; + bool hide_folding = false; int _count_selected_items(TreeItem *p_from) const; + bool _is_branch_selected(TreeItem *p_from) const; + bool _is_sibling_branch_selected(TreeItem *p_from) const; void _go_left(); void _go_right(); void _go_down(); @@ -529,7 +624,13 @@ protected: return get_item_rect(Object::cast_to<TreeItem>(p_item), p_column); } + void _scroll_to_item(Object *p_item) { + scroll_to_item(Object::cast_to<TreeItem>(p_item)); + } + 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; @@ -540,12 +641,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; @@ -564,6 +672,16 @@ public: void set_column_title(int p_column, const String &p_title); String get_column_title(int p_column) const; + void set_column_title_direction(int p_column, Control::TextDirection p_text_direction); + Control::TextDirection get_column_title_direction(int p_column) const; + + void set_column_title_opentype_feature(int p_column, const String &p_name, int p_value); + int get_column_title_opentype_feature(int p_column, const String &p_name) const; + void clear_column_title_opentype_features(int p_column); + + void set_column_title_language(int p_column, const String &p_language); + String get_column_title_language(int p_column) const; + void set_column_titles_visible(bool p_show); bool are_column_titles_visible() const; @@ -577,6 +695,7 @@ public: int get_item_offset(TreeItem *p_item) const; Rect2 get_item_rect(TreeItem *p_item, int p_column = -1) const; bool edit_selected(); + bool is_editing(); // First item that starts with the text, from the current focused item down and wraps around. TreeItem *search_item_text(const String &p_find, int *r_col = nullptr, bool p_selectable = false); @@ -585,9 +704,12 @@ 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); - bool can_cursor_exit_tree() const; VScrollBar *get_vscroll_bar() { return v_scroll; } @@ -606,6 +728,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 e118cb0d8d..8734037a57 100644 --- a/scene/gui/video_player.cpp +++ b/scene/gui/video_player.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -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,35 +451,17 @@ 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"); } -VideoPlayer::VideoPlayer() { - volume = 1; - loops = false; - paused = false; - autoplay = false; - expand = true; - - audio_track = 0; - bus_index = 0; - - buffering_ms = 500; - - // internal_stream.player=this; - // stream_rid=AudioServer::get_singleton()->audio_stream_create(&internal_stream); - last_audio_time = 0; - - wait_resampler = 0; - wait_resampler_limit = 2; -}; +VideoPlayer::VideoPlayer() {} VideoPlayer::~VideoPlayer() { // if (stream_rid.is_valid()) diff --git a/scene/gui/video_player.h b/scene/gui/video_player.h index 573aec5a2c..0edad296a1 100644 --- a/scene/gui/video_player.h +++ b/scene/gui/video_player.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* 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 */ @@ -41,8 +41,8 @@ class VideoPlayer : public Control { struct Output { AudioFrame vol; - int bus_index; - Viewport *viewport; //pointer only used for reference to previous mix + int bus_index = 0; + Viewport *viewport = nullptr; //pointer only used for reference to previous mix }; Ref<VideoStreamPlayback> playback; Ref<VideoStream> stream; @@ -56,17 +56,18 @@ class VideoPlayer : public Control { AudioRBResampler resampler; Vector<AudioFrame> mix_buffer; - int wait_resampler, wait_resampler_limit; - - bool paused; - bool autoplay; - float volume; - double last_audio_time; - bool expand; - bool loops; - int buffering_ms; - int audio_track; - int bus_index; + int wait_resampler = 0; + int wait_resampler_limit = 2; + + bool paused = false; + bool autoplay = false; + float volume = 1.0; + double last_audio_time = 0.0; + bool expand = true; + bool loops = false; + int buffering_ms = 500; + int audio_track = 0; + int bus_index = 0; StringName bus; |