diff options
Diffstat (limited to 'scene/gui')
104 files changed, 8802 insertions, 4930 deletions
diff --git a/scene/gui/aspect_ratio_container.cpp b/scene/gui/aspect_ratio_container.cpp index 9526c5e793..75f19ac452 100644 --- a/scene/gui/aspect_ratio_container.cpp +++ b/scene/gui/aspect_ratio_container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -70,6 +70,24 @@ void AspectRatioContainer::set_alignment_vertical(AlignmentMode p_alignment_vert queue_sort(); } +Vector<int> AspectRatioContainer::get_allowed_size_flags_horizontal() const { + Vector<int> flags; + flags.append(SIZE_FILL); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + +Vector<int> AspectRatioContainer::get_allowed_size_flags_vertical() const { + Vector<int> flags; + flags.append(SIZE_FILL); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + void AspectRatioContainer::_notification(int p_what) { switch (p_what) { case NOTIFICATION_SORT_CHILDREN: { @@ -154,7 +172,7 @@ void AspectRatioContainer::_bind_methods() { 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::FLOAT, "ratio", PROPERTY_HINT_RANGE, "0.001,10.0,0.0001,or_greater"), "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_"); diff --git a/scene/gui/aspect_ratio_container.h b/scene/gui/aspect_ratio_container.h index 3cdfc75734..6740e2f329 100644 --- a/scene/gui/aspect_ratio_container.h +++ b/scene/gui/aspect_ratio_container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -72,6 +72,9 @@ public: void set_alignment_vertical(AlignmentMode p_alignment_vertical); AlignmentMode get_alignment_vertical() const { return alignment_vertical; } + + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; }; VARIANT_ENUM_CAST(AspectRatioContainer::StretchMode); diff --git a/scene/gui/base_button.cpp b/scene/gui/base_button.cpp index f4de48689e..776623f7ce 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,12 @@ void BaseButton::_unpress_group() { status.pressed = true; } - for (Set<BaseButton *>::Element *E = button_group->buttons.front(); E; E = E->next()) { - if (E->get() == this) { + for (BaseButton *E : button_group->buttons) { + if (E == this) { continue; } - E->get()->set_pressed(false); + E->set_pressed(false); } } @@ -81,42 +81,50 @@ void BaseButton::gui_input(const Ref<InputEvent> &p_event) { } void BaseButton::_notification(int p_what) { - if (p_what == NOTIFICATION_MOUSE_ENTER) { - status.hovering = true; - update(); - } + switch (p_what) { + case NOTIFICATION_MOUSE_ENTER: { + status.hovering = true; + update(); + } break; - if (p_what == NOTIFICATION_MOUSE_EXIT) { - status.hovering = false; - update(); - } - if (p_what == NOTIFICATION_DRAG_BEGIN || p_what == NOTIFICATION_SCROLL_BEGIN) { - if (status.press_attempt) { - status.press_attempt = false; + case NOTIFICATION_MOUSE_EXIT: { + status.hovering = false; update(); - } - } + } break; - if (p_what == NOTIFICATION_FOCUS_ENTER) { - update(); - } + case NOTIFICATION_DRAG_BEGIN: + case NOTIFICATION_SCROLL_BEGIN: { + if (status.press_attempt) { + status.press_attempt = false; + update(); + } + } break; - if (p_what == NOTIFICATION_FOCUS_EXIT) { - if (status.press_attempt) { - status.press_attempt = false; + case NOTIFICATION_FOCUS_ENTER: { update(); - } else if (status.hovering) { - update(); - } - } + } break; - if (p_what == NOTIFICATION_EXIT_TREE || (p_what == NOTIFICATION_VISIBILITY_CHANGED && !is_visible_in_tree())) { - if (!toggle_mode) { - status.pressed = false; - } - status.hovering = false; - status.press_attempt = false; - status.pressing_inside = false; + case NOTIFICATION_FOCUS_EXIT: { + if (status.press_attempt) { + status.press_attempt = false; + update(); + } else if (status.hovering) { + update(); + } + } break; + + case NOTIFICATION_VISIBILITY_CHANGED: + case NOTIFICATION_EXIT_TREE: { + if (p_what == NOTIFICATION_VISIBILITY_CHANGED && is_visible_in_tree()) { + break; + } + if (!toggle_mode) { + status.pressed = false; + } + status.hovering = false; + status.press_attempt = false; + status.pressing_inside = false; + } break; } } @@ -331,17 +339,17 @@ bool BaseButton::is_keep_pressed_outside() const { void BaseButton::set_shortcut(const Ref<Shortcut> &p_shortcut) { shortcut = p_shortcut; - set_process_unhandled_key_input(shortcut.is_valid()); + set_process_shortcut_input(shortcut.is_valid()); } Ref<Shortcut> BaseButton::get_shortcut() const { return shortcut; } -void BaseButton::unhandled_key_input(const Ref<InputEvent> &p_event) { +void BaseButton::shortcut_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); - if (!_is_focus_owner_in_shorcut_context()) { + if (!_is_focus_owner_in_shortcut_context()) { return; } @@ -356,7 +364,7 @@ String BaseButton::get_tooltip(const Point2 &p_pos) const { if (shortcut_in_tooltip && shortcut.is_valid() && shortcut->has_valid_event()) { String text = shortcut->get_name() + " (" + shortcut->get_as_text() + ")"; if (!tooltip.is_empty() && shortcut->get_name().nocasecmp_to(tooltip) != 0) { - text += "\n" + tooltip; + text += "\n" + atr(tooltip); } tooltip = text; } @@ -382,8 +390,11 @@ Ref<ButtonGroup> BaseButton::get_button_group() const { } 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(); + if (p_node != nullptr) { + shortcut_context = p_node->get_instance_id(); + } else { + shortcut_context = ObjectID(); + } } Node *BaseButton::get_shortcut_context() const { @@ -393,14 +404,14 @@ Node *BaseButton::get_shortcut_context() const { return ctx_node; } -bool BaseButton::_is_focus_owner_in_shorcut_context() const { +bool BaseButton::_is_focus_owner_in_shortcut_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(); + Control *vp_focus = get_viewport() ? get_viewport()->gui_get_focus_owner() : nullptr; // 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)); @@ -441,10 +452,11 @@ void BaseButton::_bind_methods() { ADD_SIGNAL(MethodInfo("button_up")); ADD_SIGNAL(MethodInfo("button_down")); ADD_SIGNAL(MethodInfo("toggled", PropertyInfo(Variant::BOOL, "button_pressed"))); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "disabled"), "set_disabled", "is_disabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "toggle_mode"), "set_toggle_mode", "is_toggle_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "shortcut_in_tooltip"), "set_shortcut_in_tooltip", "is_shortcut_in_tooltip_enabled"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "pressed"), "set_pressed", "is_pressed"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "button_pressed"), "set_pressed", "is_pressed"); ADD_PROPERTY(PropertyInfo(Variant::INT, "action_mode", PROPERTY_HINT_ENUM, "Button Press,Button Release"), "set_action_mode", "get_action_mode"); ADD_PROPERTY(PropertyInfo(Variant::INT, "button_mask", PROPERTY_HINT_FLAGS, "Mouse Left, Mouse Right, Mouse Middle"), "set_button_mask", "get_button_mask"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "keep_pressed_outside"), "set_keep_pressed_outside", "is_keep_pressed_outside"); @@ -473,24 +485,24 @@ BaseButton::~BaseButton() { } void ButtonGroup::get_buttons(List<BaseButton *> *r_buttons) { - for (Set<BaseButton *>::Element *E = buttons.front(); E; E = E->next()) { - r_buttons->push_back(E->get()); + for (BaseButton *E : buttons) { + r_buttons->push_back(E); } } Array ButtonGroup::_get_buttons() { Array btns; - for (Set<BaseButton *>::Element *E = buttons.front(); E; E = E->next()) { - btns.push_back(E->get()); + for (const BaseButton *E : buttons) { + btns.push_back(E); } return btns; } BaseButton *ButtonGroup::get_pressed_button() { - for (Set<BaseButton *>::Element *E = buttons.front(); E; E = E->next()) { - if (E->get()->is_pressed()) { - return E->get(); + for (BaseButton *E : buttons) { + if (E->is_pressed()) { + return E; } } @@ -500,7 +512,8 @@ 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"))); + + ADD_SIGNAL(MethodInfo("pressed", PropertyInfo(Variant::OBJECT, "button", PROPERTY_HINT_RESOURCE_TYPE, "BaseButton"))); } ButtonGroup::ButtonGroup() { diff --git a/scene/gui/base_button.h b/scene/gui/base_button.h index 3ea59c3ff9..ba3852ec98 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 @@ #ifndef BASE_BUTTON_H #define BASE_BUTTON_H +#include "core/input/shortcut.h" #include "scene/gui/control.h" class ButtonGroup; @@ -76,10 +77,10 @@ protected: virtual void toggled(bool p_pressed); static void _bind_methods(); virtual void gui_input(const Ref<InputEvent> &p_event) override; - virtual void unhandled_key_input(const Ref<InputEvent> &p_event) override; + virtual void shortcut_input(const Ref<InputEvent> &p_event) override; void _notification(int p_what); - bool _is_focus_owner_in_shorcut_context() const; + bool _is_focus_owner_in_shortcut_context() const; GDVIRTUAL0(_pressed) GDVIRTUAL1(_toggled, bool) @@ -142,7 +143,7 @@ VARIANT_ENUM_CAST(BaseButton::ActionMode) class ButtonGroup : public Resource { GDCLASS(ButtonGroup, Resource); friend class BaseButton; - Set<BaseButton *> buttons; + HashSet<BaseButton *> buttons; protected: static void _bind_methods(); diff --git a/scene/gui/box_container.cpp b/scene/gui/box_container.cpp index 6aaa8433ec..df695feba8 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +29,7 @@ /*************************************************************************/ #include "box_container.h" + #include "label.h" #include "margin_container.h" @@ -51,7 +52,7 @@ void BoxContainer::_resort() { int stretch_min = 0; int stretch_avail = 0; float stretch_ratio_total = 0.0; - Map<Control *, _MinSizeCache> min_size_cache; + HashMap<Control *, _MinSizeCache> min_size_cache; for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); @@ -294,9 +295,11 @@ void BoxContainer::_notification(int p_what) { case NOTIFICATION_SORT_CHILDREN: { _resort(); } break; + case NOTIFICATION_THEME_CHANGED: { update_minimum_size(); } break; + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { queue_sort(); @@ -331,6 +334,30 @@ Control *BoxContainer::add_spacer(bool p_begin) { return c; } +Vector<int> BoxContainer::get_allowed_size_flags_horizontal() const { + Vector<int> flags; + flags.append(SIZE_FILL); + if (!vertical) { + flags.append(SIZE_EXPAND); + } + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + +Vector<int> BoxContainer::get_allowed_size_flags_vertical() const { + Vector<int> flags; + flags.append(SIZE_FILL); + if (vertical) { + flags.append(SIZE_EXPAND); + } + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + BoxContainer::BoxContainer(bool p_vertical) { vertical = p_vertical; } diff --git a/scene/gui/box_container.h b/scene/gui/box_container.h index 9bb26ec32c..3043c3ea45 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -62,6 +62,9 @@ public: virtual Size2 get_minimum_size() const override; + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + BoxContainer(bool p_vertical = false); }; diff --git a/scene/gui/button.cpp b/scene/gui/button.cpp index 8bb41e7abf..c54897035a 100644 --- a/scene/gui/button.cpp +++ b/scene/gui/button.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -53,18 +53,18 @@ Size2 Button::get_minimum_size() const { if (icon_alignment != HORIZONTAL_ALIGNMENT_CENTER) { minsize.width += _icon->get_width(); if (!xl_text.is_empty()) { - minsize.width += get_theme_constant(SNAME("hseparation")); + minsize.width += get_theme_constant(SNAME("h_separation")); } } else { minsize.width = MAX(minsize.width, _icon->get_width()); } } } - - 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); + if (!xl_text.is_empty()) { + 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; } @@ -78,6 +78,7 @@ void Button::_notification(int p_what) { case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { update(); } break; + case NOTIFICATION_TRANSLATION_CHANGED: { xl_text = atr(text); _shape(); @@ -85,12 +86,14 @@ void Button::_notification(int p_what) { update_minimum_size(); update(); } break; + case NOTIFICATION_THEME_CHANGED: { _shape(); update_minimum_size(); update(); } break; + case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); Size2 size = get_size(); @@ -126,7 +129,8 @@ void Button::_notification(int p_what) { } } break; case DRAW_HOVER_PRESSED: { - if (has_theme_stylebox(SNAME("hover_pressed")) && has_theme_stylebox_override("hover_pressed")) { + // Edge case for CheckButton and CheckBox. + if (has_theme_stylebox("hover_pressed")) { if (rtl && has_theme_stylebox(SNAME("hover_pressed_mirrored"))) { style = get_theme_stylebox(SNAME("hover_pressed_mirrored")); } else { @@ -138,8 +142,6 @@ void Button::_notification(int p_what) { } if (has_theme_color(SNAME("font_hover_pressed_color"))) { color = get_theme_color(SNAME("font_hover_pressed_color")); - } else { - color = get_theme_color(SNAME("font_color")); } if (has_theme_color(SNAME("icon_hover_pressed_color"))) { color_icon = get_theme_color(SNAME("icon_hover_pressed_color")); @@ -198,6 +200,8 @@ void Button::_notification(int p_what) { 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")); + } else { + color_icon.a = 0.4; } } break; @@ -233,9 +237,6 @@ void Button::_notification(int p_what) { } 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.0; Point2 style_offset; @@ -243,21 +244,22 @@ void Button::_notification(int p_what) { if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_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")); + icon_ofs_region = _internal_margin[SIDE_LEFT] + get_theme_constant(SNAME("h_separation")); } } else if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER) { style_offset.x = 0.0; } else if (icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_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")); + icon_ofs_region = -_internal_margin[SIDE_RIGHT] - get_theme_constant(SNAME("h_separation")); } } style_offset.y = style->get_margin(SIDE_TOP); if (expand_icon) { Size2 _size = get_size() - style->get_offset() * 2; - _size.width -= get_theme_constant(SNAME("hseparation")) + icon_ofs_region; + int icon_text_separation = text.is_empty() ? 0 : get_theme_constant(SNAME("h_separation")); + _size.width -= icon_text_separation + icon_ofs_region; if (!clip_text && icon_align_rtl_checked != HORIZONTAL_ALIGNMENT_CENTER) { _size.width -= text_buf->get_size().width; } @@ -285,24 +287,26 @@ void Button::_notification(int p_what) { } } - Point2 icon_ofs = !_icon.is_null() ? Point2(icon_region.size.width + get_theme_constant(SNAME("hseparation")), 0) : Point2(); + Point2 icon_ofs = !_icon.is_null() ? Point2(icon_region.size.width + get_theme_constant(SNAME("h_separation")), 0) : Point2(); if (align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER && icon_align_rtl_checked == HORIZONTAL_ALIGNMENT_CENTER) { icon_ofs.x = 0.0; } int text_clip = size.width - style->get_minimum_size().width - icon_ofs.width; text_buf->set_width(clip_text ? text_clip : -1); - int text_width = clip_text ? MIN(text_clip, text_buf->get_size().x) : text_buf->get_size().x; + int text_width = MAX(1, 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")); + text_clip -= _internal_margin[SIDE_LEFT] + get_theme_constant(SNAME("h_separation")); } if (_internal_margin[SIDE_RIGHT] > 0) { - text_clip -= _internal_margin[SIDE_RIGHT] + get_theme_constant(SNAME("hseparation")); + text_clip -= _internal_margin[SIDE_RIGHT] + get_theme_constant(SNAME("h_separation")); } 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; + text_buf->set_alignment(align_rtl_checked); + text_buf->set_width(text_width); switch (align_rtl_checked) { case HORIZONTAL_ALIGNMENT_FILL: case HORIZONTAL_ALIGNMENT_LEFT: { @@ -310,7 +314,7 @@ void Button::_notification(int p_what) { 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")); + text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x + _internal_margin[SIDE_LEFT] + get_theme_constant(SNAME("h_separation")); } else { text_ofs.x = style->get_margin(SIDE_LEFT) + icon_ofs.x; } @@ -327,7 +331,7 @@ void Button::_notification(int p_what) { } break; case HORIZONTAL_ALIGNMENT_RIGHT: { 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")); + text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width - _internal_margin[SIDE_RIGHT] - get_theme_constant(SNAME("h_separation")); } else { text_ofs.x = size.x - style->get_margin(SIDE_RIGHT) - text_width; } @@ -498,7 +502,7 @@ bool Button::_set(const StringName &p_name, const Variant &p_value) { if (str.begins_with("opentype_features/")) { String name = str.get_slicec('/', 1); int32_t tag = TS->name_to_tag(name); - double value = p_value; + int value = p_value; if (value == -1) { if (opentype_features.has(tag)) { opentype_features.erase(tag); @@ -506,7 +510,7 @@ bool Button::_set(const StringName &p_name, const Variant &p_value) { update(); } } else { - if ((double)opentype_features[tag] != value) { + if (!opentype_features.has(tag) || (int)opentype_features[tag] != value) { opentype_features[tag] = value; _shape(); update(); @@ -538,7 +542,7 @@ bool Button::_get(const StringName &p_name, Variant &r_ret) const { 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::INT, "opentype_features/" + name)); } p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); } @@ -568,7 +572,7 @@ void Button::_bind_methods() { 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::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "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"); @@ -580,8 +584,8 @@ void Button::_bind_methods() { Button::Button(const String &p_text) { text_buf.instantiate(); text_buf->set_flags(TextServer::BREAK_MANDATORY); - set_mouse_filter(MOUSE_FILTER_STOP); + set_text(p_text); } diff --git a/scene/gui/button.h b/scene/gui/button.h index 92d86542a6..1abf86c986 100644 --- a/scene/gui/button.h +++ b/scene/gui/button.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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/center_container.cpp b/scene/gui/center_container.cpp index e17552006f..33ec4006ae 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -69,22 +69,32 @@ bool CenterContainer::is_using_top_left() const { return use_top_left; } +Vector<int> CenterContainer::get_allowed_size_flags_horizontal() const { + return Vector<int>(); +} + +Vector<int> CenterContainer::get_allowed_size_flags_vertical() const { + return Vector<int>(); +} + void CenterContainer::_notification(int p_what) { - if (p_what == NOTIFICATION_SORT_CHILDREN) { - 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; - } + switch (p_what) { + case NOTIFICATION_SORT_CHILDREN: { + 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 minsize = c->get_combined_minimum_size(); - Point2 ofs = use_top_left ? (-minsize * 0.5).floor() : ((size - minsize) / 2.0).floor(); - fit_child_in_rect(c, Rect2(ofs, minsize)); - } + Size2 minsize = c->get_combined_minimum_size(); + Point2 ofs = use_top_left ? (-minsize * 0.5).floor() : ((size - minsize) / 2.0).floor(); + fit_child_in_rect(c, Rect2(ofs, minsize)); + } + } break; } } diff --git a/scene/gui/center_container.h b/scene/gui/center_container.h index 0944f200fc..c35e0c4e29 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -48,6 +48,9 @@ public: virtual Size2 get_minimum_size() const override; + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + CenterContainer(); }; diff --git a/scene/gui/check_box.cpp b/scene/gui/check_box.cpp index 134766b05f..cb80f5b5ef 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -75,7 +75,7 @@ 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(SNAME("hseparation")); + minsize.width += get_theme_constant(SNAME("h_separation")); } 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)); @@ -84,34 +84,40 @@ Size2 CheckBox::get_minimum_size() const { } void CheckBox::_notification(int p_what) { - 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(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; - 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); - } else { - off->draw(ci, ofs); - } + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case 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); + } + } break; + + case NOTIFICATION_DRAW: { + RID ci = get_canvas_item(); + + 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; + 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_v_adjust")); + + if (is_pressed()) { + on->draw(ci, ofs); + } else { + off->draw(ci, ofs); + } + } break; } } diff --git a/scene/gui/check_box.h b/scene/gui/check_box.h index 9fb0aea218..fcdb2ce08c 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,9 +32,7 @@ #define CHECK_BOX_H #include "scene/gui/button.h" -/** -@author Mariano Suligoy <marianognu.esyrpg@gmail.com> -*/ + class CheckBox : public Button { GDCLASS(CheckBox, Button); @@ -50,4 +48,4 @@ public: ~CheckBox(); }; -#endif +#endif // CHECK_BOX_H diff --git a/scene/gui/check_button.cpp b/scene/gui/check_button.cpp index 499bedda52..a09873ea4f 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,7 @@ 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(SNAME("hseparation")); + minsize.width += get_theme_constant(SNAME("h_separation")); } 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)); @@ -61,53 +61,62 @@ Size2 CheckButton::get_minimum_size() const { } void CheckButton::_notification(int p_what) { - 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; - 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(SNAME("normal")); - Vector2 ofs; - Size2 tex_size = get_icon_size(); - - 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); - } else { - off->draw(ci, ofs); - } + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case 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); + } + } break; + + case NOTIFICATION_DRAW: { + RID ci = get_canvas_item(); + bool rtl = is_layout_rtl(); + + 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(SNAME("normal")); + Vector2 ofs; + Size2 tex_size = get_icon_size(); + + 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_v_adjust")); + + if (is_pressed()) { + on->draw(ci, ofs); + } else { + off->draw(ci, ofs); + } + } break; } } -CheckButton::CheckButton() { +CheckButton::CheckButton(const String &p_text) : + Button(p_text) { set_toggle_mode(true); + set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT); + if (is_layout_rtl()) { _set_internal_margin(SIDE_LEFT, get_icon_size().width); } else { diff --git a/scene/gui/check_button.h b/scene/gui/check_button.h index 29c557ce89..7d4bb8bdfc 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,9 +32,7 @@ #define CHECK_BUTTON_H #include "scene/gui/button.h" -/** -@author Juan Linietsky <reduzio@gmail.com> -*/ + class CheckButton : public Button { GDCLASS(CheckButton, Button); @@ -44,8 +42,8 @@ protected: void _notification(int p_what); public: - CheckButton(); + CheckButton(const String &p_text = String()); ~CheckButton(); }; -#endif +#endif // CHECK_BUTTON_H diff --git a/scene/gui/code_edit.cpp b/scene/gui/code_edit.cpp index ba37d51e24..22e9763929 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,14 +34,6 @@ #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: @@ -82,6 +74,7 @@ void CodeEdit::_notification(int p_what) { 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(); @@ -92,7 +85,7 @@ void CodeEdit::_notification(int p_what) { 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; + const float char_size = 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(); @@ -113,7 +106,7 @@ void CodeEdit::_notification(int p_what) { 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 int icon_hsep = get_theme_constant(SNAME("h_separation"), 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; @@ -143,7 +136,6 @@ void CodeEdit::_notification(int p_what) { 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; @@ -177,6 +169,17 @@ void CodeEdit::_notification(int p_what) { } tl->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT); } + + Point2 match_pos = Point2(code_completion_rect.position.x + icon_area_size.x + icon_hsep, code_completion_rect.position.y + i * row_height); + + for (int j = 0; j < code_completion_options[l].matches.size(); j++) { + Pair<int, int> match = code_completion_options[l].matches[j]; + int match_offset = font->get_string_size(code_completion_options[l].display.substr(0, match.first), font_size).width; + int match_len = font->get_string_size(code_completion_options[l].display.substr(match.first, match.second), font_size).width; + + draw_rect(Rect2(match_pos + Point2(match_offset, 0), Size2(match_len, row_height)), code_completion_existing_color); + } + tl->draw(ci, title_pos, code_completion_options[l].font_color); } @@ -222,7 +225,7 @@ void CodeEdit::_notification(int p_what) { int begin = 0; int end = 0; - if (line.find(String::chr(0xFFFF)) != -1) { + if (line.contains(String::chr(0xFFFF))) { 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; } @@ -354,6 +357,11 @@ void CodeEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } Ref<InputEventKey> k = p_gui_input; + if (TextEdit::alt_input(p_gui_input)) { + accept_event(); + return; + } + bool update_code_completion = false; if (!k.is_valid()) { TextEdit::gui_input(p_gui_input); @@ -561,6 +569,8 @@ Control::CursorShape CodeEdit::get_cursor_shape(const Point2 &p_pos) const { // Overridable actions void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { bool had_selection = has_selection(); + String selection_text = (had_selection ? get_selected_text() : ""); + if (had_selection) { begin_complex_operation(); delete_selection(); @@ -581,27 +591,38 @@ void CodeEdit::_handle_unicode_input_internal(const uint32_t p_unicode) { 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))) { + if (had_selection) { insert_text_at_caret(chr); + + String close_key = get_auto_brace_completion_close_key(chr); + if (!close_key.is_empty()) { + insert_text_at_caret(selection_text + close_key); + set_caret_column(get_caret_column() - 1); + } } else { - insert_text_at_caret(chr); + 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_symbol(get_line(cl)[cc - 1]) && post_brace_pair == -1) { + insert_text_at_caret(chr); + } else if (cc < get_line(cl).length() && !is_symbol(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); + 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); } - set_caret_column(cc + caret_move_offset); } else { insert_text_at_caret(chr); } @@ -719,8 +740,8 @@ void CodeEdit::set_auto_indent_prefixes(const TypedArray<String> &p_prefixes) { 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())); + for (const char32_t &E : auto_indent_prefixes) { + prefixes.push_back(String::chr(E)); } return prefixes; } @@ -927,8 +948,10 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { return; } - const int cc = get_caret_column(); + /* When not splitting the line, we need to factor in indentation from the end of the current line. */ + const int cc = p_split_current_line ? get_caret_column() : get_line(get_caret_line()).length(); const int cl = get_caret_line(); + const String line = get_line(cl); String ins = "\n"; @@ -976,7 +999,8 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { } /* 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)) { + /* Increment column for comments because the delimiter (#) should be ignored. */ + if (should_indent && (!is_whitespace(c) && is_in_comment(cl, line_col + 1) == -1)) { should_indent = false; } } @@ -1002,6 +1026,8 @@ void CodeEdit::_new_line(bool p_split_current_line, bool p_above) { bool first_line = false; if (!p_split_current_line) { + deselect(); + if (p_above) { if (cl > 0) { set_caret_line(cl - 1, false); @@ -1602,7 +1628,7 @@ Point2 CodeEdit::get_delimiter_start_position(int p_line, int p_column) const { 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; + bool in_region = ((p_line <= 0 || delimiter_cache[p_line - 1].size() < 1) ? -1 : delimiter_cache[p_line - 1].back()->get()) != -1; /* Check the keys for this line. */ for (const KeyValue<int, int> &E : delimiter_cache[p_line]) { @@ -1726,8 +1752,8 @@ void CodeEdit::set_code_completion_prefixes(const TypedArray<String> &p_prefixes TypedArray<String> CodeEdit::get_code_completion_prefixes() const { TypedArray<String> prefixes; - for (const Set<char32_t>::Element *E = code_completion_prefixes.front(); E; E = E->next()) { - prefixes.push_back(String::chr(E->get())); + for (const char32_t &E : code_completion_prefixes) { + prefixes.push_back(String::chr(E)); } return prefixes; } @@ -1762,10 +1788,10 @@ void CodeEdit::request_code_completion(bool p_force) { /* 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; + ScriptLanguage::CodeCompletionKind kind = ScriptLanguage::CODE_COMPLETION_KIND_PLAIN_TEXT; + const ScriptLanguage::CodeCompletionOption *previous_option = nullptr; for (int i = 0; i < code_completion_options.size(); i++) { - const ScriptCodeCompletionOption ¤t_option = code_completion_options[i]; + const ScriptLanguage::CodeCompletionOption ¤t_option = code_completion_options[i]; if (!previous_option) { previous_option = ¤t_option; kind = current_option.kind; @@ -1775,7 +1801,7 @@ void CodeEdit::request_code_completion(bool p_force) { break; } } - ignored = ignored && (kind == ScriptCodeCompletionOption::KIND_FILE_PATH || kind == ScriptCodeCompletionOption::KIND_NODE_PATH || kind == ScriptCodeCompletionOption::KIND_SIGNAL); + ignored = ignored && (kind == ScriptLanguage::CODE_COMPLETION_KIND_FILE_PATH || kind == ScriptLanguage::CODE_COMPLETION_KIND_NODE_PATH || kind == ScriptLanguage::CODE_COMPLETION_KIND_SIGNAL); } if (ignored) { @@ -1783,23 +1809,23 @@ void CodeEdit::request_code_completion(bool p_force) { } if (p_force) { - emit_signal(SNAME("request_code_completion")); + emit_signal(SNAME("code_completion_requested")); 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(line[ofs - 1]))) { - emit_signal(SNAME("request_code_completion")); + if (ofs > 0 && (is_in_string(get_caret_line(), ofs) != -1 || !is_symbol(line[ofs - 1]) || code_completion_prefixes.has(line[ofs - 1]))) { + emit_signal(SNAME("code_completion_requested")); } else if (ofs > 1 && line[ofs - 1] == ' ' && code_completion_prefixes.has(line[ofs - 2])) { - emit_signal(SNAME("request_code_completion")); + emit_signal(SNAME("code_completion_requested")); } } -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; +void CodeEdit::add_code_completion_option(CodeCompletionKind p_type, const String &p_display_text, const String &p_insert_text, const Color &p_text_color, const Ref<Resource> &p_icon, const Variant &p_value) { + ScriptLanguage::CodeCompletionOption completion_option; + completion_option.kind = (ScriptLanguage::CodeCompletionKind)p_type; completion_option.display = p_display_text; completion_option.insert_text = p_insert_text; completion_option.font_color = p_text_color; @@ -1899,7 +1925,7 @@ void CodeEdit::confirm_code_completion(bool p_replace) { if (merge_text) { for (; caret_col < line.length(); caret_col++) { - if (!_is_char(line[caret_col])) { + if (is_symbol(line[caret_col])) { break; } } @@ -2075,8 +2101,6 @@ void CodeEdit::_bind_methods() { 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); - ClassDB::bind_method(D_METHOD("set_draw_breakpoints_gutter", "enable"), &CodeEdit::set_draw_breakpoints_gutter); ClassDB::bind_method(D_METHOD("is_drawing_breakpoints_gutter"), &CodeEdit::is_drawing_breakpoints_gutter); @@ -2105,16 +2129,12 @@ void CodeEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_executing_lines"), &CodeEdit::get_executing_lines); /* Line numbers */ - ClassDB::bind_method(D_METHOD("_line_number_draw_callback"), &CodeEdit::_line_number_draw_callback); - ClassDB::bind_method(D_METHOD("set_draw_line_numbers", "enable"), &CodeEdit::set_draw_line_numbers); ClassDB::bind_method(D_METHOD("is_draw_line_numbers_enabled"), &CodeEdit::is_draw_line_numbers_enabled); ClassDB::bind_method(D_METHOD("set_line_numbers_zero_padded", "enable"), &CodeEdit::set_line_numbers_zero_padded); ClassDB::bind_method(D_METHOD("is_line_numbers_zero_padded"), &CodeEdit::is_line_numbers_zero_padded); /* Fold Gutter */ - ClassDB::bind_method(D_METHOD("_fold_gutter_draw_callback"), &CodeEdit::_fold_gutter_draw_callback); - 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); @@ -2181,7 +2201,7 @@ void CodeEdit::_bind_methods() { 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("add_code_completion_option", "type", "display_text", "insert_text", "text_color", "icon", "value"), &CodeEdit::add_code_completion_option, DEFVAL(Color(1, 1, 1)), DEFVAL(Ref<Resource>()), 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); @@ -2247,7 +2267,7 @@ void CodeEdit::_bind_methods() { 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_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"); @@ -2257,7 +2277,7 @@ void CodeEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("breakpoint_toggled", PropertyInfo(Variant::INT, "line"))); /* Code Completion */ - ADD_SIGNAL(MethodInfo("request_code_completion")); + ADD_SIGNAL(MethodInfo("code_completion_requested")); /* Symbol lookup */ ADD_SIGNAL(MethodInfo("symbol_lookup", PropertyInfo(Variant::STRING, "symbol"), PropertyInfo(Variant::INT, "line"), PropertyInfo(Variant::INT, "column"))); @@ -2382,7 +2402,7 @@ void CodeEdit::_update_delimiter_cache(int p_from_line, int p_to_line) { } } else { for (int i = start_line; i < end_line; i++) { - delimiter_cache.insert(i, Map<int, int>()); + delimiter_cache.insert(i, RBMap<int, int>()); } } } @@ -2519,7 +2539,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c 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()) { + for (RBMap<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) { @@ -2541,7 +2561,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c region = E->value(); in_region = true; for (int i = E->key() - 2; i >= 0; i--) { - if (!_is_whitespace(line[i])) { + if (!is_whitespace(line[i])) { return -1; } } @@ -2560,7 +2580,7 @@ int CodeEdit::_is_in_delimiter(int p_line, int p_column, DelimiterType p_type) c } for (int i = end_col; i < line.length(); i++) { - if (!_is_whitespace(line[i])) { + if (!is_whitespace(line[i])) { return -1; } } @@ -2688,7 +2708,7 @@ void CodeEdit::_filter_code_completion_candidates_impl() { 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) { + for (const ScriptLanguage::CodeCompletionOption &E : code_completion_option_sources) { Dictionary option; option["kind"] = E.kind; option["display_text"] = E.display; @@ -2713,8 +2733,8 @@ void CodeEdit::_filter_code_completion_candidates_impl() { /* 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"); + ScriptLanguage::CodeCompletionOption option; + option.kind = (ScriptLanguage::CodeCompletionKind)(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"); @@ -2776,11 +2796,11 @@ void CodeEdit::_filter_code_completion_candidates_impl() { while (ofs > 0 && line[ofs] == ' ') { ofs--; } - prev_is_word = _is_char(line[ofs]); + prev_is_word = !is_symbol(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]))) { + while (cofs > 0 && line[cofs - 1] > 32 && (line[cofs - 1] == '/' || !is_symbol(line[cofs - 1]))) { cofs--; } string_to_complete = line.substr(cofs, start_cofs - cofs); @@ -2807,13 +2827,15 @@ void CodeEdit::_filter_code_completion_candidates_impl() { 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; + Vector<ScriptLanguage::CodeCompletionOption> completion_options_casei; + Vector<ScriptLanguage::CodeCompletionOption> completion_options_substr; + Vector<ScriptLanguage::CodeCompletionOption> completion_options_substr_casei; + Vector<ScriptLanguage::CodeCompletionOption> completion_options_subseq; + Vector<ScriptLanguage::CodeCompletionOption> 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) { + for (ScriptLanguage::CodeCompletionOption &option : code_completion_option_sources) { if (single_quote && option.display.is_quoted()) { option.display = option.display.unquote().quote("'"); } @@ -2847,7 +2869,7 @@ void CodeEdit::_filter_code_completion_candidates_impl() { 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)) { + } else if (s.is_subsequence_ofn(option.display)) { completion_options_subseq_casei.push_back(option); } @@ -2862,35 +2884,100 @@ void CodeEdit::_filter_code_completion_candidates_impl() { 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; + const char32_t *sst = &string_to_complete[0]; + const char32_t *sst_lower = &display_lower[0]; + + Vector<Pair<int, int>> ssq_matches; + int ssq_match_start = 0; + int ssq_match_len = 0; + + Vector<Pair<int, int>> ssq_lower_matches; + int ssq_lower_match_start = 0; + int ssq_lower_match_len = 0; + + int sst_start = -1; + int sst_lower_start = -1; + + for (int i = 0; *tgt; tgt++, tgt_lower++, i++) { + // Check substring. + if (*sst == *tgt) { + sst++; + if (sst_start == -1) { + sst_start = i; + } + } else if (sst_start != -1 && *sst) { + sst = &string_to_complete[0]; + sst_start = -1; + } - for (; *tgt; tgt++, tgt_lower++) { + // Check subsequence. if (*ssq == *tgt) { ssq++; - ssq_last_tgt = tgt; + if (ssq_match_len == 0) { + ssq_match_start = i; + } + ssq_match_len++; + } else if (ssq_match_len > 0) { + ssq_matches.push_back(Pair<int, int>(ssq_match_start, ssq_match_len)); + ssq_match_len = 0; + } + + // Check lower substring. + if (*sst_lower == *tgt) { + sst_lower++; + if (sst_lower_start == -1) { + sst_lower_start = i; + } + } else if (sst_lower_start != -1 && *sst_lower) { + sst_lower = &string_to_complete[0]; + sst_lower_start = -1; } + + // Check lower subsequence. if (*ssq_lower == *tgt_lower) { ssq_lower++; - ssq_lower_last_tgt = tgt; + if (ssq_lower_match_len == 0) { + ssq_lower_match_start = i; + } + ssq_lower_match_len++; + } else if (ssq_lower_match_len > 0) { + ssq_lower_matches.push_back(Pair<int, int>(ssq_lower_match_start, ssq_lower_match_len)); + ssq_lower_match_len = 0; } } /* 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]) { + if (!*ssq) { // Matched the whole subsequence in s. + option.matches.clear(); + + if (sst_start == 0) { // Matched substring in beginning of s. + option.matches.push_back(Pair<int, int>(sst_start, string_to_complete.length())); code_completion_options.push_back(option); + } else if (sst_start > 0) { // Matched substring in s. + option.matches.push_back(Pair<int, int>(sst_start, string_to_complete.length())); + completion_options_substr.push_back(option); } else { + if (ssq_match_len > 0) { + ssq_matches.push_back(Pair<int, int>(ssq_match_start, ssq_match_len)); + } + option.matches.append_array(ssq_matches); 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]) { + } else if (!*ssq_lower) { // Matched the whole subsequence in s_lower. + option.matches.clear(); + + if (sst_lower_start == 0) { // Matched substring in beginning of s_lower. + option.matches.push_back(Pair<int, int>(sst_lower_start, string_to_complete.length())); completion_options_casei.push_back(option); + } else if (sst_lower_start > 0) { // Matched substring in s_lower. + option.matches.push_back(Pair<int, int>(sst_lower_start, string_to_complete.length())); + completion_options_substr_casei.push_back(option); } else { + if (ssq_lower_match_len > 0) { + ssq_lower_matches.push_back(Pair<int, int>(ssq_lower_match_start, ssq_lower_match_len)); + } + option.matches.append_array(ssq_lower_matches); completion_options_subseq_casei.push_back(option); } max_width = MAX(max_width, font->get_string_size(option.display, font_size).width + offset); @@ -2954,7 +3041,9 @@ void CodeEdit::_text_changed() { lc = get_line_count(); List<int> breakpoints; - breakpointed_lines.get_key_list(&breakpoints); + for (const KeyValue<int, bool> &E : breakpointed_lines) { + breakpoints.push_back(E.key); + } for (const int &line : breakpoints) { if (line < lines_edited_from || (line < lc && is_line_breakpointed(line))) { continue; @@ -2990,7 +3079,7 @@ CodeEdit::CodeEdit() { add_auto_brace_completion_pair("\"", "\""); add_auto_brace_completion_pair("\'", "\'"); - /* Delimiter traking */ + /* Delimiter tracking */ add_string_delimiter("\"", "\"", false); add_string_delimiter("\'", "\'", false); @@ -3007,7 +3096,7 @@ CodeEdit::CodeEdit() { set_gutter_draw(gutter_idx, false); set_gutter_overwritable(gutter_idx, true); set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); - set_gutter_custom_draw(gutter_idx, this, "_main_gutter_draw_callback"); + set_gutter_custom_draw(gutter_idx, callable_mp(this, &CodeEdit::_main_gutter_draw_callback)); gutter_idx++; /* Line numbers */ @@ -3015,7 +3104,7 @@ CodeEdit::CodeEdit() { set_gutter_name(gutter_idx, "line_numbers"); set_gutter_draw(gutter_idx, false); set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); - set_gutter_custom_draw(gutter_idx, this, "_line_number_draw_callback"); + set_gutter_custom_draw(gutter_idx, callable_mp(this, &CodeEdit::_line_number_draw_callback)); gutter_idx++; /* Fold Gutter */ @@ -3023,7 +3112,7 @@ CodeEdit::CodeEdit() { set_gutter_name(gutter_idx, "fold_gutter"); set_gutter_draw(gutter_idx, false); set_gutter_type(gutter_idx, GUTTER_TYPE_CUSTOM); - set_gutter_custom_draw(gutter_idx, this, "_fold_gutter_draw_callback"); + set_gutter_custom_draw(gutter_idx, callable_mp(this, &CodeEdit::_fold_gutter_draw_callback)); gutter_idx++; connect("lines_edited_from", callable_mp(this, &CodeEdit::_lines_edited_from)); diff --git a/scene/gui/code_edit.h b/scene/gui/code_edit.h index f0d971dd35..ccf046c612 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 CodeEdit : public TextEdit { public: /* Keep enum in sync with: */ - /* /core/object/script_language.h - ScriptCodeCompletionOption::Kind */ + /* /core/object/script_language.h - ScriptLanguage::CodeCompletionKind */ enum CodeCompletionKind { KIND_CLASS, KIND_FUNCTION, @@ -58,7 +58,7 @@ private: String indent_text = "\t"; bool auto_indent = false; - Set<char32_t> auto_indent_prefixes; + HashSet<char32_t> auto_indent_prefixes; bool indent_using_spaces = false; int _calculate_spaces_till_next_left_indent(int p_column) const; @@ -176,7 +176,7 @@ private: * ] * ] */ - Vector<Map<int, int>> delimiter_cache; + Vector<RBMap<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; @@ -208,15 +208,15 @@ private: Color code_completion_existing_color = Color(0, 0, 0, 0); bool code_completion_active = false; - Vector<ScriptCodeCompletionOption> code_completion_options; + Vector<ScriptLanguage::CodeCompletionOption> 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<char32_t> code_completion_prefixes; - List<ScriptCodeCompletionOption> code_completion_option_submitted; - List<ScriptCodeCompletionOption> code_completion_option_sources; + HashSet<char32_t> code_completion_prefixes; + List<ScriptLanguage::CodeCompletionOption> code_completion_option_submitted; + List<ScriptLanguage::CodeCompletionOption> code_completion_option_sources; String code_completion_base; void _filter_code_completion_candidates_impl(); @@ -398,7 +398,7 @@ public: 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 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 Ref<Resource> &p_icon = Ref<Resource>(), const Variant &p_value = Variant::NIL); void update_code_completion_options(bool p_forced = false); TypedArray<Dictionary> get_code_completion_options() const; diff --git a/scene/gui/color_picker.cpp b/scene/gui/color_picker.cpp index 63ffe7bf95..6f7ad94139 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,11 +33,11 @@ #include "core/input/input.h" #include "core/os/keyboard.h" #include "core/os/os.h" +#include "scene/main/window.h" #ifdef TOOLS_ENABLED #include "editor/editor_settings.h" #endif -#include "scene/main/window.h" List<Color> ColorPicker::preset_cache; @@ -45,7 +45,6 @@ void ColorPicker::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { _update_color(); - #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint()) { if (preset_cache.is_empty()) { @@ -437,7 +436,7 @@ ColorPicker::PickerShapeType ColorPicker::get_picker_shape() const { } 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; + return (int(get_minimum_size().width) - (preset_container->get_theme_constant(SNAME("h_separation")) * (preset_column_count - 1))) / preset_column_count; } void ColorPicker::_add_preset_button(int p_size, const Color &p_color) { @@ -1222,7 +1221,7 @@ ColorPicker::ColorPicker() : hhb->add_child(text_type); text_type->set_text("#"); - text_type->set_tooltip(TTR("Switch between hexadecimal and code values.")); + text_type->set_tooltip(RTR("Switch between hexadecimal and code values.")); if (Engine::get_singleton()->is_editor_hint()) { text_type->connect("pressed", callable_mp(this, &ColorPicker::_text_type_toggled)); } else { @@ -1305,7 +1304,7 @@ void ColorPickerButton::pressed() { Size2 size = get_size() * get_viewport()->get_canvas_transform().get_scale(); - popup->set_as_minsize(); + popup->reset_size(); picker->_update_presets(); Rect2i usable_rect = popup->get_usable_parent_rect(); @@ -1347,17 +1346,18 @@ void ColorPickerButton::_notification(int p_what) { draw_texture(Control::get_theme_icon(SNAME("overbright_indicator"), SNAME("ColorPicker")), normal->get_offset()); } } break; + case NOTIFICATION_WM_CLOSE_REQUEST: { if (popup) { popup->hide(); } } break; - } - if (p_what == NOTIFICATION_VISIBILITY_CHANGED) { - if (popup && !is_visible_in_tree()) { - popup->hide(); - } + case NOTIFICATION_VISIBILITY_CHANGED: { + if (popup && !is_visible_in_tree()) { + popup->hide(); + } + } break; } } @@ -1429,7 +1429,8 @@ void ColorPickerButton::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "edit_alpha"), "set_edit_alpha", "is_editing_alpha"); } -ColorPickerButton::ColorPickerButton() { +ColorPickerButton::ColorPickerButton(const String &p_text) : + Button(p_text) { set_toggle_mode(true); } diff --git a/scene/gui/color_picker.h b/scene/gui/color_picker.h index ad4f5ad5b1..6f3e16009c 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -231,7 +231,7 @@ public: ColorPicker *get_picker(); PopupPanel *get_popup(); - ColorPickerButton(); + ColorPickerButton(const String &p_text = String()); }; VARIANT_ENUM_CAST(ColorPicker::PickerShapeType); diff --git a/scene/gui/color_rect.cpp b/scene/gui/color_rect.cpp index e35d37d520..2955f74a0c 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,8 +40,10 @@ Color ColorRect::get_color() const { } void ColorRect::_notification(int p_what) { - if (p_what == NOTIFICATION_DRAW) { - draw_rect(Rect2(Point2(), get_size()), color); + switch (p_what) { + case NOTIFICATION_DRAW: { + draw_rect(Rect2(Point2(), get_size()), color); + } break; } } diff --git a/scene/gui/color_rect.h b/scene/gui/color_rect.h index 5c650f9f01..35c8ebcaf8 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 81afa53d85..5512c0f1fd 100644 --- a/scene/gui/container.cpp +++ b/scene/gui/container.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +29,7 @@ /*************************************************************************/ #include "container.h" + #include "core/object/message_queue.h" #include "scene/scene_string_names.h" @@ -143,18 +144,46 @@ void Container::queue_sort() { pending_sort = true; } +Vector<int> Container::get_allowed_size_flags_horizontal() const { + Vector<int> flags; + if (GDVIRTUAL_CALL(_get_allowed_size_flags_horizontal, flags)) { + return flags; + } + + flags.append(SIZE_FILL); + flags.append(SIZE_EXPAND); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + +Vector<int> Container::get_allowed_size_flags_vertical() const { + Vector<int> flags; + if (GDVIRTUAL_CALL(_get_allowed_size_flags_vertical, flags)) { + return flags; + } + + flags.append(SIZE_FILL); + flags.append(SIZE_EXPAND); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + void Container::_notification(int p_what) { switch (p_what) { case NOTIFICATION_ENTER_TREE: { pending_sort = false; queue_sort(); } break; - case NOTIFICATION_RESIZED: { - queue_sort(); - } break; + + case NOTIFICATION_RESIZED: case NOTIFICATION_THEME_CHANGED: { queue_sort(); } break; + case NOTIFICATION_VISIBILITY_CHANGED: { if (is_visible_in_tree()) { queue_sort(); @@ -167,7 +196,7 @@ TypedArray<String> Container::get_configuration_warnings() const { TypedArray<String> warnings = Control::get_configuration_warnings(); if (get_class() == "Container" && get_script().is_null()) { - 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.")); + warnings.push_back(RTR("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 warnings; @@ -177,6 +206,9 @@ void Container::_bind_methods() { ClassDB::bind_method(D_METHOD("queue_sort"), &Container::queue_sort); ClassDB::bind_method(D_METHOD("fit_child_in_rect", "child", "rect"), &Container::fit_child_in_rect); + GDVIRTUAL_BIND(_get_allowed_size_flags_horizontal); + GDVIRTUAL_BIND(_get_allowed_size_flags_vertical); + BIND_CONSTANT(NOTIFICATION_PRE_SORT_CHILDREN); BIND_CONSTANT(NOTIFICATION_SORT_CHILDREN); diff --git a/scene/gui/container.h b/scene/gui/container.h index f3ae948556..9ec4ad3200 100644 --- a/scene/gui/container.h +++ b/scene/gui/container.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -46,6 +46,9 @@ protected: virtual void move_child_notify(Node *p_child) override; virtual void remove_child_notify(Node *p_child) override; + GDVIRTUAL0RC(Vector<int>, _get_allowed_size_flags_horizontal) + GDVIRTUAL0RC(Vector<int>, _get_allowed_size_flags_vertical) + void _notification(int p_what); static void _bind_methods(); @@ -57,6 +60,9 @@ public: void fit_child_in_rect(Control *p_child, const Rect2 &p_rect); + virtual Vector<int> get_allowed_size_flags_horizontal() const; + virtual Vector<int> get_allowed_size_flags_vertical() const; + TypedArray<String> get_configuration_warnings() const override; Container(); diff --git a/scene/gui/control.cpp b/scene/gui/control.cpp index 1fdc30eb6d..db78b4adb6 100644 --- a/scene/gui/control.cpp +++ b/scene/gui/control.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,7 +47,7 @@ #include "servers/text_server.h" #ifdef TOOLS_ENABLED -#include "editor/plugins/canvas_item_editor_plugin.h" +#include "editor/plugins/control_editor_plugin.h" #endif #ifdef TOOLS_ENABLED @@ -56,51 +56,71 @@ Dictionary Control::_edit_get_state() const { s["rotation"] = get_rotation(); s["scale"] = get_scale(); s["pivot"] = get_pivot_offset(); + Array anchors; 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 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; + + s["layout_mode"] = _get_layout_mode(); + s["anchors_layout_preset"] = _get_anchors_layout_preset(); + 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")); + !p_state.has("pivot") || !p_state.has("anchors") || !p_state.has("offsets") || + !p_state.has("layout_mode") || !p_state.has("anchors_layout_preset")); Dictionary state = p_state; set_rotation(state["rotation"]); set_scale(state["scale"]); set_pivot_offset(state["pivot"]); + Array anchors = state["anchors"]; + + // If anchors are not in their default position, force the anchor layout mode in place of position. + LayoutMode _layout = (LayoutMode)(int)state["layout_mode"]; + if (_layout == LayoutMode::LAYOUT_MODE_POSITION) { + bool anchors_mode = ((real_t)anchors[0] != 0.0 || (real_t)anchors[1] != 0.0 || (real_t)anchors[2] != 0.0 || (real_t)anchors[3] != 0.0); + if (anchors_mode) { + _layout = LayoutMode::LAYOUT_MODE_ANCHORS; + } + } + + _set_layout_mode(_layout); + if (_layout == LayoutMode::LAYOUT_MODE_ANCHORS) { + _set_anchors_layout_preset((int)state["anchors_layout_preset"]); + } + 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 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); -#endif + set_position(p_position, ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled() && Object::cast_to<Control>(data.parent)); }; Point2 Control::_edit_get_position() const { @@ -116,15 +136,9 @@ 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 - // Unlikely to happen. TODO: enclose all _edit_ functions into TOOLS_ENABLED - set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snapped(Vector2(1, 1))); - set_size(p_edit_rect.size.snapped(Vector2(1, 1))); -#endif + set_position((get_position() + get_transform().basis_xform(p_edit_rect.position)).snapped(Vector2(1, 1)), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); + set_size(p_edit_rect.size.snapped(Vector2(1, 1)), ControlEditorToolbar::get_singleton()->is_anchors_mode_enabled()); } Rect2 Control::_edit_get_rect() const { @@ -176,9 +190,10 @@ String Control::properties_managed_by_container[] = { "anchor_top", "anchor_right", "anchor_bottom", - "rect_position", - "rect_scale", - "rect_size" + "position", + "rotation", + "scale", + "size" }; void Control::accept_event() { @@ -235,42 +250,41 @@ Transform2D Control::_get_internal_transform() const { bool Control::_set(const StringName &p_name, const Variant &p_value) { String name = p_name; - // Prefixes "custom_*" are supported for compatibility with 3.x. - if (!name.begins_with("theme_override") && !name.begins_with("custom")) { + if (!name.begins_with("theme_override")) { return false; } - if (p_value.get_type() == Variant::NIL) { - if (name.begins_with("theme_override_icons/") || name.begins_with("custom_icons/")) { + if (p_value.get_type() == Variant::NIL || (p_value.get_type() == Variant::OBJECT && (Object *)p_value == nullptr)) { + if (name.begins_with("theme_override_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("theme_override_styles/") || name.begins_with("custom_styles/")) { + } else if (name.begins_with("theme_override_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("theme_override_fonts/") || name.begins_with("custom_fonts/")) { + } else if (name.begins_with("theme_override_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("theme_override_font_sizes/") || name.begins_with("custom_font_sizes/")) { + } else if (name.begins_with("theme_override_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/")) { + } else if (name.begins_with("theme_override_colors/")) { String dname = name.get_slicec('/', 1); data.color_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); - } else if (name.begins_with("theme_override_constants/") || name.begins_with("custom_constants/")) { + } else if (name.begins_with("theme_override_constants/")) { String dname = name.get_slicec('/', 1); data.constant_override.erase(dname); notification(NOTIFICATION_THEME_CHANGED); @@ -279,22 +293,22 @@ bool Control::_set(const StringName &p_name, const Variant &p_value) { } } else { - if (name.begins_with("theme_override_icons/") || name.begins_with("custom_icons/")) { + if (name.begins_with("theme_override_icons/")) { String dname = name.get_slicec('/', 1); add_theme_icon_override(dname, p_value); - } else if (name.begins_with("theme_override_styles/") || name.begins_with("custom_styles/")) { + } else if (name.begins_with("theme_override_styles/")) { String dname = name.get_slicec('/', 1); add_theme_style_override(dname, p_value); - } else if (name.begins_with("theme_override_fonts/") || name.begins_with("custom_fonts/")) { + } else if (name.begins_with("theme_override_fonts/")) { String dname = name.get_slicec('/', 1); add_theme_font_override(dname, p_value); - } else if (name.begins_with("theme_override_font_sizes/") || name.begins_with("custom_font_sizes/")) { + } else if (name.begins_with("theme_override_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/")) { + } else if (name.begins_with("theme_override_colors/")) { String dname = name.get_slicec('/', 1); add_theme_color_override(dname, p_value); - } else if (name.begins_with("theme_override_constants/") || name.begins_with("custom_constants/")) { + } else if (name.begins_with("theme_override_constants/")) { String dname = name.get_slicec('/', 1); add_theme_constant_override(dname, p_value); } else { @@ -353,7 +367,7 @@ 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(); - p_list->push_back(PropertyInfo(Variant::NIL, "Theme Overrides", PROPERTY_HINT_NONE, "theme_override_", PROPERTY_USAGE_GROUP)); + p_list->push_back(PropertyInfo(Variant::NIL, TTRC("Theme Overrides"), PROPERTY_HINT_NONE, "theme_override_", PROPERTY_USAGE_GROUP)); { List<StringName> names; @@ -430,6 +444,7 @@ void Control::_get_property_list(List<PropertyInfo> *p_list) const { } void Control::_validate_property(PropertyInfo &property) const { + // Update theme type variation options. if (property.name == "theme_type_variation") { List<StringName> names; @@ -455,10 +470,106 @@ void Control::_validate_property(PropertyInfo &property) const { property.hint_string = hint_string; } - if (!Object::cast_to<Container>(get_parent())) { - return; + + if (property.name == "mouse_force_pass_scroll_events") { + // Disable force pass if the control is not stopping the event. + if (data.mouse_filter != MOUSE_FILTER_STOP) { + property.usage |= PROPERTY_USAGE_READ_ONLY; + } } + + // Validate which positioning properties should be displayed depending on the parent and the layout mode. + Node *parent_node = get_parent_control(); + if (!parent_node) { + // If there is no parent, display both anchor and container options. + + // Set the layout mode to be disabled with the proper value. + if (property.name == "layout_mode") { + property.hint_string = "Position,Anchors,Container,Uncontrolled"; + property.usage |= PROPERTY_USAGE_READ_ONLY; + } + + // Use the layout mode to display or hide advanced anchoring properties. + bool use_custom_anchors = _get_anchors_layout_preset() == -1; // Custom "preset". + if (!use_custom_anchors && (property.name.begins_with("anchor_") || property.name.begins_with("offset_") || property.name.begins_with("grow_"))) { + property.usage ^= PROPERTY_USAGE_EDITOR; + } + } else if (Object::cast_to<Container>(parent_node)) { + // If the parent is a container, display only container-related properties. + if (property.name.begins_with("anchor_") || property.name.begins_with("offset_") || property.name.begins_with("grow_") || property.name == "anchors_preset" || + property.name == "position" || property.name == "rotation" || property.name == "scale" || property.name == "size" || property.name == "pivot_offset") { + property.usage ^= PROPERTY_USAGE_EDITOR; + + } else if (property.name == "layout_mode") { + // Set the layout mode to be disabled with the proper value. + property.hint_string = "Position,Anchors,Container,Uncontrolled"; + property.usage |= PROPERTY_USAGE_READ_ONLY; + } else if (property.name == "size_flags_horizontal" || property.name == "size_flags_vertical") { + // Filter allowed size flags based on the parent container configuration. + Container *parent_container = Object::cast_to<Container>(parent_node); + Vector<int> size_flags; + if (property.name == "size_flags_horizontal") { + size_flags = parent_container->get_allowed_size_flags_horizontal(); + } else if (property.name == "size_flags_vertical") { + size_flags = parent_container->get_allowed_size_flags_vertical(); + } + + // Enforce the order of the options, regardless of what the container provided. + String hint_string; + if (size_flags.has(SIZE_FILL)) { + hint_string += "Fill:1"; + } + if (size_flags.has(SIZE_EXPAND)) { + if (!hint_string.is_empty()) { + hint_string += ","; + } + hint_string += "Expand:2"; + } + if (size_flags.has(SIZE_SHRINK_CENTER)) { + if (!hint_string.is_empty()) { + hint_string += ","; + } + hint_string += "Shrink Center:4"; + } + if (size_flags.has(SIZE_SHRINK_END)) { + if (!hint_string.is_empty()) { + hint_string += ","; + } + hint_string += "Shrink End:8"; + } + + if (hint_string.is_empty()) { + property.hint_string = ""; + property.usage |= PROPERTY_USAGE_READ_ONLY; + } else { + property.hint_string = hint_string; + } + } + } else { + // If the parent is NOT a container or not a control at all, display only anchoring-related properties. + if (property.name.begins_with("size_flags_")) { + property.usage ^= PROPERTY_USAGE_EDITOR; + + } else if (property.name == "layout_mode") { + // Set the layout mode to be enabled with proper options. + property.hint_string = "Position,Anchors"; + } + + // Use the layout mode to display or hide advanced anchoring properties. + bool use_anchors = _get_layout_mode() == LayoutMode::LAYOUT_MODE_ANCHORS; + if (!use_anchors && property.name == "anchors_preset") { + property.usage ^= PROPERTY_USAGE_EDITOR; + } + bool use_custom_anchors = use_anchors && _get_anchors_layout_preset() == -1; // Custom "preset". + if (!use_custom_anchors && (property.name.begins_with("anchor_") || property.name.begins_with("offset_") || property.name.begins_with("grow_"))) { + property.usage ^= PROPERTY_USAGE_EDITOR; + } + } + // Disable the property if it's managed by the parent container. + if (!Object::cast_to<Container>(parent_node)) { + return; + } bool property_is_managed_by_container = false; for (unsigned i = 0; i < properties_managed_by_container_count; i++) { property_is_managed_by_container = properties_managed_by_container[i] == property.name; @@ -576,21 +687,27 @@ void Control::_update_canvas_item_transform() { Transform2D xform = _get_internal_transform(); xform[2] += get_position(); + // We use a little workaround to avoid flickering when moving the pivot with _edit_set_pivot() + if (is_inside_tree() && Math::abs(Math::sin(data.rotation * 4.0f)) < 0.00001f && get_viewport()->is_snap_controls_to_pixels_enabled()) { + xform[2] = xform[2].round(); + } + RenderingServer::get_singleton()->canvas_item_set_transform(get_canvas_item(), xform); } void Control::_notification(int p_notification) { switch (p_notification) { - case NOTIFICATION_ENTER_TREE: { - } break; case NOTIFICATION_POST_ENTER_TREE: { data.minimum_size_valid = false; data.is_rtl_dirty = true; _size_changed(); } break; + case NOTIFICATION_EXIT_TREE: { + release_focus(); 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); @@ -602,35 +719,25 @@ void Control::_notification(int p_notification) { data.parent_window = Object::cast_to<Window>(get_parent()); data.is_rtl_dirty = true; - Node *parent = this; //meh + CanvasItem *node = this; Control *parent_control = nullptr; - bool subwindow = false; - - while (parent) { - parent = parent->get_parent(); + while (!node->is_set_as_top_level()) { + CanvasItem *parent = Object::cast_to<CanvasItem>(node->get_parent()); if (!parent) { break; } - CanvasItem *ci = Object::cast_to<CanvasItem>(parent); - if (ci && ci->is_set_as_top_level()) { - subwindow = true; - break; - } - parent_control = Object::cast_to<Control>(parent); - if (parent_control) { break; - } else if (ci) { - } else { - break; } + + node = parent; } - if (parent_control && !subwindow) { - //do nothing, has a parent control and not top_level + if (parent_control) { + // Do nothing, has a parent control. if (data.theme.is_null() && parent_control->data.theme_owner) { data.theme_owner = parent_control->data.theme_owner; notification(NOTIFICATION_THEME_CHANGED); @@ -653,6 +760,7 @@ void Control::_notification(int p_notification) { viewport->connect("size_changed", callable_mp(this, &Control::_size_changed)); } } break; + case NOTIFICATION_EXIT_CANVAS: { if (data.parent_canvas_item) { data.parent_canvas_item->disconnect("item_rect_changed", callable_mp(this, &Control::_size_changed)); @@ -673,8 +781,8 @@ void Control::_notification(int p_notification) { 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 children to draw (like TabContainer) // update if necessary @@ -686,54 +794,59 @@ void Control::_notification(int p_notification) { if (data.RI) { get_viewport()->_gui_set_root_order_dirty(); } - } break; + case NOTIFICATION_RESIZED: { emit_signal(SceneStringNames::get_singleton()->resized); } break; + case NOTIFICATION_DRAW: { _update_canvas_item_transform(); RenderingServer::get_singleton()->canvas_item_set_custom_rect(get_canvas_item(), !data.disable_visibility_clip, Rect2(Point2(), get_size())); RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), data.clip_contents); - //emit_signal(SceneStringNames::get_singleton()->draw); - } break; + case NOTIFICATION_MOUSE_ENTER: { emit_signal(SceneStringNames::get_singleton()->mouse_entered); } break; + case NOTIFICATION_MOUSE_EXIT: { emit_signal(SceneStringNames::get_singleton()->mouse_exited); } break; + case NOTIFICATION_FOCUS_ENTER: { emit_signal(SceneStringNames::get_singleton()->focus_entered); update(); } break; + case NOTIFICATION_FOCUS_EXIT: { emit_signal(SceneStringNames::get_singleton()->focus_exited); update(); } break; + case NOTIFICATION_THEME_CHANGED: { update_minimum_size(); update(); } break; + case NOTIFICATION_VISIBILITY_CHANGED: { if (!is_visible_in_tree()) { if (get_viewport() != nullptr) { get_viewport()->_gui_hide_control(this); } - - //remove key focus - } else { data.minimum_size_valid = false; + _update_minimum_size(); _size_changed(); } - } break; + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { - data.is_rtl_dirty = true; - _size_changed(); + if (is_inside_tree()) { + data.is_rtl_dirty = true; + _size_changed(); + } } break; } } @@ -1148,12 +1261,12 @@ float Control::fetch_theme_default_base_scale(Control *p_theme_owner, Window *p_ Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - if (theme_owner && theme_owner->data.theme->has_default_theme_base_scale()) { - return theme_owner->data.theme->get_default_theme_base_scale(); + if (theme_owner && theme_owner->data.theme->has_default_base_scale()) { + return theme_owner->data.theme->get_default_base_scale(); } - if (theme_owner_window && theme_owner_window->theme->has_default_theme_base_scale()) { - return theme_owner_window->theme->get_default_theme_base_scale(); + if (theme_owner_window && theme_owner_window->theme->has_default_base_scale()) { + return theme_owner_window->theme->get_default_base_scale(); } Node *parent = theme_owner ? theme_owner->get_parent() : theme_owner_window->get_parent(); @@ -1175,13 +1288,16 @@ float Control::fetch_theme_default_base_scale(Control *p_theme_owner, Window *p_ // Secondly, check the project-defined Theme resource. if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_default_theme_base_scale()) { - return Theme::get_project_default()->get_default_theme_base_scale(); + if (Theme::get_project_default()->has_default_base_scale()) { + return Theme::get_project_default()->get_default_base_scale(); } } // Lastly, fall back on the default Theme. - return Theme::get_default()->get_default_theme_base_scale(); + if (Theme::get_default()->has_default_base_scale()) { + return Theme::get_default()->get_default_base_scale(); + } + return Theme::get_fallback_base_scale(); } float Control::get_theme_default_base_scale() const { @@ -1196,12 +1312,12 @@ Ref<Font> Control::fetch_theme_default_font(Control *p_theme_owner, Window *p_th Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - if (theme_owner && theme_owner->data.theme->has_default_theme_font()) { - return theme_owner->data.theme->get_default_theme_font(); + if (theme_owner && theme_owner->data.theme->has_default_font()) { + return theme_owner->data.theme->get_default_font(); } - if (theme_owner_window && theme_owner_window->theme->has_default_theme_font()) { - return theme_owner_window->theme->get_default_theme_font(); + if (theme_owner_window && theme_owner_window->theme->has_default_font()) { + return theme_owner_window->theme->get_default_font(); } Node *parent = theme_owner ? theme_owner->get_parent() : theme_owner_window->get_parent(); @@ -1223,13 +1339,16 @@ Ref<Font> Control::fetch_theme_default_font(Control *p_theme_owner, Window *p_th // Secondly, check the project-defined Theme resource. if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_default_theme_font()) { - return Theme::get_project_default()->get_default_theme_font(); + if (Theme::get_project_default()->has_default_font()) { + return Theme::get_project_default()->get_default_font(); } } // Lastly, fall back on the default Theme. - return Theme::get_default()->get_default_theme_font(); + if (Theme::get_default()->has_default_font()) { + return Theme::get_default()->get_default_font(); + } + return Theme::get_fallback_font(); } Ref<Font> Control::get_theme_default_font() const { @@ -1244,12 +1363,12 @@ int Control::fetch_theme_default_font_size(Control *p_theme_owner, Window *p_the Window *theme_owner_window = p_theme_owner_window; while (theme_owner || theme_owner_window) { - if (theme_owner && theme_owner->data.theme->has_default_theme_font_size()) { - return theme_owner->data.theme->get_default_theme_font_size(); + if (theme_owner && theme_owner->data.theme->has_default_font_size()) { + return theme_owner->data.theme->get_default_font_size(); } - if (theme_owner_window && theme_owner_window->theme->has_default_theme_font_size()) { - return theme_owner_window->theme->get_default_theme_font_size(); + if (theme_owner_window && theme_owner_window->theme->has_default_font_size()) { + return theme_owner_window->theme->get_default_font_size(); } Node *parent = theme_owner ? theme_owner->get_parent() : theme_owner_window->get_parent(); @@ -1271,13 +1390,16 @@ int Control::fetch_theme_default_font_size(Control *p_theme_owner, Window *p_the // Secondly, check the project-defined Theme resource. if (Theme::get_project_default().is_valid()) { - if (Theme::get_project_default()->has_default_theme_font_size()) { - return Theme::get_project_default()->get_default_theme_font_size(); + if (Theme::get_project_default()->has_default_font_size()) { + return Theme::get_project_default()->get_default_font_size(); } } // Lastly, fall back on the default Theme. - return Theme::get_default()->get_default_theme_font_size(); + if (Theme::get_default()->has_default_font_size()) { + return Theme::get_default()->get_default_font_size(); + } + return Theme::get_fallback_font_size(); } int Control::get_theme_default_font_size() const { @@ -1296,7 +1418,7 @@ Rect2 Control::get_parent_anchorable_rect() const { #ifdef TOOLS_ENABLED Node *edited_root = get_tree()->get_edited_scene_root(); 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")); + parent_rect.size = Size2(ProjectSettings::get_singleton()->get("display/window/size/viewport_width"), ProjectSettings::get_singleton()->get("display/window/size/viewport_height")); } else { parent_rect = get_viewport()->get_visible_rect(); } @@ -1373,6 +1495,55 @@ void Control::_size_changed() { } } +void Control::_set_layout_mode(LayoutMode p_mode) { + bool list_changed = false; + + if (p_mode == LayoutMode::LAYOUT_MODE_POSITION || p_mode == LayoutMode::LAYOUT_MODE_ANCHORS) { + if ((int)get_meta("_edit_layout_mode", p_mode) != (int)p_mode) { + list_changed = true; + } + + set_meta("_edit_layout_mode", (int)p_mode); + + if (p_mode == LayoutMode::LAYOUT_MODE_POSITION) { + remove_meta("_edit_layout_mode"); + remove_meta("_edit_use_custom_anchors"); + set_anchors_and_offsets_preset(LayoutPreset::PRESET_TOP_LEFT, LayoutPresetMode::PRESET_MODE_KEEP_SIZE); + set_grow_direction_preset(LayoutPreset::PRESET_TOP_LEFT); + } + } else { + if (has_meta("_edit_layout_mode")) { + remove_meta("_edit_layout_mode"); + list_changed = true; + } + } + + if (list_changed) { + notify_property_list_changed(); + } +} + +Control::LayoutMode Control::_get_layout_mode() const { + Node *parent_node = get_parent_control(); + // In these modes the property is read-only. + if (!parent_node) { + return LayoutMode::LAYOUT_MODE_UNCONTROLLED; + } else if (Object::cast_to<Container>(parent_node)) { + return LayoutMode::LAYOUT_MODE_CONTAINER; + } + + // If anchors are not in the top-left position, this is definitely in anchors mode. + if (_get_anchors_layout_preset() != (int)LayoutPreset::PRESET_TOP_LEFT) { + return LayoutMode::LAYOUT_MODE_ANCHORS; + } + // Otherwise check what was saved. + if (has_meta("_edit_layout_mode")) { + return (LayoutMode)(int)get_meta("_edit_layout_mode"); + } + // Or fallback on default. + return LayoutMode::LAYOUT_MODE_POSITION; +} + 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); @@ -1414,6 +1585,133 @@ void Control::set_anchor_and_offset(Side p_side, real_t p_anchor, real_t p_pos, set_offset(p_side, p_pos); } +void Control::_set_anchors_layout_preset(int p_preset) { + bool list_changed = false; + + if (get_meta("_edit_layout_mode", LayoutMode::LAYOUT_MODE_ANCHORS).operator int() != LayoutMode::LAYOUT_MODE_ANCHORS) { + list_changed = true; + set_meta("_edit_layout_mode", LayoutMode::LAYOUT_MODE_ANCHORS); + } + + if (p_preset == -1) { + if (!get_meta("_edit_use_custom_anchors", false)) { + set_meta("_edit_use_custom_anchors", true); + notify_property_list_changed(); + } + return; // Keep settings as is. + } + + if (get_meta("_edit_use_custom_anchors", true)) { + list_changed = true; + remove_meta("_edit_use_custom_anchors"); + } + + LayoutPreset preset = (LayoutPreset)p_preset; + // Set correct anchors. + set_anchors_preset(preset); + + // Select correct preset mode. + switch (preset) { + case PRESET_TOP_LEFT: + case PRESET_TOP_RIGHT: + case PRESET_BOTTOM_LEFT: + case PRESET_BOTTOM_RIGHT: + case PRESET_CENTER_LEFT: + case PRESET_CENTER_TOP: + case PRESET_CENTER_RIGHT: + case PRESET_CENTER_BOTTOM: + case PRESET_CENTER: + set_offsets_preset(preset, LayoutPresetMode::PRESET_MODE_KEEP_SIZE); + break; + case PRESET_LEFT_WIDE: + case PRESET_TOP_WIDE: + case PRESET_RIGHT_WIDE: + case PRESET_BOTTOM_WIDE: + case PRESET_VCENTER_WIDE: + case PRESET_HCENTER_WIDE: + case PRESET_WIDE: + set_offsets_preset(preset, LayoutPresetMode::PRESET_MODE_MINSIZE); + break; + } + + // Select correct grow directions. + set_grow_direction_preset(preset); + + if (list_changed) { + notify_property_list_changed(); + } +} + +int Control::_get_anchors_layout_preset() const { + // If the custom preset was selected by user, use it. + if ((bool)get_meta("_edit_use_custom_anchors", false)) { + return -1; + } + + // Check anchors to determine if the current state matches a preset, or not. + + float left = get_anchor(SIDE_LEFT); + float right = get_anchor(SIDE_RIGHT); + float top = get_anchor(SIDE_TOP); + float bottom = get_anchor(SIDE_BOTTOM); + + if (left == ANCHOR_BEGIN && right == ANCHOR_BEGIN && top == ANCHOR_BEGIN && bottom == ANCHOR_BEGIN) { + return (int)LayoutPreset::PRESET_TOP_LEFT; + } + if (left == ANCHOR_END && right == ANCHOR_END && top == ANCHOR_BEGIN && bottom == ANCHOR_BEGIN) { + return (int)LayoutPreset::PRESET_TOP_RIGHT; + } + if (left == ANCHOR_BEGIN && right == ANCHOR_BEGIN && top == ANCHOR_END && bottom == ANCHOR_END) { + return (int)LayoutPreset::PRESET_BOTTOM_LEFT; + } + if (left == ANCHOR_END && right == ANCHOR_END && top == ANCHOR_END && bottom == ANCHOR_END) { + return (int)LayoutPreset::PRESET_BOTTOM_RIGHT; + } + + if (left == ANCHOR_BEGIN && right == ANCHOR_BEGIN && top == 0.5 && bottom == 0.5) { + return (int)LayoutPreset::PRESET_CENTER_LEFT; + } + if (left == ANCHOR_END && right == ANCHOR_END && top == 0.5 && bottom == 0.5) { + return (int)LayoutPreset::PRESET_CENTER_RIGHT; + } + if (left == 0.5 && right == 0.5 && top == ANCHOR_BEGIN && bottom == ANCHOR_BEGIN) { + return (int)LayoutPreset::PRESET_CENTER_TOP; + } + if (left == 0.5 && right == 0.5 && top == ANCHOR_END && bottom == ANCHOR_END) { + return (int)LayoutPreset::PRESET_CENTER_BOTTOM; + } + if (left == 0.5 && right == 0.5 && top == 0.5 && bottom == 0.5) { + return (int)LayoutPreset::PRESET_CENTER; + } + + if (left == ANCHOR_BEGIN && right == ANCHOR_BEGIN && top == ANCHOR_BEGIN && bottom == ANCHOR_END) { + return (int)LayoutPreset::PRESET_LEFT_WIDE; + } + if (left == ANCHOR_END && right == ANCHOR_END && top == ANCHOR_BEGIN && bottom == ANCHOR_END) { + return (int)LayoutPreset::PRESET_RIGHT_WIDE; + } + if (left == ANCHOR_BEGIN && right == ANCHOR_END && top == ANCHOR_BEGIN && bottom == ANCHOR_BEGIN) { + return (int)LayoutPreset::PRESET_TOP_WIDE; + } + if (left == ANCHOR_BEGIN && right == ANCHOR_END && top == ANCHOR_END && bottom == ANCHOR_END) { + return (int)LayoutPreset::PRESET_BOTTOM_WIDE; + } + + if (left == 0.5 && right == 0.5 && top == ANCHOR_BEGIN && bottom == ANCHOR_END) { + return (int)LayoutPreset::PRESET_VCENTER_WIDE; + } + if (left == ANCHOR_BEGIN && right == ANCHOR_END && top == 0.5 && bottom == 0.5) { + return (int)LayoutPreset::PRESET_HCENTER_WIDE; + } + + if (left == ANCHOR_BEGIN && right == ANCHOR_END && top == ANCHOR_BEGIN && bottom == ANCHOR_END) { + return (int)LayoutPreset::PRESET_WIDE; + } + + // Does not match any preset, return "Custom". + return -1; +} + void Control::set_anchors_preset(LayoutPreset p_preset, bool p_keep_offsets) { ERR_FAIL_INDEX((int)p_preset, 16); @@ -1670,6 +1968,62 @@ void Control::set_anchors_and_offsets_preset(LayoutPreset p_preset, LayoutPreset set_offsets_preset(p_preset, p_resize_mode, p_margin); } +void Control::set_grow_direction_preset(LayoutPreset p_preset) { + // Select correct horizontal grow direction. + switch (p_preset) { + case PRESET_TOP_LEFT: + case PRESET_BOTTOM_LEFT: + case PRESET_CENTER_LEFT: + case PRESET_LEFT_WIDE: + set_h_grow_direction(GrowDirection::GROW_DIRECTION_END); + break; + case PRESET_TOP_RIGHT: + case PRESET_BOTTOM_RIGHT: + case PRESET_CENTER_RIGHT: + case PRESET_RIGHT_WIDE: + set_h_grow_direction(GrowDirection::GROW_DIRECTION_BEGIN); + break; + case PRESET_CENTER_TOP: + case PRESET_CENTER_BOTTOM: + case PRESET_CENTER: + case PRESET_TOP_WIDE: + case PRESET_BOTTOM_WIDE: + case PRESET_VCENTER_WIDE: + case PRESET_HCENTER_WIDE: + case PRESET_WIDE: + set_h_grow_direction(GrowDirection::GROW_DIRECTION_BOTH); + break; + } + + // Select correct vertical grow direction. + switch (p_preset) { + case PRESET_TOP_LEFT: + case PRESET_TOP_RIGHT: + case PRESET_CENTER_TOP: + case PRESET_TOP_WIDE: + set_v_grow_direction(GrowDirection::GROW_DIRECTION_END); + break; + + case PRESET_BOTTOM_LEFT: + case PRESET_BOTTOM_RIGHT: + case PRESET_CENTER_BOTTOM: + case PRESET_BOTTOM_WIDE: + set_v_grow_direction(GrowDirection::GROW_DIRECTION_BEGIN); + break; + + case PRESET_CENTER_LEFT: + case PRESET_CENTER_RIGHT: + case PRESET_CENTER: + case PRESET_LEFT_WIDE: + case PRESET_RIGHT_WIDE: + case PRESET_VCENTER_WIDE: + case PRESET_HCENTER_WIDE: + case PRESET_WIDE: + set_v_grow_direction(GrowDirection::GROW_DIRECTION_BOTH); + break; + } +} + real_t Control::get_anchor(Side p_side) const { ERR_FAIL_INDEX_V(int(p_side), 4, 0.0); @@ -1715,7 +2069,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_viewport()->get_canvas_transform().xform(get_global_position()); + Point2 global_pos = get_global_transform_with_canvas().get_origin(); Window *w = Object::cast_to<Window>(get_viewport()); if (w && !w->is_embedding_subwindows()) { global_pos += w->get_position(); @@ -2179,8 +2533,7 @@ void Control::release_focus() { return; } - get_viewport()->_gui_remove_focus(); - update(); + get_viewport()->gui_release_focus(); } bool Control::is_top_level_control() const { @@ -2576,6 +2929,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; + notify_property_list_changed(); update_configuration_warnings(); } @@ -2583,104 +2937,34 @@ Control::MouseFilter Control::get_mouse_filter() const { return data.mouse_filter; } -Control *Control::get_focus_owner() const { - ERR_FAIL_COND_V(!is_inside_tree(), nullptr); - return get_viewport()->_gui_get_focus_owner(); +void Control::set_force_pass_scroll_events(bool p_force_pass_scroll_events) { + data.force_pass_scroll_events = p_force_pass_scroll_events; +} + +bool Control::is_force_pass_scroll_events() const { + return data.force_pass_scroll_events; } -void Control::warp_mouse(const Point2 &p_to_pos) { +void Control::warp_mouse(const Point2 &p_position) { ERR_FAIL_COND(!is_inside_tree()); - get_viewport()->warp_mouse(get_global_transform().xform(p_to_pos)); + get_viewport()->warp_mouse(get_global_transform_with_canvas().xform(p_position)); } bool Control::is_text_field() const { return false; } -Array Control::structured_text_parser(StructuredTextParser p_theme_type, const Array &p_args, const String &p_text) const { - Array 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())); +Array Control::structured_text_parser(TextServer::StructuredTextParser p_parser_type, const Array &p_args, const String &p_text) const { + if (p_parser_type == TextServer::STRUCTURED_TEXT_CUSTOM) { + Array ret; + if (GDVIRTUAL_CALL(_structured_text_parser, p_args, p_text, ret)) { + return ret; + } else { + return Array(); } + } else { + return TS->parse_structured_text(p_parser_type, p_args, p_text); } - return ret; } void Control::set_rotation(real_t p_radians) { @@ -2792,7 +3076,7 @@ TypedArray<String> Control::get_configuration_warnings() const { TypedArray<String> warnings = Node::get_configuration_warnings(); if (data.mouse_filter == MOUSE_FILTER_IGNORE && !data.tooltip.is_empty()) { - 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\".")); + warnings.push_back(RTR("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 warnings; @@ -2836,14 +3120,22 @@ 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_layout_mode", "mode"), &Control::_set_layout_mode); + ClassDB::bind_method(D_METHOD("_get_layout_mode"), &Control::_get_layout_mode); + ClassDB::bind_method(D_METHOD("_set_anchors_layout_preset", "preset"), &Control::_set_anchors_layout_preset); + ClassDB::bind_method(D_METHOD("_get_anchors_layout_preset"), &Control::_get_anchors_layout_preset); 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("get_offset", "offset"), &Control::get_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_offsets"), &Control::set_position, DEFVAL(false)); @@ -2857,7 +3149,6 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("set_rotation", "radians"), &Control::set_rotation); ClassDB::bind_method(D_METHOD("set_scale", "scale"), &Control::set_scale); ClassDB::bind_method(D_METHOD("set_pivot_offset", "pivot_offset"), &Control::set_pivot_offset); - ClassDB::bind_method(D_METHOD("get_offset", "offset"), &Control::get_offset); 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); @@ -2868,6 +3159,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("get_custom_minimum_size"), &Control::get_custom_minimum_size); ClassDB::bind_method(D_METHOD("get_parent_area_size"), &Control::get_parent_area_size); ClassDB::bind_method(D_METHOD("get_global_position"), &Control::get_global_position); + ClassDB::bind_method(D_METHOD("get_screen_position"), &Control::get_screen_position); ClassDB::bind_method(D_METHOD("get_rect"), &Control::get_rect); ClassDB::bind_method(D_METHOD("get_global_rect"), &Control::get_global_rect); ClassDB::bind_method(D_METHOD("set_focus_mode", "mode"), &Control::set_focus_mode); @@ -2877,7 +3169,6 @@ void Control::_bind_methods() { 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); ClassDB::bind_method(D_METHOD("get_h_size_flags"), &Control::get_h_size_flags); @@ -2966,6 +3257,9 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("set_mouse_filter", "filter"), &Control::set_mouse_filter); ClassDB::bind_method(D_METHOD("get_mouse_filter"), &Control::get_mouse_filter); + ClassDB::bind_method(D_METHOD("set_force_pass_scroll_events", "force_pass_scroll_events"), &Control::set_force_pass_scroll_events); + ClassDB::bind_method(D_METHOD("is_force_pass_scroll_events"), &Control::is_force_pass_scroll_events); + ClassDB::bind_method(D_METHOD("set_clip_contents", "enable"), &Control::set_clip_contents); ClassDB::bind_method(D_METHOD("is_clipping_contents"), &Control::is_clipping_contents); @@ -2975,7 +3269,7 @@ void Control::_bind_methods() { ClassDB::bind_method(D_METHOD("set_drag_preview", "control"), &Control::set_drag_preview); ClassDB::bind_method(D_METHOD("is_drag_successful"), &Control::is_drag_successful); - ClassDB::bind_method(D_METHOD("warp_mouse", "to_position"), &Control::warp_mouse); + ClassDB::bind_method(D_METHOD("warp_mouse", "position"), &Control::warp_mouse); ClassDB::bind_method(D_METHOD("update_minimum_size"), &Control::update_minimum_size); @@ -2986,38 +3280,53 @@ void Control::_bind_methods() { 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_GROUP("Layout", ""); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_contents"), "set_clip_contents", "is_clipping_contents"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "minimum_size"), "set_custom_minimum_size", "get_custom_minimum_size"); + 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_PROPERTY(PropertyInfo(Variant::INT, "layout_mode", PROPERTY_HINT_ENUM, "Position,Anchors,Container,Uncontrolled", PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_layout_mode", "_get_layout_mode"); + ADD_PROPERTY_DEFAULT("layout_mode", LayoutMode::LAYOUT_MODE_POSITION); + + const String anchors_presets_options = "Custom:-1,PresetWide:15," + "PresetTopLeft:0,PresetTopRight:1,PresetBottomRight:3,PresetBottomLeft:2," + "PresetCenterLeft:4,PresetCenterTop:5,PresetCenterRight:6,PresetCenterBottom:7,PresetCenter:8," + "PresetLeftWide:9,PresetTopWide:10,PresetRightWide:11,PresetBottomWide:12,PresetVCenterWide:13,PresetHCenterWide:14"; + + ADD_PROPERTY(PropertyInfo(Variant::INT, "anchors_preset", PROPERTY_HINT_ENUM, anchors_presets_options, PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_anchors_layout_preset", "_get_anchors_layout_preset"); + ADD_PROPERTY_DEFAULT("anchors_preset", -1); + + ADD_SUBGROUP_INDENT("Anchor Points", "anchor_", 1); 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("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_SUBGROUP_INDENT("Anchor Offsets", "offset_", 1); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_left", PROPERTY_HINT_RANGE, "-4096,4096,suffix:px"), "set_offset", "get_offset", SIDE_LEFT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_top", PROPERTY_HINT_RANGE, "-4096,4096,suffix:px"), "set_offset", "get_offset", SIDE_TOP); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_right", PROPERTY_HINT_RANGE, "-4096,4096,suffix:px"), "set_offset", "get_offset", SIDE_RIGHT); + ADD_PROPERTYI(PropertyInfo(Variant::INT, "offset_bottom", PROPERTY_HINT_RANGE, "-4096,4096,suffix:px"), "set_offset", "get_offset", SIDE_BOTTOM); + + ADD_SUBGROUP_INDENT("Grow Direction", "grow_", 1); + ADD_PROPERTY(PropertyInfo(Variant::INT, "grow_horizontal", PROPERTY_HINT_ENUM, "Left,Right,Both"), "set_h_grow_direction", "get_h_grow_direction"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "grow_vertical", PROPERTY_HINT_ENUM, "Top,Bottom,Both"), "set_v_grow_direction", "get_v_grow_direction"); + + ADD_SUBGROUP("Transform", ""); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "size", PROPERTY_HINT_NONE, "suffix:px", PROPERTY_USAGE_EDITOR), "_set_size", "get_size"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "position", PROPERTY_HINT_NONE, "suffix:px", PROPERTY_USAGE_EDITOR), "_set_position", "get_position"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "global_position", PROPERTY_HINT_NONE, "suffix:px", PROPERTY_USAGE_NONE), "_set_global_position", "get_global_position"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "rotation", PROPERTY_HINT_RANGE, "-360,360,0.1,or_lesser,or_greater,radians"), "set_rotation", "get_rotation"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "scale"), "set_scale", "get_scale"); + ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "pivot_offset"), "set_pivot_offset", "get_pivot_offset"); + + ADD_SUBGROUP("Container Sizing", "size_flags_"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "size_flags_horizontal", PROPERTY_HINT_FLAGS, "Fill:1,Expand:2,Shrink Center:4,Shrink End:8"), "set_h_size_flags", "get_h_size_flags"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "size_flags_vertical", PROPERTY_HINT_FLAGS, "Fill:1,Expand:2,Shrink Center:4,Shrink End:8"), "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("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, "", 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,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"); - ADD_GROUP("Hint", "hint_"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "hint_tooltip", PROPERTY_HINT_MULTILINE_TEXT), "set_tooltip", "_get_tooltip"); @@ -3032,13 +3341,9 @@ void Control::_bind_methods() { 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::BOOL, "mouse_force_pass_scroll_events"), "set_force_pass_scroll_events", "is_force_pass_scroll_events"); 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", "theme_"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "theme", PROPERTY_HINT_RESOURCE_TYPE, "Theme"), "set_theme", "get_theme"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "theme_type_variation", PROPERTY_HINT_ENUM_SUGGESTION), "set_theme_type_variation", "get_theme_type_variation"); @@ -3097,6 +3402,7 @@ void Control::_bind_methods() { BIND_ENUM_CONSTANT(PRESET_MODE_KEEP_HEIGHT); BIND_ENUM_CONSTANT(PRESET_MODE_KEEP_SIZE); + BIND_ENUM_CONSTANT(SIZE_SHRINK_BEGIN); BIND_ENUM_CONSTANT(SIZE_FILL); BIND_ENUM_CONSTANT(SIZE_EXPAND); BIND_ENUM_CONSTANT(SIZE_EXPAND_FILL); @@ -3124,14 +3430,6 @@ void Control::_bind_methods() { 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")); diff --git a/scene/gui/control.h b/scene/gui/control.h index ae9ca1489a..f18dd99bff 100644 --- a/scene/gui/control.h +++ b/scene/gui/control.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,12 +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/main/canvas_item.h" -#include "scene/main/node.h" #include "scene/main/timer.h" #include "scene/resources/theme.h" @@ -46,7 +44,6 @@ class Panel; class Control : public CanvasItem { GDCLASS(Control, CanvasItem); - OBJ_CATEGORY("GUI Nodes"); public: enum Anchor { @@ -67,12 +64,13 @@ public: }; enum SizeFlags { + SIZE_SHRINK_BEGIN = 0, SIZE_FILL = 1, SIZE_EXPAND = 2, - SIZE_EXPAND_FILL = SIZE_EXPAND | SIZE_FILL, - SIZE_SHRINK_CENTER = 4, //ignored by expand or fill - SIZE_SHRINK_END = 8, //ignored by expand or fill + SIZE_SHRINK_CENTER = 4, + SIZE_SHRINK_END = 8, + SIZE_EXPAND_FILL = SIZE_EXPAND | SIZE_FILL, }; enum MouseFilter { @@ -128,6 +126,13 @@ public: PRESET_MODE_KEEP_SIZE }; + enum LayoutMode { + LAYOUT_MODE_POSITION, + LAYOUT_MODE_ANCHORS, + LAYOUT_MODE_CONTAINER, + LAYOUT_MODE_UNCONTROLLED, + }; + enum LayoutDirection { LAYOUT_DIRECTION_INHERITED, LAYOUT_DIRECTION_LOCALE, @@ -142,16 +147,6 @@ public: 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 { @@ -195,6 +190,7 @@ private: Point2 custom_minimum_size; MouseFilter mouse_filter = MOUSE_FILTER_STOP; + bool force_pass_scroll_events = true; bool clip_contents = false; @@ -221,16 +217,16 @@ private: NodePath focus_prev; bool bulk_theme_override = false; - HashMap<StringName, Ref<Texture2D>> icon_override; - HashMap<StringName, Ref<StyleBox>> style_override; - HashMap<StringName, Ref<Font>> font_override; - HashMap<StringName, int> font_size_override; - HashMap<StringName, Color> color_override; - HashMap<StringName, int> constant_override; + Theme::ThemeIconMap icon_override; + Theme::ThemeStyleMap style_override; + Theme::ThemeFontMap font_override; + Theme::ThemeFontSizeMap font_size_override; + Theme::ThemeColorMap color_override; + Theme::ThemeConstantMap constant_override; } data; - static constexpr unsigned properties_managed_by_container_count = 11; + static constexpr unsigned properties_managed_by_container_count = 12; static String properties_managed_by_container[properties_managed_by_container_count]; 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); @@ -241,6 +237,12 @@ private: void _set_global_position(const Point2 &p_point); void _set_size(const Size2 &p_size); + void _set_layout_mode(LayoutMode p_mode); + LayoutMode _get_layout_mode() const; + + void _set_anchors_layout_preset(int p_preset); + int _get_anchors_layout_preset() const; + void _theme_changed(); void _notify_theme_changed(); @@ -279,15 +281,16 @@ protected: //virtual void _window_gui_input(InputEvent p_event); - virtual Array structured_text_parser(StructuredTextParser p_theme_type, const Array &p_args, const String &p_text) const; + virtual Array structured_text_parser(TextServer::StructuredTextParser p_parser_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; + virtual void _validate_property(PropertyInfo &property) const override; + void _notification(int p_notification); static void _bind_methods(); - virtual void _validate_property(PropertyInfo &property) const override; //bind helpers @@ -378,6 +381,7 @@ public: 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_grow_direction_preset(LayoutPreset p_preset); 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; @@ -462,11 +466,12 @@ public: void set_focus_previous(const NodePath &p_prev); NodePath get_focus_previous() const; - Control *get_focus_owner() const; - void set_mouse_filter(MouseFilter p_filter); MouseFilter get_mouse_filter() const; + void set_force_pass_scroll_events(bool p_force_pass_scroll_events); + bool is_force_pass_scroll_events() const; + /* SKINNING */ void begin_bulk_theme_override(); @@ -536,7 +541,7 @@ public: void grab_click_focus(); - void warp_mouse(const Point2 &p_to_pos); + void warp_mouse(const Point2 &p_position); virtual bool is_text_field() const; @@ -565,8 +570,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::LayoutMode); 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 f1725e57ea..0bb96a18a5 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,24 +33,19 @@ #include "core/os/keyboard.h" #include "core/string/print_string.h" #include "core/string/translation.h" -#include "line_edit.h" - -#ifdef TOOLS_ENABLED -#include "editor/editor_node.h" -#include "scene/main/window.h" // Only used to check for more modals when dimming the editor. -#endif +#include "scene/gui/line_edit.h" // AcceptDialog void AcceptDialog::_input_from_window(const Ref<InputEvent> &p_event) { Ref<InputEventKey> key = p_event; - if (key.is_valid() && key->is_pressed() && key->get_keycode() == Key::ESCAPE) { + if (close_on_escape && key.is_valid() && key->is_pressed() && key->get_keycode() == Key::ESCAPE) { _cancel_pressed(); } } void AcceptDialog::_parent_focused() { - if (!is_exclusive()) { + if (close_on_escape && !is_exclusive()) { _cancel_pressed(); } } @@ -72,21 +67,25 @@ void AcceptDialog::_notification(int p_what) { } } } break; + case NOTIFICATION_THEME_CHANGED: { 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)); parent_visible = nullptr; } } break; + case NOTIFICATION_READY: case NOTIFICATION_WM_SIZE_CHANGED: { if (is_visible()) { _update_child_rects(); } } break; + case NOTIFICATION_WM_CLOSE_REQUEST: { _cancel_pressed(); } break; @@ -94,6 +93,9 @@ void AcceptDialog::_notification(int p_what) { } void AcceptDialog::_text_submitted(const String &p_text) { + if (get_ok_button() && get_ok_button()->is_disabled()) { + return; // Do not allow submission if OK button is disabled. + } _ok_pressed(); } @@ -143,6 +145,14 @@ bool AcceptDialog::get_hide_on_ok() const { return hide_on_ok; } +void AcceptDialog::set_close_on_escape(bool p_hide) { + close_on_escape = p_hide; +} + +bool AcceptDialog::get_close_on_escape() const { + return close_on_escape; +} + void AcceptDialog::set_autowrap(bool p_autowrap) { label->set_autowrap_mode(p_autowrap ? Label::AUTOWRAP_WORD : Label::AUTOWRAP_OFF); } @@ -286,6 +296,8 @@ void AcceptDialog::_bind_methods() { 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("set_close_on_escape", "enabled"), &AcceptDialog::set_close_on_escape); + ClassDB::bind_method(D_METHOD("get_close_on_escape"), &AcceptDialog::get_close_on_escape); ClassDB::bind_method(D_METHOD("add_button", "text", "right", "action"), &AcceptDialog::add_button, DEFVAL(false), DEFVAL("")); ClassDB::bind_method(D_METHOD("add_cancel_button", "name"), &AcceptDialog::add_cancel_button); ClassDB::bind_method(D_METHOD("remove_button", "button"), &AcceptDialog::remove_button); @@ -302,6 +314,7 @@ void AcceptDialog::_bind_methods() { ADD_GROUP("Dialog", "dialog"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "dialog_text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dialog_hide_on_ok"), "set_hide_on_ok", "get_hide_on_ok"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dialog_close_on_escape"), "set_close_on_escape", "get_close_on_escape"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dialog_autowrap"), "set_autowrap", "has_autowrap"); } diff --git a/scene/gui/dialogs.h b/scene/gui/dialogs.h index 8e803a2a7c..41fd9c0a10 100644 --- a/scene/gui/dialogs.h +++ b/scene/gui/dialogs.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,11 +45,12 @@ class AcceptDialog : public Window { GDCLASS(AcceptDialog, Window); Window *parent_visible = nullptr; - Panel *bg; - HBoxContainer *hbc; - Label *label; - Button *ok; + Panel *bg = nullptr; + HBoxContainer *hbc = nullptr; + Label *label = nullptr; + Button *ok = nullptr; bool hide_on_ok = true; + bool close_on_escape = true; void _custom_action(const String &p_action); void _update_child_rects(); @@ -87,6 +88,9 @@ public: void set_hide_on_ok(bool p_hide); bool get_hide_on_ok() const; + void set_close_on_escape(bool p_enable); + bool get_close_on_escape() const; + void set_text(String p_text); String get_text() const; @@ -99,7 +103,7 @@ public: class ConfirmationDialog : public AcceptDialog { GDCLASS(ConfirmationDialog, AcceptDialog); - Button *cancel; + Button *cancel = nullptr; protected: static void _bind_methods(); diff --git a/scene/gui/file_dialog.cpp b/scene/gui/file_dialog.cpp index 08d5df3db8..1725816c31 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +42,17 @@ FileDialog::RegisterFunc FileDialog::unregister_func = nullptr; void FileDialog::popup_file_dialog() { popup_centered_clamped(Size2i(700, 500), 0.8f); + _focus_file_text(); +} + +void FileDialog::_focus_file_text() { + int lp = file->get_text().rfind("."); + if (lp != -1) { + file->select(0, lp); + if (file->is_inside_tree() && !get_tree()->is_node_being_edited(file)) { + file->grab_focus(); + } + } } VBoxContainer *FileDialog::get_vbox() { @@ -81,27 +92,34 @@ void FileDialog::_theme_changed() { } void FileDialog::_notification(int p_what) { - if (p_what == NOTIFICATION_VISIBILITY_CHANGED) { - if (!is_visible()) { - set_process_unhandled_input(false); - } - } - if (p_what == NOTIFICATION_ENTER_TREE) { - 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(); + switch (p_what) { + case NOTIFICATION_VISIBILITY_CHANGED: { + if (!is_visible()) { + set_process_shortcut_input(false); + } + } break; + + case NOTIFICATION_ENTER_TREE: { + 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(); + } break; + + case NOTIFICATION_TRANSLATION_CHANGED: { + update_filters(); + } break; } } -void FileDialog::unhandled_input(const Ref<InputEvent> &p_event) { +void FileDialog::shortcut_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); Ref<InputEventKey> k = p_event; @@ -155,7 +173,14 @@ void FileDialog::update_dir() { dir->set_text(dir_access->get_current_dir(false)); if (drives->is_visible()) { - drives->select(dir_access->get_current_drive()); + if (dir_access->get_current_dir().is_network_share_path()) { + _update_drives(false); + drives->add_item(RTR("Network")); + drives->set_item_disabled(-1, true); + drives->select(drives->get_item_count() - 1); + } else { + drives->select(dir_access->get_current_drive()); + } } // Deselect any item, to make "Select Current Folder" button text by default. @@ -192,7 +217,7 @@ void FileDialog::_post_popup() { tree->grab_focus(); } - set_process_unhandled_input(true); + set_process_shortcut_input(true); // For open dir mode, deselect all items on file dialog open. if (mode == FILE_MODE_OPEN_DIR) { @@ -237,7 +262,8 @@ void FileDialog::_action_pressed() { return; } - String f = dir_access->get_current_dir().plus_file(file->get_text()); + String file_text = file->get_text(); + String f = file_text.is_absolute_path() ? file_text : dir_access->get_current_dir().plus_file(file_text); if ((mode == FILE_MODE_OPEN_ANY || mode == FILE_MODE_OPEN_FILE) && dir_access->file_exists(f)) { emit_signal(SNAME("file_selected"), f); @@ -627,7 +653,7 @@ void FileDialog::update_filters() { all_filters += ", ..."; } - filter->add_item(String(TTRC("All Recognized")) + " (" + all_filters + ")"); + filter->add_item(RTR("All Recognized") + " (" + all_filters + ")"); } for (int i = 0; i < filters.size(); i++) { String flt = filters[i].get_slice(";", 0).strip_edges(); @@ -639,7 +665,7 @@ void FileDialog::update_filters() { } } - filter->add_item(TTRC("All Files (*)")); + filter->add_item(RTR("All Files") + " (*)"); } void FileDialog::clear_filters() { @@ -649,6 +675,7 @@ void FileDialog::clear_filters() { } void FileDialog::add_filter(const String &p_filter) { + ERR_FAIL_COND_MSG(p_filter.begins_with("."), "Filter must be \"filename.extension\", can't start with dot."); filters.push_back(p_filter); update_filters(); invalidate(); @@ -687,13 +714,7 @@ void FileDialog::set_current_file(const String &p_file) { file->set_text(p_file); update_dir(); invalidate(); - int lp = p_file.rfind("."); - if (lp != -1) { - file->select(0, lp); - if (file->is_inside_tree() && !get_tree()->is_node_being_edited(file)) { - file->grab_focus(); - } - } + _focus_file_text(); } void FileDialog::set_current_path(const String &p_path) { @@ -777,7 +798,6 @@ void FileDialog::set_access(Access p_access) { if (access == p_access) { return; } - memdelete(dir_access); switch (p_access) { case ACCESS_FILESYSTEM: { dir_access = DirAccess::create(DirAccess::ACCESS_FILESYSTEM); @@ -837,7 +857,7 @@ void FileDialog::_select_drive(int p_idx) { _push_history(); } -void FileDialog::_update_drives() { +void FileDialog::_update_drives(bool p_select) { int dc = dir_access->get_drive_count(); if (dc == 0 || access != ACCESS_FILESYSTEM) { drives->hide(); @@ -855,7 +875,9 @@ void FileDialog::_update_drives() { drives->add_item(dir_access->get_drive(i)); } - drives->select(dir_access->get_current_drive()); + if (p_select) { + drives->select(dir_access->get_current_drive()); + } } } @@ -893,12 +915,12 @@ void FileDialog::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "mode_overrides_title"), "set_mode_overrides_title", "is_mode_overriding_title"); ADD_PROPERTY(PropertyInfo(Variant::INT, "file_mode", PROPERTY_HINT_ENUM, "Open File,Open Files,Open Folder,Open Any,Save"), "set_file_mode", "get_file_mode"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "access", PROPERTY_HINT_ENUM, "Resources,User data,File system"), "set_access", "get_access"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "access", PROPERTY_HINT_ENUM, "Resources,User Data,File System"), "set_access", "get_access"); ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "filters"), "set_filters", "get_filters"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "show_hidden_files"), "set_show_hidden_files", "is_showing_hidden_files"); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_dir"), "set_current_dir", "get_current_dir"); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_file"), "set_current_file", "get_current_file"); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_path"), "set_current_path", "get_current_path"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_dir", PROPERTY_HINT_DIR, "", PROPERTY_USAGE_NONE), "set_current_dir", "get_current_dir"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_file", PROPERTY_HINT_FILE, "*", PROPERTY_USAGE_NONE), "set_current_file", "get_current_file"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "current_path", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NONE), "set_current_path", "get_current_path"); ADD_SIGNAL(MethodInfo("file_selected", PropertyInfo(Variant::STRING, "path"))); ADD_SIGNAL(MethodInfo("files_selected", PropertyInfo(Variant::PACKED_STRING_ARRAY, "paths"))); @@ -966,7 +988,7 @@ FileDialog::FileDialog() { hbc->add_child(drives); dir = memnew(LineEdit); - dir->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); + dir->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE); hbc->add_child(dir); dir->set_h_size_flags(Control::SIZE_EXPAND_FILL); @@ -1007,7 +1029,7 @@ FileDialog::FileDialog() { file_box = memnew(HBoxContainer); file_box->add_child(memnew(Label(TTRC("File:")))); file = memnew(LineEdit); - file->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); + file->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE); file->set_stretch_ratio(4); file->set_h_size_flags(Control::SIZE_EXPAND_FILL); file_box->add_child(file); @@ -1041,7 +1063,7 @@ FileDialog::FileDialog() { makedialog->add_child(makevb); makedirname = memnew(LineEdit); - makedirname->set_structured_text_bidi_override(Control::STRUCTURED_TEXT_FILE); + makedirname->set_structured_text_bidi_override(TextServer::STRUCTURED_TEXT_FILE); makevb->add_margin_child(TTRC("Name:"), makedirname); add_child(makedialog, false, INTERNAL_MODE_FRONT); makedialog->register_text_enter(makedirname); @@ -1068,5 +1090,4 @@ FileDialog::~FileDialog() { if (unregister_func) { unregister_func(this); } - memdelete(dir_access); } diff --git a/scene/gui/file_dialog.h b/scene/gui/file_dialog.h index b5190bdab1..2e326d2949 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -65,35 +65,34 @@ public: static RegisterFunc unregister_func; private: - ConfirmationDialog *makedialog; - LineEdit *makedirname; + ConfirmationDialog *makedialog = nullptr; + LineEdit *makedirname = nullptr; - Button *makedir; + Button *makedir = nullptr; Access access = ACCESS_RESOURCES; - //Button *action; - VBoxContainer *vbox; + VBoxContainer *vbox = nullptr; FileMode mode; - LineEdit *dir; - HBoxContainer *drives_container; - HBoxContainer *shortcuts_container; - OptionButton *drives; - Tree *tree; - HBoxContainer *file_box; - LineEdit *file; - OptionButton *filter; - AcceptDialog *mkdirerr; - AcceptDialog *exterr; - DirAccess *dir_access; - ConfirmationDialog *confirm_save; - - Label *message; - - Button *dir_prev; - Button *dir_next; - Button *dir_up; - - Button *refresh; - Button *show_hidden; + LineEdit *dir = nullptr; + HBoxContainer *drives_container = nullptr; + HBoxContainer *shortcuts_container = nullptr; + OptionButton *drives = nullptr; + Tree *tree = nullptr; + HBoxContainer *file_box = nullptr; + LineEdit *file = nullptr; + OptionButton *filter = nullptr; + AcceptDialog *mkdirerr = nullptr; + AcceptDialog *exterr = nullptr; + Ref<DirAccess> dir_access; + ConfirmationDialog *confirm_save = nullptr; + + Label *message = nullptr; + + Button *dir_prev = nullptr; + Button *dir_next = nullptr; + Button *dir_up = nullptr; + + Button *refresh = nullptr; + Button *show_hidden = nullptr; Vector<String> filters; @@ -113,6 +112,8 @@ private: void update_file_list(); void update_filters(); + void _focus_file_text(); + void _tree_multi_selected(Object *p_object, int p_cell, bool p_selected); void _tree_selected(); @@ -130,9 +131,9 @@ private: void _go_back(); void _go_forward(); - void _update_drives(); + void _update_drives(bool p_select = true); - virtual void unhandled_input(const Ref<InputEvent> &p_event) override; + virtual void shortcut_input(const Ref<InputEvent> &p_event) override; bool _is_open_should_be_disabled(); diff --git a/scene/gui/flow_container.cpp b/scene/gui/flow_container.cpp new file mode 100644 index 0000000000..30b694da76 --- /dev/null +++ b/scene/gui/flow_container.cpp @@ -0,0 +1,281 @@ +/*************************************************************************/ +/* flow_container.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "flow_container.h" + +struct _LineData { + int child_count = 0; + int min_line_height = 0; + int min_line_length = 0; + int stretch_avail = 0; + float stretch_ratio_total = 0; +}; + +void FlowContainer::_resort() { + // Avoid resorting if invisible. + if (!is_visible_in_tree()) { + return; + } + + int separation_horizontal = get_theme_constant(SNAME("h_separation")); + int separation_vertical = get_theme_constant(SNAME("v_separation")); + + bool rtl = is_layout_rtl(); + + HashMap<Control *, Size2i> children_minsize_cache; + + Vector<_LineData> lines_data; + + Vector2i ofs; + int line_height = 0; + int line_length = 0; + float line_stretch_ratio_total = 0; + int current_container_size = vertical ? get_rect().size.y : get_rect().size.x; + int children_in_current_line = 0; + + // First pass for line wrapping and minimum size calculation. + for (int i = 0; i < get_child_count(); i++) { + Control *child = Object::cast_to<Control>(get_child(i)); + if (!child || !child->is_visible()) { + continue; + } + if (child->is_set_as_top_level()) { + continue; + } + + Size2i child_msc = child->get_combined_minimum_size(); + + if (vertical) { /* VERTICAL */ + if (children_in_current_line > 0) { + ofs.y += separation_vertical; + } + if (ofs.y + child_msc.y > current_container_size) { + line_length = ofs.y - separation_vertical; + lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total }); + + // Move in new column (vertical line). + ofs.x += line_height + separation_horizontal; + ofs.y = 0; + line_height = 0; + line_stretch_ratio_total = 0; + children_in_current_line = 0; + } + + line_height = MAX(line_height, child_msc.x); + if (child->get_v_size_flags() & SIZE_EXPAND) { + line_stretch_ratio_total += child->get_stretch_ratio(); + } + ofs.y += child_msc.y; + + } else { /* HORIZONTAL */ + if (children_in_current_line > 0) { + ofs.x += separation_horizontal; + } + if (ofs.x + child_msc.x > current_container_size) { + line_length = ofs.x - separation_horizontal; + lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total }); + + // Move in new line. + ofs.y += line_height + separation_vertical; + ofs.x = 0; + line_height = 0; + line_stretch_ratio_total = 0; + children_in_current_line = 0; + } + + line_height = MAX(line_height, child_msc.y); + if (child->get_h_size_flags() & SIZE_EXPAND) { + line_stretch_ratio_total += child->get_stretch_ratio(); + } + ofs.x += child_msc.x; + } + + children_minsize_cache[child] = child_msc; + children_in_current_line++; + } + line_length = vertical ? (ofs.y) : (ofs.x); + lines_data.push_back(_LineData{ children_in_current_line, line_height, line_length, current_container_size - line_length, line_stretch_ratio_total }); + + // Second pass for in-line expansion and alignment. + + int current_line_idx = 0; + int child_idx_in_line = 0; + + ofs.x = 0; + ofs.y = 0; + + for (int i = 0; i < get_child_count(); i++) { + Control *child = Object::cast_to<Control>(get_child(i)); + if (!child || !child->is_visible()) { + continue; + } + if (child->is_set_as_top_level()) { + continue; + } + Size2i child_size = children_minsize_cache[child]; + + _LineData line_data = lines_data[current_line_idx]; + if (child_idx_in_line >= lines_data[current_line_idx].child_count) { + current_line_idx++; + child_idx_in_line = 0; + if (vertical) { + ofs.x += line_data.min_line_height + separation_horizontal; + ofs.y = 0; + } else { + ofs.x = 0; + ofs.y += line_data.min_line_height + separation_vertical; + } + line_data = lines_data[current_line_idx]; + } + + if (vertical) { /* VERTICAL */ + if (child->get_h_size_flags() & (SIZE_FILL | SIZE_SHRINK_CENTER | SIZE_SHRINK_END)) { + child_size.width = line_data.min_line_height; + } + + if (child->get_v_size_flags() & SIZE_EXPAND) { + int stretch = line_data.stretch_avail * child->get_stretch_ratio() / line_data.stretch_ratio_total; + child_size.height += stretch; + } + + } else { /* HORIZONTAL */ + if (child->get_v_size_flags() & (SIZE_FILL | SIZE_SHRINK_CENTER | SIZE_SHRINK_END)) { + child_size.height = line_data.min_line_height; + } + + if (child->get_h_size_flags() & SIZE_EXPAND) { + int stretch = line_data.stretch_avail * child->get_stretch_ratio() / line_data.stretch_ratio_total; + child_size.width += stretch; + } + } + + Rect2 child_rect = Rect2(ofs, child_size); + if (rtl) { + child_rect.position.x = get_rect().size.x - child_rect.position.x - child_rect.size.width; + } + + fit_child_in_rect(child, child_rect); + + if (vertical) { /* VERTICAL */ + ofs.y += child_size.height + separation_vertical; + } else { /* HORIZONTAL */ + ofs.x += child_size.width + separation_horizontal; + } + + child_idx_in_line++; + } + cached_size = (vertical ? ofs.x : ofs.y) + line_height; + cached_line_count = lines_data.size(); +} + +Size2 FlowContainer::get_minimum_size() const { + Size2i minimum; + + 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; + } + + Size2i size = c->get_combined_minimum_size(); + + if (vertical) { /* VERTICAL */ + minimum.height = MAX(minimum.height, size.height); + minimum.width = cached_size; + + } else { /* HORIZONTAL */ + minimum.width = MAX(minimum.width, size.width); + minimum.height = cached_size; + } + } + + return minimum; +} + +Vector<int> FlowContainer::get_allowed_size_flags_horizontal() const { + Vector<int> flags; + flags.append(SIZE_FILL); + if (!vertical) { + flags.append(SIZE_EXPAND); + } + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + +Vector<int> FlowContainer::get_allowed_size_flags_vertical() const { + Vector<int> flags; + flags.append(SIZE_FILL); + if (vertical) { + flags.append(SIZE_EXPAND); + } + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + +void FlowContainer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_SORT_CHILDREN: { + _resort(); + update_minimum_size(); + } break; + + case NOTIFICATION_THEME_CHANGED: { + update_minimum_size(); + } break; + + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + queue_sort(); + } break; + } +} + +int FlowContainer::get_line_count() const { + return cached_line_count; +} + +FlowContainer::FlowContainer(bool p_vertical) { + vertical = p_vertical; +} + +void FlowContainer::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_line_count"), &FlowContainer::get_line_count); +} diff --git a/scene/gui/flow_container.h b/scene/gui/flow_container.h new file mode 100644 index 0000000000..a2da43e071 --- /dev/null +++ b/scene/gui/flow_container.h @@ -0,0 +1,79 @@ +/*************************************************************************/ +/* flow_container.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef FLOW_CONTAINER_H +#define FLOW_CONTAINER_H + +#include "scene/gui/container.h" + +class FlowContainer : public Container { + GDCLASS(FlowContainer, Container); + +private: + int cached_size = 0; + int cached_line_count = 0; + + bool vertical = false; + + void _resort(); + +protected: + void _notification(int p_what); + + static void _bind_methods(); + +public: + int get_line_count() const; + + virtual Size2 get_minimum_size() const override; + + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + + FlowContainer(bool p_vertical = false); +}; + +class HFlowContainer : public FlowContainer { + GDCLASS(HFlowContainer, FlowContainer); + +public: + HFlowContainer() : + FlowContainer(false) {} +}; + +class VFlowContainer : public FlowContainer { + GDCLASS(VFlowContainer, FlowContainer); + +public: + VFlowContainer() : + FlowContainer(true) {} +}; + +#endif // FLOW_CONTAINER_H diff --git a/scene/gui/gradient_edit.cpp b/scene/gui/gradient_edit.cpp index 1210be15ce..0690acbe16 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -287,84 +287,85 @@ void GradientEdit::gui_input(const Ref<InputEvent> &p_event) { } void GradientEdit::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE) { - if (!picker->is_connected("color_changed", callable_mp(this, &GradientEdit::_color_changed))) { - picker->connect("color_changed", callable_mp(this, &GradientEdit::_color_changed)); + switch (p_what) { + case NOTIFICATION_ENTER_TREE: { + if (!picker->is_connected("color_changed", callable_mp(this, &GradientEdit::_color_changed))) { + picker->connect("color_changed", callable_mp(this, &GradientEdit::_color_changed)); + } + [[fallthrough]]; } - } + case NOTIFICATION_THEME_CHANGED: { + draw_spacing = BASE_SPACING * get_theme_default_base_scale(); + draw_point_width = BASE_POINT_WIDTH * get_theme_default_base_scale(); + } break; - if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED) { - draw_spacing = BASE_SPACING * get_theme_default_base_scale(); - draw_point_width = BASE_POINT_WIDTH * get_theme_default_base_scale(); - } + case NOTIFICATION_DRAW: { + int w = get_size().x; + int h = get_size().y; - if (p_what == NOTIFICATION_DRAW) { - int w = get_size().x; - int h = get_size().y; + if (w == 0 || h == 0) { + return; // Safety check. We have division by 'h'. And in any case there is nothing to draw with such size. + } - if (w == 0 || h == 0) { - return; // Safety check. We have division by 'h'. And in any case there is nothing to draw with such size. - } + int total_w = get_size().width - get_size().height - draw_spacing; - int total_w = get_size().width - get_size().height - draw_spacing; + // Draw checker pattern for ramp. + draw_texture_rect(get_theme_icon(SNAME("GuiMiniCheckerboard"), SNAME("EditorIcons")), Rect2(0, 0, total_w, h), true); - // Draw checker pattern for ramp. - draw_texture_rect(get_theme_icon(SNAME("GuiMiniCheckerboard"), SNAME("EditorIcons")), Rect2(0, 0, total_w, h), true); + // Draw color ramp. + gradient_cache->set_points(points); + gradient_cache->set_interpolation_mode(interpolation_mode); + preview_texture->set_gradient(gradient_cache); + draw_texture_rect(preview_texture, Rect2(0, 0, total_w, h)); - // Draw color ramp. + // Draw point markers. + for (int i = 0; i < points.size(); i++) { + Color col = points[i].color.inverted(); + col.a = 0.9; - gradient_cache->set_points(points); - gradient_cache->set_interpolation_mode(interpolation_mode); - preview_texture->set_gradient(gradient_cache); - draw_texture_rect(preview_texture, Rect2(0, 0, total_w, h)); + draw_line(Vector2(points[i].offset * total_w, 0), Vector2(points[i].offset * total_w, h / 2), col); + Rect2 rect = Rect2(points[i].offset * total_w - draw_point_width / 2, h / 2, draw_point_width, h / 2); + draw_rect(rect, points[i].color, true); + draw_rect(rect, col, false); + if (grabbed == i) { + rect = rect.grow(-1); + if (has_focus()) { + draw_rect(rect, Color(1, 0, 0, 0.9), false); + } else { + draw_rect(rect, Color(0.6, 0, 0, 0.9), false); + } - // Draw point markers. - for (int i = 0; i < points.size(); i++) { - Color col = points[i].color.inverted(); - col.a = 0.9; - - draw_line(Vector2(points[i].offset * total_w, 0), Vector2(points[i].offset * total_w, h / 2), col); - Rect2 rect = Rect2(points[i].offset * total_w - draw_point_width / 2, h / 2, draw_point_width, h / 2); - draw_rect(rect, points[i].color, true); - draw_rect(rect, col, false); - if (grabbed == i) { - rect = rect.grow(-1); - if (has_focus()) { - draw_rect(rect, Color(1, 0, 0, 0.9), false); - } else { - draw_rect(rect, Color(0.6, 0, 0, 0.9), false); + rect = rect.grow(-1); + draw_rect(rect, col, false); } - - rect = rect.grow(-1); - draw_rect(rect, col, false); } - } - //Draw "button" for color selector - draw_texture_rect(get_theme_icon(SNAME("GuiMiniCheckerboard"), SNAME("EditorIcons")), Rect2(total_w + draw_spacing, 0, h, h), true); - if (grabbed != -1) { - //Draw with selection color - draw_rect(Rect2(total_w + draw_spacing, 0, h, h), points[grabbed].color); - } else { - //if no color selected draw grey color with 'X' on top. - draw_rect(Rect2(total_w + draw_spacing, 0, h, h), Color(0.5, 0.5, 0.5, 1)); - draw_line(Vector2(total_w + draw_spacing, 0), Vector2(total_w + draw_spacing + h, h), Color(1, 1, 1, 0.6)); - draw_line(Vector2(total_w + draw_spacing, h), Vector2(total_w + draw_spacing + h, 0), Color(1, 1, 1, 0.6)); - } + // Draw "button" for color selector. + draw_texture_rect(get_theme_icon(SNAME("GuiMiniCheckerboard"), SNAME("EditorIcons")), Rect2(total_w + draw_spacing, 0, h, h), true); + if (grabbed != -1) { + // Draw with selection color. + draw_rect(Rect2(total_w + draw_spacing, 0, h, h), points[grabbed].color); + } else { + // If no color selected draw grey color with 'X' on top. + draw_rect(Rect2(total_w + draw_spacing, 0, h, h), Color(0.5, 0.5, 0.5, 1)); + draw_line(Vector2(total_w + draw_spacing, 0), Vector2(total_w + draw_spacing + h, h), Color(1, 1, 1, 0.6)); + draw_line(Vector2(total_w + draw_spacing, h), Vector2(total_w + draw_spacing + h, 0), Color(1, 1, 1, 0.6)); + } - // Draw borders around color ramp if in focus. - if (has_focus()) { - draw_line(Vector2(-1, -1), Vector2(total_w + 1, -1), Color(1, 1, 1, 0.6)); - draw_line(Vector2(total_w + 1, -1), Vector2(total_w + 1, h + 1), Color(1, 1, 1, 0.6)); - draw_line(Vector2(total_w + 1, h + 1), Vector2(-1, h + 1), Color(1, 1, 1, 0.6)); - draw_line(Vector2(-1, -1), Vector2(-1, h + 1), Color(1, 1, 1, 0.6)); - } - } + // Draw borders around color ramp if in focus. + if (has_focus()) { + draw_line(Vector2(-1, -1), Vector2(total_w + 1, -1), Color(1, 1, 1, 0.6)); + draw_line(Vector2(total_w + 1, -1), Vector2(total_w + 1, h + 1), Color(1, 1, 1, 0.6)); + draw_line(Vector2(total_w + 1, h + 1), Vector2(-1, h + 1), Color(1, 1, 1, 0.6)); + draw_line(Vector2(-1, -1), Vector2(-1, h + 1), Color(1, 1, 1, 0.6)); + } + } break; - if (p_what == NOTIFICATION_VISIBILITY_CHANGED) { - if (!is_visible()) { - grabbing = false; - } + case NOTIFICATION_VISIBILITY_CHANGED: { + if (!is_visible()) { + grabbing = false; + } + } break; } } @@ -432,6 +433,10 @@ Gradient::InterpolationMode GradientEdit::get_interpolation_mode() { return interpolation_mode; } +ColorPicker *GradientEdit::get_picker() { + return picker; +} + void GradientEdit::_bind_methods() { ADD_SIGNAL(MethodInfo("ramp_changed")); } diff --git a/scene/gui/gradient_edit.h b/scene/gui/gradient_edit.h index 66b60d87c7..4e3c6525f9 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,14 +33,13 @@ #include "scene/gui/color_picker.h" #include "scene/gui/popup.h" -#include "scene/resources/default_theme/theme_data.h" #include "scene/resources/gradient.h" class GradientEdit : public Control { GDCLASS(GradientEdit, Control); - PopupPanel *popup; - ColorPicker *picker; + PopupPanel *popup = nullptr; + ColorPicker *picker = nullptr; bool grabbing = false; int grabbed = -1; @@ -75,6 +74,7 @@ public: Vector<Gradient::Point> &get_points(); void set_interpolation_mode(Gradient::InterpolationMode p_interp_mode); Gradient::InterpolationMode get_interpolation_mode(); + ColorPicker *get_picker(); virtual Size2 get_minimum_size() const override; diff --git a/scene/gui/graph_edit.cpp b/scene/gui/graph_edit.cpp index 32c62b7226..8ad55fc6ef 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +35,7 @@ #include "core/os/keyboard.h" #include "scene/gui/box_container.h" #include "scene/gui/button.h" +#include "scene/gui/view_panner.h" constexpr int MINIMAP_OFFSET = 12; constexpr int MINIMAP_PADDING = 5; @@ -422,89 +423,94 @@ void GraphEdit::remove_child_notify(Node *p_child) { } void GraphEdit::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED) { - port_grab_distance_horizontal = get_theme_constant(SNAME("port_grab_distance_horizontal")); - port_grab_distance_vertical = get_theme_constant(SNAME("port_grab_distance_vertical")); + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: { + port_hotzone_inner_extent = get_theme_constant("port_hotzone_inner_extent"); + port_hotzone_outer_extent = get_theme_constant("port_hotzone_outer_extent"); + + 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"))); + + zoom_label->set_custom_minimum_size(Size2(48, 0) * get_theme_default_base_scale()); + } break; - 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"))); + case NOTIFICATION_READY: { + Size2 hmin = h_scroll->get_combined_minimum_size(); + Size2 vmin = v_scroll->get_combined_minimum_size(); - zoom_label->set_custom_minimum_size(Size2(48, 0) * get_theme_default_base_scale()); - } - 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_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); - 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_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); + } break; - 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(SNAME("bg")), Rect2(Point2(), get_size())); + case NOTIFICATION_DRAW: { + draw_style_box(get_theme_stylebox(SNAME("bg")), Rect2(Point2(), get_size())); - if (is_using_snap()) { - //draw grid + if (is_using_snap()) { + // Draw grid. + int snap = get_snap(); - int snap = get_snap(); + Vector2 offset = get_scroll_ofs() / zoom; + Size2 size = get_size() / zoom; - Vector2 offset = get_scroll_ofs() / zoom; - Size2 size = get_size() / zoom; + Point2i from = (offset / float(snap)).floor(); + Point2i len = (size / float(snap)).floor() + Vector2(1, 1); - Point2i from = (offset / float(snap)).floor(); - Point2i len = (size / float(snap)).floor() + Vector2(1, 1); + Color grid_minor = get_theme_color(SNAME("grid_minor")); + Color grid_major = get_theme_color(SNAME("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; - for (int i = from.x; i < from.x + len.x; i++) { - Color color; + if (ABS(i) % 10 == 0) { + color = grid_major; + } else { + color = grid_minor; + } - if (ABS(i) % 10 == 0) { - color = grid_major; - } else { - color = grid_minor; + float base_ofs = i * snap * zoom - offset.x * zoom; + draw_line(Vector2(base_ofs, 0), Vector2(base_ofs, get_size().height), color); } - float base_ofs = i * snap * zoom - offset.x * zoom; - draw_line(Vector2(base_ofs, 0), Vector2(base_ofs, get_size().height), color); - } + for (int i = from.y; i < from.y + len.y; i++) { + Color color; - for (int i = from.y; i < from.y + len.y; i++) { - Color color; + if (ABS(i) % 10 == 0) { + color = grid_major; + } else { + color = grid_minor; + } - if (ABS(i) % 10 == 0) { - color = grid_major; - } else { - color = grid_minor; + float base_ofs = i * snap * zoom - offset.y * zoom; + draw_line(Vector2(0, base_ofs), Vector2(get_size().width, base_ofs), color); } - - float base_ofs = i * snap * zoom - offset.y * zoom; - draw_line(Vector2(0, base_ofs), Vector2(get_size().width, base_ofs), color); } - } - } + } break; - if (p_what == NOTIFICATION_RESIZED) { - _update_scroll(); - top_layer->update(); - minimap->update(); + case NOTIFICATION_RESIZED: { + _update_scroll(); + top_layer->update(); + minimap->update(); + } break; } } void GraphEdit::_update_comment_enclosed_nodes_list(GraphNode *p_node, HashMap<StringName, Vector<GraphNode *>> &p_comment_enclosed_nodes) { Rect2 comment_node_rect = p_node->get_rect(); - Vector<GraphNode *> enclosed_nodes; + comment_node_rect.size *= zoom; + Vector<GraphNode *> enclosed_nodes; for (int i = 0; i < get_child_count(); i++) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); if (!gn || gn->is_selected()) { @@ -512,13 +518,15 @@ void GraphEdit::_update_comment_enclosed_nodes_list(GraphNode *p_node, HashMap<S } Rect2 node_rect = gn->get_rect(); + node_rect.size *= zoom; + bool included = comment_node_rect.encloses(node_rect); if (included) { enclosed_nodes.push_back(gn); } } - p_comment_enclosed_nodes.set(p_node->get_name(), enclosed_nodes); + p_comment_enclosed_nodes.insert(p_node->get_name(), enclosed_nodes); } void GraphEdit::_set_drag_comment_enclosed_nodes(GraphNode *p_node, HashMap<StringName, Vector<GraphNode *>> &p_comment_enclosed_nodes, bool p_drag) { @@ -539,8 +547,7 @@ void GraphEdit::_set_position_of_comment_enclosed_nodes(GraphNode *p_node, HashM } bool GraphEdit::_filter_input(const Point2 &p_point) { - Ref<Texture2D> port = get_theme_icon(SNAME("port"), SNAME("GraphNode")); - Vector2i port_size = Vector2i(port->get_width(), port->get_height()); + Ref<Texture2D> port_icon = get_theme_icon(SNAME("port"), SNAME("GraphNode")); for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); @@ -548,16 +555,18 @@ bool GraphEdit::_filter_input(const Point2 &p_point) { continue; } - for (int j = 0; j < gn->get_connection_output_count(); j++) { - Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); - if (is_in_hot_zone(pos / zoom, p_point / zoom, port_size, false)) { + for (int j = 0; j < gn->get_connection_input_count(); j++) { + Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height()); + port_size.height = MAX(port_size.height, gn->get_connection_input_height(j)); + if (is_in_input_hotzone(gn, j, p_point / zoom, port_size)) { return true; } } - for (int j = 0; j < gn->get_connection_input_count(); j++) { - Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); - if (is_in_hot_zone(pos / zoom, p_point / zoom, port_size, true)) { + for (int j = 0; j < gn->get_connection_output_count(); j++) { + Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height()); + port_size.height = MAX(port_size.height, gn->get_connection_output_height(j)); + if (is_in_output_hotzone(gn, j, p_point / zoom, port_size)) { return true; } } @@ -569,8 +578,7 @@ 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() == MouseButton::LEFT && mb->is_pressed()) { - Ref<Texture2D> port = get_theme_icon(SNAME("port"), SNAME("GraphNode")); - Vector2i port_size = Vector2i(port->get_width(), port->get_height()); + Ref<Texture2D> port_icon = get_theme_icon(SNAME("port"), SNAME("GraphNode")); connecting_valid = false; click_pos = mb->get_position() / zoom; @@ -582,7 +590,10 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { for (int j = 0; j < gn->get_connection_output_count(); j++) { Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); - if (is_in_hot_zone(pos / zoom, click_pos, port_size, false)) { + Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height()); + port_size.height = MAX(port_size.height, gn->get_connection_output_height(j)); + + if (is_in_output_hotzone(gn, j, click_pos, port_size)) { if (valid_left_disconnect_types.has(gn->get_connection_output_type(j))) { //check disconnect for (const Connection &E : connections) { @@ -602,6 +613,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { to = get_node(String(connecting_from)); //maybe it was erased if (Object::cast_to<GraphNode>(to)) { connecting = true; + emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false); } return; } @@ -618,13 +630,18 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting_target = false; connecting_to = pos; just_disconnected = false; + emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true); return; } } for (int j = 0; j < gn->get_connection_input_count(); j++) { Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); - if (is_in_hot_zone(pos / zoom, click_pos, port_size, true)) { + + Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height()); + port_size.height = MAX(port_size.height, gn->get_connection_input_height(j)); + + if (is_in_input_hotzone(gn, j, click_pos, port_size)) { if (right_disconnects || valid_right_disconnect_types.has(gn->get_connection_input_type(j))) { //check disconnect for (const Connection &E : connections) { @@ -644,6 +661,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { fr = get_node(String(connecting_from)); //maybe it was erased if (Object::cast_to<GraphNode>(fr)) { connecting = true; + emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, true); } return; } @@ -660,7 +678,7 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting_target = false; connecting_to = pos; just_disconnected = false; - + emit_signal(SNAME("connection_drag_started"), connecting_from, connecting_index, false); return; } } @@ -676,11 +694,9 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { connecting_valid = just_disconnected || click_pos.distance_to(connecting_to / zoom) > 20.0; if (connecting_valid) { - 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--) { + Ref<Texture2D> port_icon = get_theme_icon(SNAME("port"), SNAME("GraphNode")); GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); if (!gn) { continue; @@ -689,8 +705,11 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { if (!connecting_out) { for (int j = 0; j < gn->get_connection_output_count(); j++) { Vector2 pos = gn->get_connection_output_position(j) + gn->get_position(); + Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height()); + port_size.height = MAX(port_size.height, gn->get_connection_output_height(j)); + int type = gn->get_connection_output_type(j); - if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos / zoom, mpos, port_size, false)) { + if ((type == connecting_type || valid_connection_types.has(ConnType(connecting_type, type))) && is_in_output_hotzone(gn, j, mpos, port_size)) { connecting_target = true; connecting_to = pos; connecting_target_to = gn->get_name(); @@ -701,8 +720,11 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { } else { for (int j = 0; j < gn->get_connection_input_count(); j++) { Vector2 pos = gn->get_connection_input_position(j) + gn->get_position(); + Vector2i port_size = Vector2i(port_icon->get_width(), port_icon->get_height()); + port_size.height = MAX(port_size.height, gn->get_connection_input_height(j)); + int type = gn->get_connection_input_type(j); - if ((type == connecting_type || valid_connection_types.has(ConnType(type, connecting_type))) && is_in_hot_zone(pos / zoom, mpos, port_size, true)) { + if ((type == connecting_type || valid_connection_types.has(ConnType(connecting_type, type))) && is_in_input_hotzone(gn, j, mpos, port_size)) { connecting_target = true; connecting_to = pos; connecting_target_to = gn->get_name(); @@ -742,27 +764,30 @@ void GraphEdit::_top_layer_input(const Ref<InputEvent> &p_ev) { } } - connecting = false; - top_layer->update(); - minimap->update(); - update(); - connections_layer->update(); + if (connecting) { + force_connection_drag_end(); + } } } -bool GraphEdit::_check_clickable_control(Control *p_control, const Vector2 &pos) { - if (p_control->is_set_as_top_level() || !p_control->is_visible()) { +bool GraphEdit::_check_clickable_control(Control *p_control, const Vector2 &mpos, const Vector2 &p_offset) { + if (p_control->is_set_as_top_level() || !p_control->is_visible() || !p_control->is_inside_tree()) { return false; } - if (!p_control->has_point(pos) || p_control->get_mouse_filter() == MOUSE_FILTER_IGNORE) { - //test children + Rect2 control_rect = p_control->get_rect(); + control_rect.size *= zoom; + control_rect.position *= zoom; + control_rect.position += p_offset; + + if (!control_rect.has_point(mpos) || p_control->get_mouse_filter() == MOUSE_FILTER_IGNORE) { + // Test children. for (int i = 0; i < p_control->get_child_count(); i++) { - Control *subchild = Object::cast_to<Control>(p_control->get_child(i)); - if (!subchild) { + Control *child_rect = Object::cast_to<Control>(p_control->get_child(i)); + if (!child_rect) { continue; } - if (_check_clickable_control(subchild, pos - subchild->get_position())) { + if (_check_clickable_control(child_rect, mpos, control_rect.position)) { return true; } } @@ -773,25 +798,35 @@ 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, 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; - } +bool GraphEdit::is_in_input_hotzone(GraphNode *p_graph_node, int p_slot_index, const Vector2 &p_mouse_pos, const Vector2i &p_port_size) { + bool success; + if (GDVIRTUAL_CALL(_is_in_input_hotzone, p_graph_node, p_slot_index, p_mouse_pos, success)) { + return success; } 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; - } + Vector2 pos = p_graph_node->get_connection_input_position(p_slot_index) + p_graph_node->get_position(); + return is_in_port_hotzone(pos / zoom, p_mouse_pos, p_port_size, true); + } +} + +bool GraphEdit::is_in_output_hotzone(GraphNode *p_graph_node, int p_slot_index, const Vector2 &p_mouse_pos, const Vector2i &p_port_size) { + bool success; + if (GDVIRTUAL_CALL(_is_in_output_hotzone, p_graph_node, p_slot_index, p_mouse_pos, success)) { + return success; + } else { + Vector2 pos = p_graph_node->get_connection_output_position(p_slot_index) + p_graph_node->get_position(); + return is_in_port_hotzone(pos / zoom, p_mouse_pos, p_port_size, false); + } +} + +bool GraphEdit::is_in_port_hotzone(const Vector2 &pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left) { + Rect2 hotzone = Rect2( + pos.x - (p_left ? port_hotzone_outer_extent : port_hotzone_inner_extent), + pos.y - p_port_size.height / 2.0, + port_hotzone_inner_extent + port_hotzone_outer_extent, + p_port_size.height); + + if (!hotzone.has_point(p_mouse_pos)) { + return false; } for (int i = 0; i < get_child_count(); i++) { @@ -799,23 +834,17 @@ bool GraphEdit::is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos, c if (!child) { 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; + Rect2 child_rect = child->get_rect(); + child_rect.size *= zoom; + if (child_rect.has_point(p_mouse_pos * zoom)) { for (int j = 0; j < child->get_child_count(); j++) { Control *subchild = Object::cast_to<Control>(child->get_child(j)); if (!subchild) { continue; } - if (_check_clickable_control(subchild, subpos - subchild->get_position())) { + if (_check_clickable_control(subchild, p_mouse_pos * zoom, child_rect.position)) { return false; } } @@ -831,13 +860,23 @@ PackedVector2Array GraphEdit::get_connection_line(const Vector2 &p_from, const V return ret; } + float x_diff = (p_to.x - p_from.x); + float cp_offset = x_diff * lines_curvature; + if (x_diff < 0) { + cp_offset *= -1; + } + Curve2D curve; - Vector<Color> colors; curve.add_point(p_from); - curve.set_point_out(0, Vector2(60, 0)); + curve.set_point_out(0, Vector2(cp_offset, 0)); curve.add_point(p_to); - curve.set_point_in(1, Vector2(-60, 0)); - return curve.tessellate(); + curve.set_point_in(1, Vector2(-cp_offset, 0)); + + if (lines_curvature > 0) { + return curve.tessellate(5, 2.0); + } else { + return curve.tessellate(1); + } } 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) { @@ -973,7 +1012,7 @@ void GraphEdit::_minimap_draw() { 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"); + Ref<StyleBoxFlat> sbf = gn->get_theme_stylebox(gn->is_selected() ? "comment_focus" : "comment"); if (sbf.is_valid()) { Color node_color = sbf->get_bg_color(); sb_minimap->set_bg_color(node_color); @@ -996,7 +1035,7 @@ void GraphEdit::_minimap_draw() { 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"); + Ref<StyleBoxFlat> sbf = gn->get_theme_stylebox(gn->is_selected() ? "selected_frame" : "frame"); if (sbf.is_valid()) { Color node_color = sbf->get_border_color(); sb_minimap->set_bg_color(node_color); @@ -1066,13 +1105,11 @@ void GraphEdit::set_selected(Node *p_child) { void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { ERR_FAIL_COND(p_ev.is_null()); + if (panner->gui_input(p_ev, warped_panning ? get_global_rect() : Rect2())) { + return; + } Ref<InputEventMouseMotion> mm = p_ev; - if (mm.is_valid() && ((mm->get_button_mask() & MouseButton::MASK_MIDDLE) != MouseButton::NONE || ((mm->get_button_mask() & MouseButton::MASK_LEFT) != MouseButton::NONE && 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) { @@ -1162,11 +1199,9 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { minimap->update(); } else { if (connecting) { - connecting = false; - top_layer->update(); - minimap->update(); + force_connection_drag_end(); } else { - emit_signal(SNAME("popup_request"), get_screen_position() + b->get_position()); + emit_signal(SNAME("popup_request"), b->get_position()); } } } @@ -1274,7 +1309,7 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { if (_filter_input(b->get_position())) { return; } - if (Input::get_singleton()->is_key_pressed(Key::SPACE)) { + if (panner->is_panning()) { return; } @@ -1326,22 +1361,6 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { top_layer->update(); minimap->update(); } - - int scroll_direction = (b->get_button_index() == MouseButton::WHEEL_DOWN) - (b->get_button_index() == MouseButton::WHEEL_UP); - if (scroll_direction != 0) { - if (b->is_ctrl_pressed()) { - if (b->is_shift_pressed()) { - // Horizontal scrolling. - h_scroll->set_value(h_scroll->get_value() + (h_scroll->get_page() * b->get_factor() / 8) * scroll_direction); - } else { - // Vertical scrolling. - v_scroll->set_value(v_scroll->get_value() + (v_scroll->get_page() * b->get_factor() / 8) * scroll_direction); - } - } else { - // Zooming. - set_zoom_custom(scroll_direction < 0 ? zoom * zoom_step : zoom / zoom_step, b->get_position()); - } - } } if (p_ev->is_pressed()) { @@ -1355,7 +1374,19 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { emit_signal(SNAME("paste_nodes_request")); accept_event(); } else if (p_ev->is_action("ui_graph_delete")) { - emit_signal(SNAME("delete_nodes_request")); + TypedArray<StringName> nodes; + + for (int i = 0; i < get_child_count(); i++) { + GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); + if (!gn) { + continue; + } + if (gn->is_selected() && gn->is_close_button_visible()) { + nodes.push_back(gn->get_name()); + } + } + + emit_signal(SNAME("delete_nodes_request"), nodes); accept_event(); } } @@ -1372,6 +1403,23 @@ void GraphEdit::gui_input(const Ref<InputEvent> &p_ev) { } } +void GraphEdit::_scroll_callback(Vector2 p_scroll_vec, bool p_alt) { + if (p_scroll_vec.x != 0) { + h_scroll->set_value(h_scroll->get_value() + (h_scroll->get_page() * Math::abs(p_scroll_vec.x) / 8) * SIGN(p_scroll_vec.x)); + } else { + v_scroll->set_value(v_scroll->get_value() + (v_scroll->get_page() * Math::abs(p_scroll_vec.y) / 8) * SIGN(p_scroll_vec.y)); + } +} + +void GraphEdit::_pan_callback(Vector2 p_scroll_vec) { + h_scroll->set_value(h_scroll->get_value() - p_scroll_vec.x); + v_scroll->set_value(v_scroll->get_value() - p_scroll_vec.y); +} + +void GraphEdit::_zoom_callback(Vector2 p_scroll_vec, Vector2 p_origin, bool p_alt) { + set_zoom_custom(p_scroll_vec.y < 0 ? zoom * zoom_step : zoom / zoom_step, p_origin); +} + 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 (Connection &E : connections) { if (E.from == p_from && E.from_port == p_from_port && E.to == p_to && E.to_port == p_to_port) { @@ -1394,6 +1442,26 @@ void GraphEdit::clear_connections() { connections_layer->update(); } +void GraphEdit::force_connection_drag_end() { + ERR_FAIL_COND_MSG(!connecting, "Drag end requested without active drag!"); + connecting = false; + connecting_valid = false; + top_layer->update(); + minimap->update(); + update(); + connections_layer->update(); + emit_signal(SNAME("connection_drag_ended")); +} + +void GraphEdit::set_panning_scheme(PanningScheme p_scheme) { + panning_scheme = p_scheme; + panner->set_control_scheme((ViewPanner::ControlScheme)p_scheme); +} + +GraphEdit::PanningScheme GraphEdit::get_panning_scheme() const { + return panning_scheme; +} + void GraphEdit::set_zoom(float p_zoom) { set_zoom_custom(p_zoom, get_size() / 2); } @@ -1543,26 +1611,17 @@ void GraphEdit::_update_zoom_label() { } void GraphEdit::add_valid_connection_type(int p_type, int p_with_type) { - ConnType ct; - ct.type_a = p_type; - ct.type_b = p_with_type; - + ConnType ct(p_type, p_with_type); valid_connection_types.insert(ct); } void GraphEdit::remove_valid_connection_type(int p_type, int p_with_type) { - ConnType ct; - ct.type_a = p_type; - ct.type_b = p_with_type; - + ConnType ct(p_type, p_with_type); valid_connection_types.erase(ct); } bool GraphEdit::is_valid_connection_type(int p_type, int p_with_type) const { - ConnType ct; - ct.type_a = p_type; - ct.type_b = p_with_type; - + ConnType ct(p_type, p_with_type); return valid_connection_types.has(ct); } @@ -1621,6 +1680,7 @@ float GraphEdit::get_minimap_opacity() const { void GraphEdit::set_minimap_enabled(bool p_enable) { minimap_button->set_pressed(p_enable); + _minimap_toggled(); minimap->update(); } @@ -1637,6 +1697,15 @@ void GraphEdit::_minimap_toggled() { } } +void GraphEdit::set_connection_lines_curvature(float p_curvature) { + lines_curvature = p_curvature; + update(); +} + +float GraphEdit::get_connection_lines_curvature() const { + return lines_curvature; +} + void GraphEdit::set_connection_lines_thickness(float p_thickness) { lines_thickness = p_thickness; update(); @@ -1659,12 +1728,21 @@ 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) { +Ref<ViewPanner> GraphEdit::get_panner() { + return panner; +} + +void GraphEdit::set_warped_panning(bool p_warped) { + warped_panning = p_warped; +} + +int GraphEdit::_set_operations(SET_OPERATIONS p_operation, HashSet<StringName> &r_u, const HashSet<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())) + for (const StringName &E : r_u) { + if (!r_v.has(E)) { return 0; + } } return r_u.size() == r_v.size(); } break; @@ -1672,27 +1750,31 @@ int GraphEdit::_set_operations(SET_OPERATIONS p_operation, Set<StringName> &r_u, 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())) + for (const StringName &E : r_u) { + if (!r_v.has(E)) { 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()); + for (HashSet<StringName>::Iterator E = r_u.begin(); E;) { + HashSet<StringName>::Iterator N = E; + ++N; + if (r_v.has(*E)) { + r_u.remove(E); } + E = N; } 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()); + for (const StringName &E : r_v) { + if (!r_u.has(E)) { + r_u.insert(E); } } - return r_v.size(); + return r_u.size(); } break; default: break; @@ -1700,28 +1782,28 @@ int GraphEdit::_set_operations(SET_OPERATIONS p_operation, Set<StringName> &r_u, 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>> GraphEdit::_layering(const HashSet<StringName> &r_selected_nodes, const HashMap<StringName, HashSet<StringName>> &r_upper_neighbours) { HashMap<int, Vector<StringName>> l; - Set<StringName> p = r_selected_nodes, q = r_selected_nodes, u, z; + HashSet<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()]; + for (const StringName &E : p) { + HashSet<StringName> n = r_upper_neighbours[E]; if (_set_operations(GraphEdit::IS_SUBSET, n, z)) { Vector<StringName> t; - t.push_back(E->get()); + t.push_back(E); if (!l.has(current_layer)) { - l.set(current_layer, Vector<StringName>{}); + l.insert(current_layer, Vector<StringName>{}); } selected = true; t.append_array(l[current_layer]); - l.set(current_layer, t); - Set<StringName> V; - V.insert(E->get()); + l.insert(current_layer, t); + HashSet<StringName> V; + V.insert(E); _set_operations(GraphEdit::UNION, u, V); } } @@ -1762,10 +1844,10 @@ Vector<StringName> GraphEdit::_split(const Vector<StringName> &r_layer, const Ha 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(); +void GraphEdit::_horizontal_alignment(Dictionary &r_root, Dictionary &r_align, const HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, HashSet<StringName>> &r_upper_neighbours, const HashSet<StringName> &r_selected_nodes) { + for (const StringName &E : r_selected_nodes) { + r_root[E] = E; + r_align[E] = E; } if (r_layers.size() == 1) { @@ -1787,8 +1869,8 @@ void GraphEdit::_horizontal_alignment(Dictionary &r_root, Dictionary &r_align, c } } - int start = up.size() / 2; - int end = up.size() % 2 ? start : start + 1; + int start = (up.size() - 1) / 2; + int end = (up.size() - 1) % 2 ? start + 1 : start; for (int p = start; p <= end; p++) { StringName Align = r_align[current_node]; if (Align == current_node && r < up[p].first) { @@ -1802,7 +1884,7 @@ void GraphEdit::_horizontal_alignment(Dictionary &r_root, Dictionary &r_align, c } } -void GraphEdit::_crossing_minimisation(HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, Set<StringName>> &r_upper_neighbours) { +void GraphEdit::_crossing_minimisation(HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, HashSet<StringName>> &r_upper_neighbours) { if (r_layers.size() == 1) { return; } @@ -1833,17 +1915,17 @@ void GraphEdit::_crossing_minimisation(HashMap<int, Vector<StringName>> &r_layer } d[q] = crossings; } - c.set(p, d); + c.insert(p, d); } - r_layers.set(i, _split(lower_layer, c)); + r_layers.insert(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()) { +void GraphEdit::_calculate_inner_shifts(Dictionary &r_inner_shifts, const Dictionary &r_root, const Dictionary &r_node_names, const Dictionary &r_align, const HashSet<StringName> &r_block_heads, const HashMap<StringName, Pair<int, int>> &r_port_info) { + for (const StringName &E : r_block_heads) { real_t left = 0; - StringName u = E->get(); + StringName u = E; StringName v = r_align[u]; while (u != v && (StringName)r_root[u] != v) { String _connection = String(u) + " " + String(v); @@ -1864,11 +1946,11 @@ void GraphEdit::_calculate_inner_shifts(Dictionary &r_inner_shifts, const Dictio v = (StringName)r_align[v]; } - u = E->get(); + u = E; do { r_inner_shifts[u] = (real_t)r_inner_shifts[u] - left; u = (StringName)r_align[u]; - } while (u != E->get()); + } while (u != E); } } @@ -1999,7 +2081,7 @@ void GraphEdit::_place_block(StringName p_v, float p_delta, const HashMap<int, V 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); + r_node_positions.insert(p_v, pos); } #undef PRED @@ -2013,7 +2095,7 @@ void GraphEdit::arrange_nodes() { } Dictionary node_names; - Set<StringName> selected_nodes; + HashSet<StringName> selected_nodes; for (int i = get_child_count() - 1; i >= 0; i--) { GraphNode *gn = Object::cast_to<GraphNode>(get_child(i)); @@ -2024,7 +2106,7 @@ void GraphEdit::arrange_nodes() { node_names[gn->get_name()] = gn; } - HashMap<StringName, Set<StringName>> upper_neighbours; + HashMap<StringName, HashSet<StringName>> upper_neighbours; HashMap<StringName, Pair<int, int>> port_info; Vector2 origin(FLT_MAX, FLT_MAX); @@ -2039,7 +2121,7 @@ void GraphEdit::arrange_nodes() { if (gn->is_selected()) { selected_nodes.insert(gn->get_name()); - Set<StringName> s; + HashSet<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()) { @@ -2055,10 +2137,10 @@ void GraphEdit::arrange_nodes() { ports = p_ports; } } - port_info.set(_connection, ports); + port_info.insert(_connection, ports); } } - upper_neighbours.set(gn->get_name(), s); + upper_neighbours.insert(gn->get_name(), s); } } @@ -2076,38 +2158,38 @@ void GraphEdit::arrange_nodes() { HashMap<StringName, Vector2> new_positions; Vector2 default_position(FLT_MAX, FLT_MAX); Dictionary inner_shift; - Set<StringName> block_heads; + HashSet<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()); + for (const StringName &E : selected_nodes) { + inner_shift[E] = 0.0f; + sink[E] = E; + shift[E] = FLT_MAX; + new_positions.insert(E, default_position); + if ((StringName)root[E] == E) { + block_heads.insert(E); } } _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); + for (const StringName &E : block_heads) { + _place_block(E, 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; + for (const StringName &E : block_heads) { + StringName u = E; + float start_from = origin.y + new_positions[E].y; do { Vector2 cal_pos; cal_pos.y = start_from + (real_t)inner_shift[u]; - new_positions.set(u, cal_pos); + new_positions.insert(u, cal_pos); u = align[u]; - } while (u != E->get()); + } while (u != E); } - //Compute horizontal co-ordinates individually for layers to get uniform gap + // Compute horizontal coordinates individually for layers to get uniform gap. float start_from = origin.x; float largest_node_size = 0.0f; @@ -2134,7 +2216,7 @@ void GraphEdit::arrange_nodes() { } cal_pos.x = current_node_start_pos; } - new_positions.set(layer[j], cal_pos); + new_positions.insert(layer[j], cal_pos); } start_from += largest_node_size + gap_h; @@ -2142,10 +2224,10 @@ void GraphEdit::arrange_nodes() { } emit_signal(SNAME("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()]); + for (const StringName &E : selected_nodes) { + GraphNode *gn = Object::cast_to<GraphNode>(node_names[E]); gn->set_drag(true); - Vector2 pos = (new_positions[E->get()]); + Vector2 pos = (new_positions[E]); if (is_using_snap()) { const int snap = get_snap(); @@ -2165,8 +2247,9 @@ void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("set_connection_activity", "from", "from_port", "to", "to_port", "amount"), &GraphEdit::set_connection_activity); ClassDB::bind_method(D_METHOD("get_connection_list"), &GraphEdit::_get_connection_list); ClassDB::bind_method(D_METHOD("clear_connections"), &GraphEdit::clear_connections); + ClassDB::bind_method(D_METHOD("force_connection_drag_end"), &GraphEdit::force_connection_drag_end); ClassDB::bind_method(D_METHOD("get_scroll_ofs"), &GraphEdit::get_scroll_ofs); - ClassDB::bind_method(D_METHOD("set_scroll_ofs", "ofs"), &GraphEdit::set_scroll_ofs); + ClassDB::bind_method(D_METHOD("set_scroll_ofs", "offset"), &GraphEdit::set_scroll_ofs); ClassDB::bind_method(D_METHOD("add_valid_right_disconnect_type", "type"), &GraphEdit::add_valid_right_disconnect_type); ClassDB::bind_method(D_METHOD("remove_valid_right_disconnect_type", "type"), &GraphEdit::remove_valid_right_disconnect_type); @@ -2177,6 +2260,9 @@ void GraphEdit::_bind_methods() { 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_panning_scheme", "scheme"), &GraphEdit::set_panning_scheme); + ClassDB::bind_method(D_METHOD("get_panning_scheme"), &GraphEdit::get_panning_scheme); + ClassDB::bind_method(D_METHOD("set_zoom", "zoom"), &GraphEdit::set_zoom); ClassDB::bind_method(D_METHOD("get_zoom"), &GraphEdit::get_zoom); @@ -2198,6 +2284,9 @@ void GraphEdit::_bind_methods() { 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_curvature", "curvature"), &GraphEdit::set_connection_lines_curvature); + ClassDB::bind_method(D_METHOD("get_connection_lines_curvature"), &GraphEdit::get_connection_lines_curvature); + 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); @@ -2216,6 +2305,8 @@ void GraphEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("is_right_disconnects_enabled"), &GraphEdit::is_right_disconnects_enabled); ClassDB::bind_method(D_METHOD("_update_scroll_offset"), &GraphEdit::_update_scroll_offset); + GDVIRTUAL_BIND(_is_in_input_hotzone, "graph_node", "slot_index", "mouse_position"); + GDVIRTUAL_BIND(_is_in_output_hotzone, "graph_node", "slot_index", "mouse_position"); ClassDB::bind_method(D_METHOD("get_zoom_hbox"), &GraphEdit::get_zoom_hbox); @@ -2229,8 +2320,10 @@ void GraphEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::VECTOR2, "scroll_offset"), "set_scroll_ofs", "get_scroll_ofs"); ADD_PROPERTY(PropertyInfo(Variant::INT, "snap_distance"), "set_snap", "get_snap"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_snap"), "set_use_snap", "is_using_snap"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "panning_scheme", PROPERTY_HINT_ENUM, "Scroll Zooms,Scroll Pans"), "set_panning_scheme", "get_panning_scheme"); ADD_GROUP("Connection Lines", "connection_lines"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "connection_lines_curvature"), "set_connection_lines_curvature", "get_connection_lines_curvature"); 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"); @@ -2256,10 +2349,15 @@ void GraphEdit::_bind_methods() { 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")); + ADD_SIGNAL(MethodInfo("delete_nodes_request", PropertyInfo(Variant::ARRAY, "nodes", PROPERTY_HINT_ARRAY_TYPE, "StringName"))); ADD_SIGNAL(MethodInfo("begin_node_move")); ADD_SIGNAL(MethodInfo("end_node_move")); - ADD_SIGNAL(MethodInfo("scroll_offset_changed", PropertyInfo(Variant::VECTOR2, "ofs"))); + ADD_SIGNAL(MethodInfo("scroll_offset_changed", PropertyInfo(Variant::VECTOR2, "offset"))); + ADD_SIGNAL(MethodInfo("connection_drag_started", PropertyInfo(Variant::STRING, "from"), PropertyInfo(Variant::STRING, "slot"), PropertyInfo(Variant::BOOL, "is_output"))); + ADD_SIGNAL(MethodInfo("connection_drag_ended")); + + BIND_ENUM_CONSTANT(SCROLL_ZOOMS); + BIND_ENUM_CONSTANT(SCROLL_PANS); } GraphEdit::GraphEdit() { @@ -2272,12 +2370,16 @@ GraphEdit::GraphEdit() { // Allow zooming 4 times from the default zoom level. zoom_max = (1 * Math::pow(zoom_step, 4)); + panner.instantiate(); + panner->set_callbacks(callable_mp(this, &GraphEdit::_scroll_callback), callable_mp(this, &GraphEdit::_pan_callback), callable_mp(this, &GraphEdit::_zoom_callback)); + top_layer = memnew(GraphEditFilter(this)); add_child(top_layer, false, INTERNAL_MODE_BACK); top_layer->set_mouse_filter(MOUSE_FILTER_PASS); top_layer->set_anchors_and_offsets_preset(Control::PRESET_WIDE); top_layer->connect("draw", callable_mp(this, &GraphEdit::_top_layer_draw)); top_layer->connect("gui_input", callable_mp(this, &GraphEdit::_top_layer_input)); + top_layer->connect("focus_exited", callable_mp(panner.ptr(), &ViewPanner::release_pan_key)); connections_layer = memnew(Control); add_child(connections_layer, false, INTERNAL_MODE_FRONT); diff --git a/scene/gui/graph_edit.h b/scene/gui/graph_edit.h index 6c11f9df6a..02e90e4717 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,18 +36,17 @@ #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" -#include "scene/gui/texture_rect.h" class GraphEdit; +class ViewPanner; class GraphEditFilter : public Control { GDCLASS(GraphEditFilter, Control); friend class GraphEdit; friend class GraphEditMinimap; - GraphEdit *ge; + GraphEdit *ge = nullptr; virtual bool has_point(const Point2 &p_point) const override; public: @@ -59,7 +58,7 @@ class GraphEditMinimap : public Control { friend class GraphEdit; friend class GraphEditFilter; - GraphEdit *ge; + GraphEdit *ge = nullptr; protected: public: @@ -103,24 +102,36 @@ public: float activity = 0.0; }; + // Should be in sync with ControlScheme in ViewPanner. + enum PanningScheme { + SCROLL_ZOOMS, + SCROLL_PANS, + }; + private: - Label *zoom_label; - Button *zoom_minus; - Button *zoom_reset; - Button *zoom_plus; + Label *zoom_label = nullptr; + Button *zoom_minus = nullptr; + Button *zoom_reset = nullptr; + Button *zoom_plus = nullptr; - Button *snap_button; - SpinBox *snap_amount; + Button *snap_button = nullptr; + SpinBox *snap_amount = nullptr; - Button *minimap_button; + Button *minimap_button = nullptr; - Button *layout_button; + Button *layout_button = nullptr; - HScrollBar *h_scroll; - VScrollBar *v_scroll; + HScrollBar *h_scroll = nullptr; + VScrollBar *v_scroll = nullptr; - float port_grab_distance_horizontal = 0.0; - float port_grab_distance_vertical; + float port_hotzone_inner_extent = 0.0; + float port_hotzone_outer_extent = 0.0; + + Ref<ViewPanner> panner; + bool warped_panning = true; + void _scroll_callback(Vector2 p_scroll_vec, bool p_alt); + void _pan_callback(Vector2 p_scroll_vec); + void _zoom_callback(Vector2 p_scroll_vec, Vector2 p_origin, bool p_alt); bool connecting = false; String connecting_from; @@ -131,11 +142,12 @@ private: bool connecting_target = false; Vector2 connecting_to; String connecting_target_to; - int connecting_target_index; + int connecting_target_index = 0; bool just_disconnected = false; bool connecting_valid = false; Vector2 click_pos; + PanningScheme panning_scheme = SCROLL_ZOOMS; bool dragging = false; bool just_selected = false; bool moving_selection = false; @@ -143,8 +155,9 @@ private: float zoom = 1.0; float zoom_step = 1.2; - float zoom_min; - float zoom_max; + // Proper values set in constructor. + float zoom_min = 0.0; + float zoom_max = 0.0; void _zoom_minus(); void _zoom_reset(); @@ -165,6 +178,7 @@ private: List<Connection> connections; float lines_thickness = 2.0f; + float lines_curvature = 0.5f; bool lines_antialiased = true; PackedVector2Array get_connection_line(const Vector2 &p_from, const Vector2 &p_to); @@ -178,12 +192,14 @@ private: void _scroll_moved(double); virtual void gui_input(const Ref<InputEvent> &p_ev) override; - Control *connections_layer; - GraphEditFilter *top_layer; - GraphEditMinimap *minimap; + Control *connections_layer = nullptr; + GraphEditFilter *top_layer = nullptr; + GraphEditMinimap *minimap = nullptr; void _top_layer_input(const Ref<InputEvent> &p_ev); - bool is_in_hot_zone(const Vector2 &pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left); + bool is_in_input_hotzone(GraphNode *p_graph_node, int p_slot_index, const Vector2 &p_mouse_pos, const Vector2i &p_port_size); + bool is_in_output_hotzone(GraphNode *p_graph_node, int p_slot_index, const Vector2 &p_mouse_pos, const Vector2i &p_port_size); + bool is_in_port_hotzone(const Vector2 &pos, const Vector2 &p_mouse_pos, const Vector2i &p_port_size, bool p_left); void _top_layer_draw(); void _connections_layer_draw(); @@ -192,7 +208,7 @@ private: Array _get_connection_list() const; - bool lines_on_bg; + bool lines_on_bg = false; struct ConnType { union { @@ -203,8 +219,11 @@ private: uint64_t key = 0; }; - bool operator<(const ConnType &p_type) const { - return key < p_type.key; + static uint32_t hash(const ConnType &p_conn) { + return hash_one_uint64(p_conn.key); + } + bool operator==(const ConnType &p_type) const { + return key == p_type.key; } ConnType(uint32_t a = 0, uint32_t b = 0) { @@ -213,16 +232,16 @@ private: } }; - Set<ConnType> valid_connection_types; - Set<int> valid_left_disconnect_types; - Set<int> valid_right_disconnect_types; + HashSet<ConnType, ConnType> valid_connection_types; + HashSet<int> valid_left_disconnect_types; + HashSet<int> valid_right_disconnect_types; HashMap<StringName, Vector<GraphNode *>> comment_enclosed_nodes; void _update_comment_enclosed_nodes_list(GraphNode *p_node, HashMap<StringName, Vector<GraphNode *>> &p_comment_enclosed_nodes); void _set_drag_comment_enclosed_nodes(GraphNode *p_node, HashMap<StringName, Vector<GraphNode *>> &p_comment_enclosed_nodes, bool p_drag); void _set_position_of_comment_enclosed_nodes(GraphNode *p_node, HashMap<StringName, Vector<GraphNode *>> &p_comment_enclosed_nodes, Vector2 p_pos); - HBoxContainer *zoom_hb; + HBoxContainer *zoom_hb = nullptr; friend class GraphEditFilter; bool _filter_input(const Point2 &p_point); @@ -232,7 +251,7 @@ private: friend class GraphEditMinimap; void _minimap_toggled(); - bool _check_clickable_control(Control *p_control, const Vector2 &pos); + bool _check_clickable_control(Control *p_control, const Vector2 &r_mouse_pos, const Vector2 &p_offset); bool arranging_graph = false; @@ -243,12 +262,12 @@ private: 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); + int _set_operations(SET_OPERATIONS p_operation, HashSet<StringName> &r_u, const HashSet<StringName> &r_v); + HashMap<int, Vector<StringName>> _layering(const HashSet<StringName> &r_selected_nodes, const HashMap<StringName, HashSet<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); + void _horizontal_alignment(Dictionary &r_root, Dictionary &r_align, const HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, HashSet<StringName>> &r_upper_neighbours, const HashSet<StringName> &r_selected_nodes); + void _crossing_minimisation(HashMap<int, Vector<StringName>> &r_layers, const HashMap<StringName, HashSet<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 HashSet<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); @@ -259,12 +278,15 @@ protected: void _notification(int p_what); GDVIRTUAL2RC(Vector<Vector2>, _get_connection_line, Vector2, Vector2) + GDVIRTUAL3R(bool, _is_in_input_hotzone, Object *, int, Vector2) + GDVIRTUAL3R(bool, _is_in_output_hotzone, Object *, int, Vector2) public: Error connect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); bool is_node_connected(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); void disconnect_node(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port); void clear_connections(); + void force_connection_drag_end(); void set_connection_activity(const StringName &p_from, int p_from_port, const StringName &p_to, int p_to_port, float p_activity); @@ -272,6 +294,9 @@ public: void remove_valid_connection_type(int p_type, int p_with_type); bool is_valid_connection_type(int p_type, int p_with_type) const; + void set_panning_scheme(PanningScheme p_scheme); + PanningScheme get_panning_scheme() const; + void set_zoom(float p_zoom); void set_zoom_custom(float p_zoom, const Vector2 &p_center); float get_zoom() const; @@ -320,6 +345,9 @@ public: int get_snap() const; void set_snap(int p_snap); + void set_connection_lines_curvature(float p_curvature); + float get_connection_lines_curvature() const; + void set_connection_lines_thickness(float p_thickness); float get_connection_lines_thickness() const; @@ -327,10 +355,14 @@ public: bool is_connection_lines_antialiased() const; HBoxContainer *get_zoom_hbox(); + Ref<ViewPanner> get_panner(); + void set_warped_panning(bool p_warped); void arrange_nodes(); GraphEdit(); }; +VARIANT_ENUM_CAST(GraphEdit::PanningScheme); + #endif // GRAPHEdit_H diff --git a/scene/gui/graph_node.cpp b/scene/gui/graph_node.cpp index 7a7b35585e..5cb28a30e8 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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_node.h" #include "core/string/translation.h" + #ifdef TOOLS_ENABLED #include "graph_edit.h" #endif @@ -46,7 +47,7 @@ bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { if (str.begins_with("opentype_features/")) { String name = str.get_slicec('/', 1); int32_t tag = TS->name_to_tag(name); - double value = p_value; + int value = p_value; if (value == -1) { if (opentype_features.has(tag)) { opentype_features.erase(tag); @@ -54,7 +55,7 @@ bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { update(); } } else { - if ((double)opentype_features[tag] != value) { + if (!opentype_features.has(tag) || (int)opentype_features[tag] != value) { opentype_features[tag] = value; _shape(); update(); @@ -92,11 +93,13 @@ bool GraphNode::_set(const StringName &p_name, const Variant &p_value) { si.color_right = p_value; } else if (what == "right_icon") { si.custom_slot_right = p_value; + } else if (what == "draw_stylebox") { + si.draw_stylebox = 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, si.custom_slot_left, si.custom_slot_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, si.draw_stylebox); update(); return true; } @@ -143,6 +146,8 @@ bool GraphNode::_get(const StringName &p_name, Variant &r_ret) const { r_ret = si.color_right; } else if (what == "right_icon") { r_ret = si.custom_slot_right; + } else if (what == "draw_stylebox") { + r_ret = si.draw_stylebox; } else { return false; } @@ -153,7 +158,7 @@ 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::INT, "opentype_features/" + name)); } p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); @@ -174,7 +179,7 @@ void GraphNode::_get_property_list(List<PropertyInfo> *p_list) const { 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)); - + p_list->push_back(PropertyInfo(Variant::BOOL, base + "draw_stylebox")); idx++; } } @@ -184,6 +189,7 @@ void GraphNode::_resort() { Size2i new_size = get_size(); Ref<StyleBox> sb = get_theme_stylebox(SNAME("frame")); + Ref<StyleBox> sb_slot = get_theme_stylebox(SNAME("slot")); int sep = get_theme_constant(SNAME("separation")); @@ -192,7 +198,7 @@ void GraphNode::_resort() { int stretch_min = 0; int stretch_avail = 0; float stretch_ratio_total = 0; - Map<Control *, _MinSizeCache> min_size_cache; + HashMap<Control *, _MinSizeCache> min_size_cache; for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); @@ -203,7 +209,7 @@ void GraphNode::_resort() { continue; } - Size2i size = c->get_combined_minimum_size(); + Size2i size = c->get_combined_minimum_size() + (slot_info[i].draw_stylebox ? sb_slot->get_minimum_size() : Size2()); _MinSizeCache msc; stretch_min += size.height; @@ -311,7 +317,9 @@ void GraphNode::_resort() { int size = to - from; - Rect2 rect(sb->get_margin(SIDE_LEFT), from, w, size); + float margin = sb->get_margin(SIDE_LEFT) + (slot_info[i].draw_stylebox ? sb_slot->get_margin(SIDE_LEFT) : 0); + float width = w - (slot_info[i].draw_stylebox ? sb_slot->get_minimum_size().x : 0); + Rect2 rect(margin, from, width, size); fit_child_in_rect(c, rect); cache_y.push_back(from - sb->get_margin(SIDE_TOP) + size * 0.5); @@ -350,14 +358,14 @@ void GraphNode::_notification(int p_what) { Ref<StyleBox> sb; if (comment) { - sb = get_theme_stylebox(selected ? "commentfocus" : "comment"); + sb = get_theme_stylebox(selected ? SNAME("comment_focus") : SNAME("comment")); } else { - sb = get_theme_stylebox(selected ? "selectedframe" : "frame"); + sb = get_theme_stylebox(selected ? SNAME("selected_frame") : SNAME("frame")); } - //sb=sb->duplicate(); - //sb->call("set_modulate",modulate); + Ref<StyleBox> sb_slot = get_theme_stylebox(SNAME("slot")); + Ref<Texture2D> port = get_theme_icon(SNAME("port")); Ref<Texture2D> close = get_theme_icon(SNAME("close")); Ref<Texture2D> resizer = get_theme_icon(SNAME("resizer")); @@ -388,14 +396,9 @@ void GraphNode::_notification(int p_what) { int w = get_size().width - sb->get_minimum_size().x; - if (show_close) { - w -= close->get_width(); - } - - 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(SIDE_LEFT) + close_h_offset, -close->get_height() + close_offset); + Vector2 cpos = Point2(w + sb->get_margin(SIDE_LEFT) + close_h_offset - close->get_width(), -close->get_height() + close_offset); draw_texture(close, cpos, close_color); close_rect.position = cpos; close_rect.size = close->get_size(); @@ -411,7 +414,7 @@ void GraphNode::_notification(int p_what) { continue; } const Slot &s = slot_info[E.key]; - //left + // Left port. if (s.enable_left) { Ref<Texture2D> p = port; if (s.custom_slot_left.is_valid()) { @@ -419,6 +422,7 @@ void GraphNode::_notification(int p_what) { } p->draw(get_canvas_item(), icofs + Point2(edgeofs, cache_y[E.key]), s.color_left); } + // Right port. if (s.enable_right) { Ref<Texture2D> p = port; if (s.custom_slot_right.is_valid()) { @@ -426,6 +430,15 @@ void GraphNode::_notification(int p_what) { } p->draw(get_canvas_item(), icofs + Point2(get_size().x - edgeofs, cache_y[E.key]), s.color_right); } + + // Draw slot stylebox. + if (s.draw_stylebox) { + Control *c = Object::cast_to<Control>(get_child(E.key)); + Rect2 c_rect = c->get_rect(); + c_rect.position.x = sb->get_margin(SIDE_LEFT); + c_rect.size.width = w; + draw_style_box(sb_slot, c_rect); + } } if (resizable) { @@ -482,7 +495,7 @@ void GraphNode::_validate_property(PropertyInfo &property) const { } #endif -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) { +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, bool p_draw_stylebox) { 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) && @@ -501,6 +514,7 @@ void GraphNode::set_slot(int p_idx, bool p_enable_left, int p_type_left, const C s.color_right = p_color_right; s.custom_slot_left = p_custom_left; s.custom_slot_right = p_custom_right; + s.draw_stylebox = p_draw_stylebox; slot_info[p_idx] = s; update(); connpos_dirty = true; @@ -622,16 +636,39 @@ Color GraphNode::get_slot_color_right(int p_idx) const { return slot_info[p_idx].color_right; } +bool GraphNode::is_slot_draw_stylebox(int p_idx) const { + if (!slot_info.has(p_idx)) { + return false; + } + return slot_info[p_idx].draw_stylebox; +} + +void GraphNode::set_slot_draw_stylebox(int p_idx, bool p_enable) { + ERR_FAIL_COND_MSG(p_idx < 0, vformat("Cannot set draw_stylebox for the slot with p_idx (%d) lesser than zero.", p_idx)); + + slot_info[p_idx].draw_stylebox = p_enable; + update(); + connpos_dirty = true; + + emit_signal(SNAME("slot_updated"), p_idx); +} + Size2 GraphNode::get_minimum_size() const { - int sep = get_theme_constant(SNAME("separation")); Ref<StyleBox> sb = get_theme_stylebox(SNAME("frame")); + Ref<StyleBox> sb_slot = get_theme_stylebox(SNAME("slot")); + + int sep = get_theme_constant(SNAME("separation")); + int title_h_offset = get_theme_constant(SNAME("title_h_offset")); + bool first = true; Size2 minsize; - minsize.x = title_buf->get_size().x; + minsize.x = title_buf->get_size().x + title_h_offset; if (show_close) { + int close_h_offset = get_theme_constant(SNAME("close_h_offset")); Ref<Texture2D> close = get_theme_icon(SNAME("close")); - minsize.x += sep + close->get_width(); + //TODO: Remove this magic number after GraphNode rework. + minsize.x += 12 + close->get_width() + close_h_offset; } for (int i = 0; i < get_child_count(); i++) { @@ -644,6 +681,9 @@ Size2 GraphNode::get_minimum_size() const { } Size2i size = c->get_combined_minimum_size(); + if (slot_info.has(i)) { + size += slot_info[i].draw_stylebox ? sb_slot->get_minimum_size() : Size2(); + } minsize.y += size.y; minsize.x = MAX(minsize.x, size.x); @@ -792,6 +832,7 @@ void GraphNode::_connpos_update() { cc.pos = Point2i(edgeofs, y + h / 2); cc.type = slot_info[idx].type_left; cc.color = slot_info[idx].color_left; + cc.height = size.height; conn_input_cache.push_back(cc); } if (slot_info[idx].enable_right) { @@ -799,6 +840,7 @@ void GraphNode::_connpos_update() { cc.pos = Point2i(get_size().width - edgeofs, y + h / 2); cc.type = slot_info[idx].type_right; cc.color = slot_info[idx].color_right; + cc.height = size.height; conn_output_cache.push_back(cc); } } @@ -819,12 +861,13 @@ int GraphNode::get_connection_input_count() { return conn_input_cache.size(); } -int GraphNode::get_connection_output_count() { +int GraphNode::get_connection_input_height(int p_idx) { if (connpos_dirty) { _connpos_update(); } - return conn_output_cache.size(); + ERR_FAIL_INDEX_V(p_idx, conn_input_cache.size(), 0); + return conn_input_cache[p_idx].height; } Vector2 GraphNode::get_connection_input_position(int p_idx) { @@ -857,6 +900,23 @@ Color GraphNode::get_connection_input_color(int p_idx) { return conn_input_cache[p_idx].color; } +int GraphNode::get_connection_output_count() { + if (connpos_dirty) { + _connpos_update(); + } + + return conn_output_cache.size(); +} + +int GraphNode::get_connection_output_height(int p_idx) { + if (connpos_dirty) { + _connpos_update(); + } + + ERR_FAIL_INDEX_V(p_idx, conn_output_cache.size(), 0); + return conn_output_cache[p_idx].height; +} + Vector2 GraphNode::get_connection_output_position(int p_idx) { if (connpos_dirty) { _connpos_update(); @@ -959,6 +1019,25 @@ bool GraphNode::is_resizable() const { return resizable; } +Vector<int> GraphNode::get_allowed_size_flags_horizontal() const { + Vector<int> flags; + flags.append(SIZE_FILL); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + +Vector<int> GraphNode::get_allowed_size_flags_vertical() const { + Vector<int> flags; + flags.append(SIZE_FILL); + flags.append(SIZE_EXPAND); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + 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); @@ -970,7 +1049,7 @@ void GraphNode::_bind_methods() { ClassDB::bind_method(D_METHOD("set_language", "language"), &GraphNode::set_language); ClassDB::bind_method(D_METHOD("get_language"), &GraphNode::get_language); - ClassDB::bind_method(D_METHOD("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("set_slot", "idx", "enable_left", "type_left", "color_left", "enable_right", "type_right", "color_right", "custom_left", "custom_right", "enable"), &GraphNode::set_slot, DEFVAL(Ref<Texture2D>()), DEFVAL(Ref<Texture2D>()), DEFVAL(true)); ClassDB::bind_method(D_METHOD("clear_slot", "idx"), &GraphNode::clear_slot); ClassDB::bind_method(D_METHOD("clear_all_slots"), &GraphNode::clear_all_slots); @@ -992,6 +1071,9 @@ void GraphNode::_bind_methods() { 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("is_slot_draw_stylebox", "idx"), &GraphNode::is_slot_draw_stylebox); + ClassDB::bind_method(D_METHOD("set_slot_draw_stylebox", "idx", "draw_stylebox"), &GraphNode::set_slot_draw_stylebox); + 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); @@ -1004,15 +1086,17 @@ void GraphNode::_bind_methods() { ClassDB::bind_method(D_METHOD("set_selected", "selected"), &GraphNode::set_selected); ClassDB::bind_method(D_METHOD("is_selected"), &GraphNode::is_selected); - ClassDB::bind_method(D_METHOD("get_connection_output_count"), &GraphNode::get_connection_output_count); ClassDB::bind_method(D_METHOD("get_connection_input_count"), &GraphNode::get_connection_input_count); + ClassDB::bind_method(D_METHOD("get_connection_input_height", "idx"), &GraphNode::get_connection_input_height); + ClassDB::bind_method(D_METHOD("get_connection_input_position", "idx"), &GraphNode::get_connection_input_position); + ClassDB::bind_method(D_METHOD("get_connection_input_type", "idx"), &GraphNode::get_connection_input_type); + ClassDB::bind_method(D_METHOD("get_connection_input_color", "idx"), &GraphNode::get_connection_input_color); + ClassDB::bind_method(D_METHOD("get_connection_output_count"), &GraphNode::get_connection_output_count); + ClassDB::bind_method(D_METHOD("get_connection_output_height", "idx"), &GraphNode::get_connection_output_height); ClassDB::bind_method(D_METHOD("get_connection_output_position", "idx"), &GraphNode::get_connection_output_position); ClassDB::bind_method(D_METHOD("get_connection_output_type", "idx"), &GraphNode::get_connection_output_type); ClassDB::bind_method(D_METHOD("get_connection_output_color", "idx"), &GraphNode::get_connection_output_color); - ClassDB::bind_method(D_METHOD("get_connection_input_position", "idx"), &GraphNode::get_connection_input_position); - ClassDB::bind_method(D_METHOD("get_connection_input_type", "idx"), &GraphNode::get_connection_input_type); - ClassDB::bind_method(D_METHOD("get_connection_input_color", "idx"), &GraphNode::get_connection_input_color); ClassDB::bind_method(D_METHOD("set_show_close_button", "show"), &GraphNode::set_show_close_button); ClassDB::bind_method(D_METHOD("is_close_button_visible"), &GraphNode::is_close_button_visible); @@ -1022,7 +1106,7 @@ void GraphNode::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::STRING, "title"), "set_title", "get_title"); 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::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "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"); diff --git a/scene/gui/graph_node.h b/scene/gui/graph_node.h index 2238cfdb56..f6c943dc89 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +54,7 @@ private: Color color_right = Color(1, 1, 1, 1); Ref<Texture2D> custom_slot_left; Ref<Texture2D> custom_slot_right; + bool draw_stylebox = true; }; String title; @@ -80,12 +81,13 @@ private: Vector2 pos; int type = 0; Color color; + int height; }; Vector<ConnCache> conn_input_cache; Vector<ConnCache> conn_output_cache; - Map<int, Slot> slot_info; + HashMap<int, Slot> slot_info; bool connpos_dirty = true; @@ -115,7 +117,7 @@ protected: public: bool has_point(const Point2 &p_point) const override; - 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 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>(), bool p_draw_stylebox = true); void clear_slot(int p_idx); void clear_all_slots(); @@ -137,6 +139,9 @@ public: void set_slot_color_right(int p_idx, const Color &p_color_right); Color get_slot_color_right(int p_idx) const; + bool is_slot_draw_stylebox(int p_idx) const; + void set_slot_draw_stylebox(int p_idx, bool p_enable); + void set_title(const String &p_title); String get_title() const; @@ -163,10 +168,13 @@ public: bool is_close_button_visible() const; int get_connection_input_count(); - int get_connection_output_count(); + int get_connection_input_height(int p_idx); Vector2 get_connection_input_position(int p_idx); int get_connection_input_type(int p_idx); Color get_connection_input_color(int p_idx); + + int get_connection_output_count(); + int get_connection_output_height(int p_idx); Vector2 get_connection_output_position(int p_idx); int get_connection_output_type(int p_idx); Color get_connection_output_color(int p_idx); @@ -182,7 +190,12 @@ public: virtual Size2 get_minimum_size() const override; - bool is_resizing() const { return resizing; } + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + + bool is_resizing() const { + return resizing; + } GraphNode(); }; diff --git a/scene/gui/grid_container.cpp b/scene/gui/grid_container.cpp index 624330cdf6..6f8518a7b0 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,18 @@ /*************************************************************************/ #include "grid_container.h" +#include "core/templates/rb_set.h" 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> 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. + RBMap<int, int> col_minw; // Max of min_width of all controls in each col (indexed by col). + RBMap<int, int> row_minh; // Max of min_height of all controls in each row (indexed by row). + RBSet<int> col_expanded; // Columns which have the SIZE_EXPAND flag set. + RBSet<int> row_expanded; // Rows which have the SIZE_EXPAND flag set. - int hsep = get_theme_constant(SNAME("hseparation")); - int vsep = get_theme_constant(SNAME("vseparation")); + int hsep = get_theme_constant(SNAME("h_separation")); + int vsep = get_theme_constant(SNAME("v_separation")); int max_col = MIN(get_child_count(), columns); int max_row = ceil((float)get_child_count() / (float)columns); @@ -50,6 +51,9 @@ void GridContainer::_notification(int p_what) { if (!c || !c->is_visible_in_tree()) { continue; } + if (c->is_set_as_top_level()) { + continue; + } int row = valid_controls_index / columns; int col = valid_controls_index % columns; @@ -101,11 +105,11 @@ void GridContainer::_notification(int p_what) { // Check if all minwidth constraints are OK if we use the remaining space. can_fit = true; int max_index = col_expanded.front()->get(); - for (Set<int>::Element *E = col_expanded.front(); E; E = E->next()) { - if (col_minw[E->get()] > col_minw[max_index]) { - max_index = E->get(); + for (const int &E : col_expanded) { + if (col_minw[E] > col_minw[max_index]) { + max_index = E; } - if (can_fit && (remaining_space.width / col_expanded.size()) < col_minw[E->get()]) { + if (can_fit && (remaining_space.width / col_expanded.size()) < col_minw[E]) { can_fit = false; } } @@ -122,11 +126,11 @@ void GridContainer::_notification(int p_what) { // Check if all minheight constraints are OK if we use the remaining space. can_fit = true; int max_index = row_expanded.front()->get(); - for (Set<int>::Element *E = row_expanded.front(); E; E = E->next()) { - if (row_minh[E->get()] > row_minh[max_index]) { - max_index = E->get(); + for (const int &E : row_expanded) { + if (row_minh[E] > row_minh[max_index]) { + max_index = E; } - if (can_fit && (remaining_space.height / row_expanded.size()) < row_minh[E->get()]) { + if (can_fit && (remaining_space.height / row_expanded.size()) < row_minh[E]) { can_fit = false; } } @@ -139,13 +143,47 @@ 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; + int col_remaining_pixel = 0; + int col_expand = 0; + if (col_expanded.size() > 0) { + col_expand = remaining_space.width / col_expanded.size(); + col_remaining_pixel = remaining_space.width - col_expanded.size() * col_expand; + } + + int row_remaining_pixel = 0; + int row_expand = 0; + if (row_expanded.size() > 0) { + row_expand = remaining_space.height / row_expanded.size(); + row_remaining_pixel = remaining_space.height - row_expanded.size() * row_expand; + } + bool rtl = is_layout_rtl(); int col_ofs = 0; int row_ofs = 0; + // Calculate the index of rows and columns that receive the remaining pixel. + int col_remaining_pixel_index = 0; + for (int i = 0; i < max_col; i++) { + if (col_remaining_pixel == 0) { + break; + } + if (col_expanded.has(i)) { + col_remaining_pixel_index = i + 1; + col_remaining_pixel--; + } + } + int row_remaining_pixel_index = 0; + for (int i = 0; i < max_row; i++) { + if (row_remaining_pixel == 0) { + break; + } + if (row_expanded.has(i)) { + row_remaining_pixel_index = i + 1; + row_remaining_pixel--; + } + } + valid_controls_index = 0; for (int i = 0; i < get_child_count(); i++) { Control *c = Object::cast_to<Control>(get_child(i)); @@ -164,26 +202,40 @@ void GridContainer::_notification(int p_what) { } if (row > 0) { row_ofs += (row_expanded.has(row - 1) ? row_expand : row_minh[row - 1]) + vsep; + + if (row_expanded.has(row - 1) && row - 1 < row_remaining_pixel_index) { + // Apply the remaining pixel of the previous row. + row_ofs++; + } } } + Size2 s(col_expanded.has(col) ? col_expand : col_minw[col], row_expanded.has(row) ? row_expand : row_minh[row]); + + // Add the remaining pixel to the expanding columns and rows, starting from left and top. + if (col_expanded.has(col) && col < col_remaining_pixel_index) { + s.x++; + } + if (row_expanded.has(row) && row < row_remaining_pixel_index) { + s.y++; + } + 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: { update_minimum_size(); } break; + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { queue_sort(); @@ -210,11 +262,11 @@ void GridContainer::_bind_methods() { } Size2 GridContainer::get_minimum_size() const { - Map<int, int> col_minw; - Map<int, int> row_minh; + RBMap<int, int> col_minw; + RBMap<int, int> row_minh; - int hsep = get_theme_constant(SNAME("hseparation")); - int vsep = get_theme_constant(SNAME("vseparation")); + int hsep = get_theme_constant(SNAME("h_separation")); + int vsep = get_theme_constant(SNAME("v_separation")); int max_row = 0; int max_col = 0; diff --git a/scene/gui/grid_container.h b/scene/gui/grid_container.h index 9b43a5bc7e..9d77f90ab3 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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/item_list.cpp b/scene/gui/item_list.cpp index 6deb39b9e2..8b22f3722a 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -83,6 +83,9 @@ int ItemList::add_icon_item(const Ref<Texture2D> &p_item, bool p_selectable) { } void ItemList::set_item_text(int p_idx, const String &p_text) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].text = p_text; @@ -97,6 +100,9 @@ String ItemList::get_item_text(int p_idx) const { } void ItemList::set_item_text_direction(int p_idx, Control::TextDirection p_text_direction) { + if (p_idx < 0) { + p_idx += get_item_count(); + } 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) { @@ -119,6 +125,9 @@ void ItemList::clear_item_opentype_features(int p_idx) { } void ItemList::set_item_opentype_feature(int p_idx, const String &p_name, int p_value) { + if (p_idx < 0) { + p_idx += get_item_count(); + } 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) { @@ -138,6 +147,9 @@ int ItemList::get_item_opentype_feature(int p_idx, const String &p_name) const { } void ItemList::set_item_language(int p_idx, const String &p_language) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); if (items[p_idx].language != p_language) { items.write[p_idx].language = p_language; @@ -152,6 +164,9 @@ String ItemList::get_item_language(int p_idx) const { } void ItemList::set_item_tooltip_enabled(int p_idx, const bool p_enabled) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].tooltip_enabled = p_enabled; } @@ -162,6 +177,9 @@ bool ItemList::is_item_tooltip_enabled(int p_idx) const { } void ItemList::set_item_tooltip(int p_idx, const String &p_tooltip) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].tooltip = p_tooltip; @@ -175,6 +193,9 @@ String ItemList::get_item_tooltip(int p_idx) const { } void ItemList::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].icon = p_icon; @@ -189,6 +210,9 @@ Ref<Texture2D> ItemList::get_item_icon(int p_idx) const { } void ItemList::set_item_icon_transposed(int p_idx, const bool p_transposed) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].icon_transposed = p_transposed; @@ -203,6 +227,9 @@ bool ItemList::is_item_icon_transposed(int p_idx) const { } void ItemList::set_item_icon_region(int p_idx, const Rect2 &p_region) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].icon_region = p_region; @@ -217,6 +244,9 @@ Rect2 ItemList::get_item_icon_region(int p_idx) const { } void ItemList::set_item_icon_modulate(int p_idx, const Color &p_modulate) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].icon_modulate = p_modulate; @@ -230,6 +260,9 @@ Color ItemList::get_item_icon_modulate(int p_idx) const { } void ItemList::set_item_custom_bg_color(int p_idx, const Color &p_custom_bg_color) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].custom_bg = p_custom_bg_color; @@ -243,6 +276,9 @@ Color ItemList::get_item_custom_bg_color(int p_idx) const { } void ItemList::set_item_custom_fg_color(int p_idx, const Color &p_custom_fg_color) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].custom_fg = p_custom_fg_color; @@ -256,6 +292,9 @@ Color ItemList::get_item_custom_fg_color(int p_idx) const { } void ItemList::set_item_tag_icon(int p_idx, const Ref<Texture2D> &p_tag_icon) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].tag_icon = p_tag_icon; @@ -270,6 +309,9 @@ Ref<Texture2D> ItemList::get_item_tag_icon(int p_idx) const { } void ItemList::set_item_selectable(int p_idx, bool p_selectable) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].selectable = p_selectable; @@ -281,6 +323,9 @@ bool ItemList::is_item_selectable(int p_idx) const { } void ItemList::set_item_disabled(int p_idx, bool p_disabled) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].disabled = p_disabled; @@ -293,6 +338,9 @@ bool ItemList::is_item_disabled(int p_idx) const { } void ItemList::set_item_metadata(int p_idx, const Variant &p_metadata) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].metadata = p_metadata; @@ -537,6 +585,9 @@ Size2 ItemList::Item::get_icon_size() const { void ItemList::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); +#define CAN_SELECT(i) (items[i].selectable && !items[i].disabled) +#define IS_SAME_ROW(i, row) (i / current_columns == row) + double prev_scroll = scroll_bar->get_value(); Ref<InputEventMouseMotion> mm = p_event; @@ -555,7 +606,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { return; } - if (mb.is_valid() && (mb->get_button_index() == MouseButton::LEFT || (allow_rmb_select && mb->get_button_index() == MouseButton::RIGHT)) && mb->is_pressed()) { + if (mb.is_valid() && mb->is_pressed()) { search_string = ""; //any mousepress cancels Vector2 pos = mb->get_position(); Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); @@ -580,7 +631,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { } } - if (closest != -1) { + if (closest != -1 && (mb->get_button_index() == MouseButton::LEFT || (allow_rmb_select && mb->get_button_index() == MouseButton::RIGHT))) { int i = closest; if (select_mode == SELECT_MULTI && items[i].selected && mb->is_command_pressed()) { @@ -594,55 +645,47 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { SWAP(from, to); } for (int j = from; j <= to; j++) { + if (!CAN_SELECT(j)) { + continue; + } bool selected = !items[j].selected; select(j, false); if (selected) { emit_signal(SNAME("multi_selected"), j, true); } } + emit_signal(SNAME("item_clicked"), i, get_local_mouse_position(), mb->get_button_index()); - if (mb->get_button_index() == MouseButton::RIGHT) { - emit_signal(SNAME("item_rmb_selected"), i, get_local_mouse_position()); - } } else { if (!mb->is_double_click() && !mb->is_command_pressed() && select_mode == SELECT_MULTI && items[i].selectable && !items[i].disabled && items[i].selected && mb->get_button_index() == MouseButton::LEFT) { defer_select_single = i; return; } - if (items[i].selected && mb->get_button_index() == MouseButton::RIGHT) { - emit_signal(SNAME("item_rmb_selected"), i, get_local_mouse_position()); - } else { - bool selected = items[i].selected; - + if (!items[i].selected || allow_reselect) { select(i, select_mode == SELECT_SINGLE || !mb->is_command_pressed()); - if (!selected || allow_reselect) { - if (select_mode == SELECT_SINGLE) { - emit_signal(SNAME("item_selected"), i); - } else { - emit_signal(SNAME("multi_selected"), i, true); - } + if (select_mode == SELECT_SINGLE) { + emit_signal(SNAME("item_selected"), i); + } else { + emit_signal(SNAME("multi_selected"), i, true); } + } - if (mb->get_button_index() == MouseButton::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); - } + emit_signal(SNAME("item_clicked"), i, get_local_mouse_position(), mb->get_button_index()); + + if (mb->get_button_index() == MouseButton::LEFT && mb->is_double_click()) { + emit_signal(SNAME("item_activated"), i); } } return; + } else if (closest != -1) { + emit_signal(SNAME("item_clicked"), closest, get_local_mouse_position(), mb->get_button_index()); + } else { + // 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(SNAME("empty_clicked"), get_local_mouse_position(), mb->get_button_index()); } - if (mb->get_button_index() == MouseButton::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(SNAME("nothing_selected")); } if (mb.is_valid() && mb->get_button_index() == MouseButton::WHEEL_UP && mb->is_pressed()) { scroll_bar->set_value(scroll_bar->get_value() - scroll_bar->get_page() * mb->get_factor() / 8); @@ -659,7 +702,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { if (diff < uint64_t(ProjectSettings::get_singleton()->get("gui/timers/incremental_search_max_interval_msec")) * 2) { for (int i = current - 1; i >= 0; i--) { - if (items[i].text.begins_with(search_string)) { + if (CAN_SELECT(i) && items[i].text.begins_with(search_string)) { set_current(i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { @@ -675,7 +718,15 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { } if (current >= current_columns) { - set_current(current - current_columns); + int next = current - current_columns; + while (next >= 0 && !CAN_SELECT(next)) { + next = next - current_columns; + } + if (next < 0) { + accept_event(); + return; + } + set_current(next); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { emit_signal(SNAME("item_selected"), current); @@ -689,7 +740,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { if (diff < uint64_t(ProjectSettings::get_singleton()->get("gui/timers/incremental_search_max_interval_msec")) * 2) { for (int i = current + 1; i < items.size(); i++) { - if (items[i].text.begins_with(search_string)) { + if (CAN_SELECT(i) && items[i].text.begins_with(search_string)) { set_current(i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { @@ -704,7 +755,15 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { } if (current < items.size() - current_columns) { - set_current(current + current_columns); + int next = current + current_columns; + while (next < items.size() && !CAN_SELECT(next)) { + next = next + current_columns; + } + if (next >= items.size()) { + accept_event(); + return; + } + set_current(next); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { emit_signal(SNAME("item_selected"), current); @@ -715,7 +774,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { search_string = ""; //any mousepress cancels for (int i = 4; i > 0; i--) { - if (current - current_columns * i >= 0) { + if (current - current_columns * i >= 0 && CAN_SELECT(current - current_columns * i)) { set_current(current - current_columns * i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { @@ -729,7 +788,7 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { search_string = ""; //any mousepress cancels for (int i = 4; i > 0; i--) { - if (current + current_columns * i < items.size()) { + if (current + current_columns * i < items.size() && CAN_SELECT(current + current_columns * i)) { set_current(current + current_columns * i); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { @@ -744,7 +803,16 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { search_string = ""; //any mousepress cancels if (current % current_columns != 0) { - set_current(current - 1); + int current_row = current / current_columns; + int next = current - 1; + while (!CAN_SELECT(next)) { + next = next - 1; + } + if (next < 0 || !IS_SAME_ROW(next, current_row)) { + accept_event(); + return; + } + set_current(next); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { emit_signal(SNAME("item_selected"), current); @@ -755,7 +823,16 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { search_string = ""; //any mousepress cancels if (current % current_columns != (current_columns - 1) && current + 1 < items.size()) { - set_current(current + 1); + int current_row = current / current_columns; + int next = current + 1; + while (!CAN_SELECT(next)) { + next = next + 1; + } + if (items.size() <= next || !IS_SAME_ROW(next, current_row)) { + accept_event(); + return; + } + set_current(next); ensure_current_is_visible(); if (select_mode == SELECT_SINGLE) { emit_signal(SNAME("item_selected"), current); @@ -831,6 +908,9 @@ void ItemList::gui_input(const Ref<InputEvent> &p_event) { if (scroll_bar->get_value() != prev_scroll) { accept_event(); //accept event if scroll changed } + +#undef CAN_SELECT +#undef IS_SAME_ROW } void ItemList::ensure_current_is_visible() { @@ -855,445 +935,451 @@ static Rect2 _adjust_to_max_size(Size2 p_size, Size2 p_max_size) { } void ItemList::_notification(int p_what) { - if (p_what == NOTIFICATION_RESIZED) { - shape_changed = true; - update(); - } + switch (p_what) { + case NOTIFICATION_RESIZED: { + shape_changed = true; + update(); + } break; + + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: + case NOTIFICATION_THEME_CHANGED: { + for (int i = 0; i < items.size(); i++) { + _shape(i); + } + shape_changed = true; + update(); + } break; - 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(); - } + case NOTIFICATION_DRAW: { + Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); - if (p_what == NOTIFICATION_DRAW) { - Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg")); + int mw = scroll_bar->get_minimum_size().x; + scroll_bar->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -mw); + 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)); - int mw = scroll_bar->get_minimum_size().x; - 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(); - Size2 size = get_size(); + int width = size.width - bg->get_minimum_size().width; + if (scroll_bar->is_visible()) { + width -= mw; + } - int width = size.width - bg->get_minimum_size().width; - if (scroll_bar->is_visible()) { - width -= mw; - } + draw_style_box(bg, Rect2(Point2(), size)); - draw_style_box(bg, Rect2(Point2(), size)); + int hseparation = get_theme_constant(SNAME("h_separation")); + int vseparation = get_theme_constant(SNAME("v_separation")); + 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")); - 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(); - 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")); - 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(SNAME("bg_focus")), Rect2(Point2(), size)); + RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), false); + } - if (has_focus()) { - RenderingServer::get_singleton()->canvas_item_add_clip_ignore(get_canvas_item(), true); - 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.0; - if (shape_changed) { - float max_column_width = 0.0; + //1- compute item minimum sizes + for (int i = 0; i < items.size(); i++) { + Size2 minsize; + if (items[i].icon.is_valid()) { + if (fixed_icon_size.x > 0 && fixed_icon_size.y > 0) { + minsize = fixed_icon_size * icon_scale; + } else { + minsize = items[i].get_icon_size() * icon_scale; + } - //1- compute item minimum sizes - for (int i = 0; i < items.size(); i++) { - Size2 minsize; - if (items[i].icon.is_valid()) { - if (fixed_icon_size.x > 0 && fixed_icon_size.y > 0) { - minsize = fixed_icon_size * icon_scale; - } else { - minsize = items[i].get_icon_size() * icon_scale; + if (!items[i].text.is_empty()) { + if (icon_mode == ICON_MODE_TOP) { + minsize.y += icon_margin; + } else { + minsize.x += icon_margin; + } + } } if (!items[i].text.is_empty()) { - if (icon_mode == ICON_MODE_TOP) { - minsize.y += icon_margin; - } else { - minsize.x += icon_margin; + 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 (!items[i].text.is_empty()) { - 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 += s.height + line_separation * max_text_lines; + } else { + minsize.y += s.height; + } - if (icon_mode == ICON_MODE_TOP) { - minsize.x = MAX(minsize.x, s.width); - if (max_text_lines > 0) { - minsize.y += s.height + line_separation * max_text_lines; } else { - minsize.y += s.height; + minsize.y = MAX(minsize.y, s.height); + minsize.x += s.width; } + } - } else { - minsize.y = MAX(minsize.y, s.height); - minsize.x += s.width; + if (fixed_column_width > 0) { + minsize.x = fixed_column_width; } + max_column_width = MAX(max_column_width, minsize.x); + + // elements need to adapt to the selected size + minsize.y += vseparation; + minsize.x += hseparation; + items.write[i].rect_cache.size = minsize; + items.write[i].min_rect_cache.size = minsize; } - if (fixed_column_width > 0) { - minsize.x = fixed_column_width; + int fit_size = size.x - bg->get_minimum_size().width - mw; + + //2-attempt best fit + current_columns = 0x7FFFFFFF; + if (max_columns > 0) { + current_columns = max_columns; } - max_column_width = MAX(max_column_width, minsize.x); - // elements need to adapt to the selected size - minsize.y += vseparation; - minsize.x += hseparation; - items.write[i].rect_cache.size = minsize; - items.write[i].min_rect_cache.size = minsize; - } + while (true) { + //repeat until all fits + bool all_fit = true; + Vector2 ofs; + int col = 0; + int max_h = 0; + separators.clear(); + for (int i = 0; i < items.size(); i++) { + if (current_columns > 1 && items[i].rect_cache.size.width + ofs.x > fit_size) { + //went past + current_columns = MAX(col, 1); + all_fit = false; + break; + } - int fit_size = size.x - bg->get_minimum_size().width - mw; + if (same_column_width) { + items.write[i].rect_cache.size.x = max_column_width; + } + items.write[i].rect_cache.position = ofs; + max_h = MAX(max_h, items[i].rect_cache.size.y); + ofs.x += items[i].rect_cache.size.x + hseparation; + col++; + if (col == current_columns) { + if (i < items.size() - 1) { + separators.push_back(ofs.y + max_h + vseparation / 2); + } - //2-attempt best fit - current_columns = 0x7FFFFFFF; - if (max_columns > 0) { - current_columns = max_columns; - } + for (int j = i; j >= 0 && col > 0; j--, col--) { + items.write[j].rect_cache.size.y = max_h; + } - while (true) { - //repeat until all fits - bool all_fit = true; - Vector2 ofs; - int col = 0; - int max_h = 0; - separators.clear(); - for (int i = 0; i < items.size(); i++) { - if (current_columns > 1 && items[i].rect_cache.size.width + ofs.x > fit_size) { - //went past - current_columns = MAX(col, 1); - all_fit = false; - break; + ofs.x = 0; + ofs.y += max_h + vseparation; + col = 0; + max_h = 0; + } } - if (same_column_width) { - items.write[i].rect_cache.size.x = max_column_width; + for (int j = items.size() - 1; j >= 0 && col > 0; j--, col--) { + items.write[j].rect_cache.size.y = max_h; } - items.write[i].rect_cache.position = ofs; - max_h = MAX(max_h, items[i].rect_cache.size.y); - ofs.x += items[i].rect_cache.size.x + hseparation; - col++; - if (col == current_columns) { - if (i < items.size() - 1) { - separators.push_back(ofs.y + max_h + vseparation / 2); - } - for (int j = i; j >= 0 && col > 0; j--, col--) { - items.write[j].rect_cache.size.y = max_h; + if (all_fit) { + float page = MAX(0, size.height - bg->get_minimum_size().height); + float max = MAX(page, ofs.y + max_h); + if (auto_height) { + auto_height_value = ofs.y + max_h + bg->get_minimum_size().height; } + scroll_bar->set_max(max); + scroll_bar->set_page(page); + if (max <= page) { + scroll_bar->set_value(0); + scroll_bar->hide(); + } else { + scroll_bar->show(); - ofs.x = 0; - ofs.y += max_h + vseparation; - col = 0; - max_h = 0; + if (do_autoscroll_to_bottom) { + scroll_bar->set_value(max); + } + } + break; } } - for (int j = items.size() - 1; j >= 0 && col > 0; j--, col--) { - items.write[j].rect_cache.size.y = max_h; - } + update_minimum_size(); + shape_changed = false; + } - if (all_fit) { - float page = MAX(0, size.height - bg->get_minimum_size().height); - float max = MAX(page, ofs.y + max_h); - if (auto_height) { - auto_height_value = ofs.y + max_h + bg->get_minimum_size().height; - } - scroll_bar->set_max(max); - scroll_bar->set_page(page); - if (max <= page) { - scroll_bar->set_value(0); - scroll_bar->hide(); - } else { - scroll_bar->show(); + //ensure_selected_visible needs to be checked before we draw the list. + if (ensure_selected_visible && current >= 0 && current < items.size()) { + Rect2 r = items[current].rect_cache; + int from = scroll_bar->get_value(); + int to = from + scroll_bar->get_page(); - if (do_autoscroll_to_bottom) { - scroll_bar->set_value(max); - } - } - break; + if (r.position.y < from) { + scroll_bar->set_value(r.position.y); + } else if (r.position.y + r.size.y > to) { + scroll_bar->set_value(r.position.y + r.size.y - (to - from)); } } - update_minimum_size(); - shape_changed = false; - } - - //ensure_selected_visible needs to be checked before we draw the list. - if (ensure_selected_visible && current >= 0 && current < items.size()) { - Rect2 r = items[current].rect_cache; - int from = scroll_bar->get_value(); - int to = from + scroll_bar->get_page(); + ensure_selected_visible = false; - if (r.position.y < from) { - scroll_bar->set_value(r.position.y); - } else if (r.position.y + r.size.y > to) { - scroll_bar->set_value(r.position.y + r.size.y - (to - from)); - } - } + Vector2 base_ofs = bg->get_offset(); + base_ofs.y -= int(scroll_bar->get_value()); - ensure_selected_visible = false; + const Rect2 clip(-base_ofs, size); // visible frame, don't need to draw outside of there - Vector2 base_ofs = bg->get_offset(); - base_ofs.y -= int(scroll_bar->get_value()); - - const Rect2 clip(-base_ofs, size); // visible frame, don't need to draw outside of there - - int first_item_visible; - { - // do a binary search to find the first item whose rect reaches below clip.position.y - int lo = 0; - int hi = items.size(); - while (lo < hi) { - const int mid = (lo + hi) / 2; - const Rect2 &rcache = items[mid].rect_cache; - if (rcache.position.y + rcache.size.y < clip.position.y) { - lo = mid + 1; - } else { - hi = mid; + int first_item_visible; + { + // do a binary search to find the first item whose rect reaches below clip.position.y + int lo = 0; + int hi = items.size(); + while (lo < hi) { + const int mid = (lo + hi) / 2; + const Rect2 &rcache = items[mid].rect_cache; + if (rcache.position.y + rcache.size.y < clip.position.y) { + lo = mid + 1; + } else { + hi = mid; + } } - } - // we might have ended up with column 2, or 3, ..., so let's find the first column - while (lo > 0 && items[lo - 1].rect_cache.position.y == items[lo].rect_cache.position.y) { - lo -= 1; - } - first_item_visible = lo; - } - - for (int i = first_item_visible; i < items.size(); i++) { - Rect2 rcache = items[i].rect_cache; - - if (rcache.position.y > clip.position.y + clip.size.y) { - break; // done + // we might have ended up with column 2, or 3, ..., so let's find the first column + while (lo > 0 && items[lo - 1].rect_cache.position.y == items[lo].rect_cache.position.y) { + lo -= 1; + } + first_item_visible = lo; } - if (!clip.intersects(rcache)) { - continue; - } + for (int i = first_item_visible; i < items.size(); i++) { + Rect2 rcache = items[i].rect_cache; - if (current_columns == 1) { - rcache.size.width = width - rcache.position.x; - } + if (rcache.position.y > clip.position.y + clip.size.y) { + break; // done + } - if (items[i].selected) { - Rect2 r = rcache; - r.position += base_ofs; - r.position.y -= vseparation / 2; - 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; + if (!clip.intersects(rcache)) { + continue; } - draw_style_box(sbsel, r); - } - if (items[i].custom_bg.a > 0.001) { - Rect2 r = rcache; - r.position += base_ofs; - - // Size rect to make the align the temperature colors - r.position.y -= vseparation / 2; - 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; + if (current_columns == 1) { + rcache.size.width = width - rcache.position.x; } - draw_rect(r, items[i].custom_bg); - } + if (items[i].selected) { + Rect2 r = rcache; + r.position += base_ofs; + r.position.y -= vseparation / 2; + r.size.y += vseparation; + r.position.x -= hseparation / 2; + r.size.x += hseparation; - Vector2 text_ofs; - if (items[i].icon.is_valid()) { - Size2 icon_size; - //= _adjust_to_max_size(items[i].get_icon_size(),fixed_icon_size) * icon_scale; + if (rtl) { + r.position.x = size.width - r.position.x - r.size.x; + } - if (fixed_icon_size.x > 0 && fixed_icon_size.y > 0) { - icon_size = fixed_icon_size * icon_scale; - } else { - icon_size = items[i].get_icon_size() * icon_scale; + draw_style_box(sbsel, r); } + if (items[i].custom_bg.a > 0.001) { + Rect2 r = rcache; + r.position += base_ofs; - Vector2 icon_ofs; + // Size rect to make the align the temperature colors + r.position.y -= vseparation / 2; + r.size.y += vseparation; + r.position.x -= hseparation / 2; + r.size.x += hseparation; - Point2 pos = items[i].rect_cache.position + icon_ofs + base_ofs; + if (rtl) { + r.position.x = size.width - r.position.x - r.size.x; + } - if (icon_mode == ICON_MODE_TOP) { - pos.x += Math::floor((items[i].rect_cache.size.width - icon_size.width) / 2); - 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; + draw_rect(r, items[i].custom_bg); } - Rect2 draw_rect = Rect2(pos, icon_size); + Vector2 text_ofs; + if (items[i].icon.is_valid()) { + Size2 icon_size; + //= _adjust_to_max_size(items[i].get_icon_size(),fixed_icon_size) * icon_scale; - if (fixed_icon_size.x > 0 && fixed_icon_size.y > 0) { - Rect2 adj = _adjust_to_max_size(items[i].get_icon_size() * icon_scale, icon_size); - draw_rect.position += adj.position; - draw_rect.size = adj.size; - } + if (fixed_icon_size.x > 0 && fixed_icon_size.y > 0) { + icon_size = fixed_icon_size * icon_scale; + } else { + icon_size = items[i].get_icon_size() * icon_scale; + } - Color modulate = items[i].icon_modulate; - if (items[i].disabled) { - modulate.a *= 0.5; - } + Vector2 icon_ofs; - // If the icon is transposed, we have to switch the size so that it is drawn correctly - if (items[i].icon_transposed) { - Size2 size_tmp = draw_rect.size; - draw_rect.size.x = size_tmp.y; - draw_rect.size.y = size_tmp.x; - } + Point2 pos = items[i].rect_cache.position + icon_ofs + base_ofs; - 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 (icon_mode == ICON_MODE_TOP) { + pos.x += Math::floor((items[i].rect_cache.size.width - icon_size.width) / 2); + 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; + } - 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); - } + Rect2 draw_rect = Rect2(pos, icon_size); - if (items[i].tag_icon.is_valid()) { - 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 (fixed_icon_size.x > 0 && fixed_icon_size.y > 0) { + Rect2 adj = _adjust_to_max_size(items[i].get_icon_size() * icon_scale, icon_size); + draw_rect.position += adj.position; + draw_rect.size = adj.size; + } - if (!items[i].text.is_empty()) { - int max_len = -1; + Color modulate = items[i].icon_modulate; + if (items[i].disabled) { + modulate.a *= 0.5; + } - Vector2 size2 = items[i].text_buf->get_size(); - if (fixed_column_width) { - max_len = fixed_column_width; - } else if (same_column_width) { - max_len = items[i].rect_cache.size.x; - } else { - max_len = size2.x; - } + // If the icon is transposed, we have to switch the size so that it is drawn correctly + if (items[i].icon_transposed) { + Size2 size_tmp = draw_rect.size; + draw_rect.size.x = size_tmp.y; + draw_rect.size.y = size_tmp.x; + } - 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; - } + 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 (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { - text_ofs += base_ofs; - text_ofs += items[i].rect_cache.position; + 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()) { + Point2 draw_pos = items[i].rect_cache.position; if (rtl) { - text_ofs.x = size.width - text_ofs.x - max_len; + draw_pos.x = size.width - draw_pos.x - items[i].tag_icon->get_width(); } + draw_texture(items[i].tag_icon, draw_pos + base_ofs); + } - items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_CENTER); + if (!items[i].text.is_empty()) { + int max_len = -1; - 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); + Vector2 size2 = items[i].text_buf->get_size(); + if (fixed_column_width) { + max_len = fixed_column_width; + } else if (same_column_width) { + max_len = items[i].rect_cache.size.x; + } else { + max_len = size2.x; } - 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); + 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) { - text_ofs.x += (items[i].rect_cache.size.width - size2.x) / 2; - } else { - text_ofs.y += (items[i].rect_cache.size.height - size2.y) / 2; - } + if (icon_mode == ICON_MODE_TOP && max_text_lines > 0) { + text_ofs += base_ofs; + text_ofs += items[i].rect_cache.position; - text_ofs += base_ofs; - text_ofs += items[i].rect_cache.position; + if (rtl) { + text_ofs.x = size.width - text_ofs.x - max_len; + } - if (rtl) { - text_ofs.x = size.width - text_ofs.x - max_len; - } + items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_CENTER); - items.write[i].text_buf->set_width(max_len); + 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); + } - if (rtl) { - items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + items[i].text_buf->draw(get_canvas_item(), text_ofs, modulate); } else { - items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_LEFT); + if (fixed_column_width > 0) { + size2.x = MIN(size2.x, fixed_column_width); + } + + if (icon_mode == ICON_MODE_TOP) { + text_ofs.x += (items[i].rect_cache.size.width - size2.x) / 2; + } else { + text_ofs.y += (items[i].rect_cache.size.height - size2.y) / 2; + } + + text_ofs += base_ofs; + text_ofs += items[i].rect_cache.position; + + if (rtl) { + text_ofs.x = size.width - text_ofs.x - max_len; + } + + items.write[i].text_buf->set_width(width - text_ofs.x); + + if (rtl) { + items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_RIGHT); + } else { + items.write[i].text_buf->set_alignment(HORIZONTAL_ALIGNMENT_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); + } + + if (width - text_ofs.x > 0) { + items[i].text_buf->draw(get_canvas_item(), text_ofs, modulate); + } } + } - 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); + if (select_mode == SELECT_MULTI && i == current) { + Rect2 r = rcache; + r.position += base_ofs; + r.position.y -= vseparation / 2; + 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; } - items[i].text_buf->draw(get_canvas_item(), text_ofs, modulate); + draw_style_box(cursor, r); } } - if (select_mode == SELECT_MULTI && i == current) { - Rect2 r = rcache; - r.position += base_ofs; - r.position.y -= vseparation / 2; - 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; + int first_visible_separator = 0; + { + // do a binary search to find the first separator that is below clip_position.y + int lo = 0; + int hi = separators.size(); + while (lo < hi) { + const int mid = (lo + hi) / 2; + if (separators[mid] < clip.position.y) { + lo = mid + 1; + } else { + hi = mid; + } } - - draw_style_box(cursor, r); + first_visible_separator = lo; } - } - int first_visible_separator = 0; - { - // do a binary search to find the first separator that is below clip_position.y - int lo = 0; - int hi = separators.size(); - while (lo < hi) { - const int mid = (lo + hi) / 2; - if (separators[mid] < clip.position.y) { - lo = mid + 1; - } else { - hi = mid; + for (int i = first_visible_separator; i < separators.size(); i++) { + if (separators[i] > clip.position.y + clip.size.y) { + break; // done } - } - first_visible_separator = lo; - } - for (int i = first_visible_separator; i < separators.size(); i++) { - if (separators[i] > clip.position.y + clip.size.y) { - break; // done + const int y = base_ofs.y + separators[i]; + draw_line(Vector2(bg->get_margin(SIDE_LEFT), y), Vector2(width, y), guide_color); } - - const int y = base_ofs.y + separators[i]; - draw_line(Vector2(bg->get_margin(SIDE_LEFT), y), Vector2(width, y), guide_color); - } + } break; } } @@ -1492,6 +1578,9 @@ bool ItemList::_set(const StringName &p_name, const Variant &p_value) { } else if (components[1] == "disabled") { set_item_disabled(item_index, p_value); return true; + } else if (components[1] == "selectable") { + set_item_selectable(item_index, p_value); + return true; } } #ifndef DISABLE_DEPRECATED @@ -1528,6 +1617,9 @@ bool ItemList::_get(const StringName &p_name, Variant &r_ret) const { } else if (components[1] == "disabled") { r_ret = is_item_disabled(item_index); return true; + } else if (components[1] == "selectable") { + r_ret = is_item_selectable(item_index); + return true; } } return false; @@ -1541,6 +1633,10 @@ void ItemList::_get_property_list(List<PropertyInfo> *p_list) const { pi.usage &= ~(get_item_icon(i).is_null() ? PROPERTY_USAGE_STORAGE : 0); p_list->push_back(pi); + pi = PropertyInfo(Variant::BOOL, vformat("item_%d/selectable", i)); + pi.usage &= ~(is_item_selectable(i) ? PROPERTY_USAGE_STORAGE : 0); + p_list->push_back(pi); + pi = PropertyInfo(Variant::BOOL, vformat("item_%d/disabled", i)); pi.usage &= ~(!is_item_disabled(i) ? PROPERTY_USAGE_STORAGE : 0); p_list->push_back(pi); @@ -1652,7 +1748,7 @@ void ItemList::_bind_methods() { ClassDB::bind_method(D_METHOD("ensure_current_is_visible"), &ItemList::ensure_current_is_visible); - ClassDB::bind_method(D_METHOD("get_v_scroll"), &ItemList::get_v_scroll); + ClassDB::bind_method(D_METHOD("get_v_scroll_bar"), &ItemList::get_v_scroll_bar); 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); @@ -1680,11 +1776,10 @@ void ItemList::_bind_methods() { BIND_ENUM_CONSTANT(SELECT_MULTI); ADD_SIGNAL(MethodInfo("item_selected", PropertyInfo(Variant::INT, "index"))); - ADD_SIGNAL(MethodInfo("item_rmb_selected", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::VECTOR2, "at_position"))); + ADD_SIGNAL(MethodInfo("empty_clicked", PropertyInfo(Variant::VECTOR2, "at_position"), PropertyInfo(Variant::INT, "mouse_button_index"))); + ADD_SIGNAL(MethodInfo("item_clicked", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::VECTOR2, "at_position"), PropertyInfo(Variant::INT, "mouse_button_index"))); ADD_SIGNAL(MethodInfo("multi_selected", PropertyInfo(Variant::INT, "index"), PropertyInfo(Variant::BOOL, "selected"))); ADD_SIGNAL(MethodInfo("item_activated", PropertyInfo(Variant::INT, "index"))); - ADD_SIGNAL(MethodInfo("rmb_clicked", PropertyInfo(Variant::VECTOR2, "at_position"))); - ADD_SIGNAL(MethodInfo("nothing_selected")); GLOBAL_DEF("gui/timers/incremental_search_max_interval_msec", 2000); ProjectSettings::get_singleton()->set_custom_property_info("gui/timers/incremental_search_max_interval_msec", PropertyInfo(Variant::INT, "gui/timers/incremental_search_max_interval_msec", PROPERTY_HINT_RANGE, "0,10000,1,or_greater")); // No negative numbers diff --git a/scene/gui/item_list.h b/scene/gui/item_list.h index e780179e7b..ffbe7d055a 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -62,7 +62,7 @@ private: String language; TextDirection text_direction = TEXT_DIRECTION_AUTO; - bool selectable = false; + bool selectable = true; bool selected = false; bool disabled = false; bool tooltip_enabled = true; @@ -98,8 +98,8 @@ private: SelectMode select_mode = SELECT_SINGLE; IconMode icon_mode = ICON_MODE_LEFT; - VScrollBar *scroll_bar; - TextParagraph::OverrunBehavior text_overrun_behavior = TextParagraph::OVERRUN_NO_TRIMMING; + VScrollBar *scroll_bar = nullptr; + TextParagraph::OverrunBehavior text_overrun_behavior = TextParagraph::OVERRUN_TRIM_ELLIPSIS; uint64_t search_time_msec = 0; String search_string; @@ -255,7 +255,7 @@ public: void set_autoscroll_to_bottom(const bool p_enable); - VScrollBar *get_v_scroll() { return scroll_bar; } + VScrollBar *get_v_scroll_bar() { return scroll_bar; } ItemList(); ~ItemList(); diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index 0a09d87509..eda3d40f63 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -82,8 +82,11 @@ 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 (dirty || font_dirty) { + String lang = (!language.is_empty()) ? language : TranslationServer::get_singleton()->get_tool_locale(); + 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 { @@ -92,19 +95,27 @@ void Label::_shape() { 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()); - String text = (uppercase) ? xl_text.to_upper() : xl_text; - if (visible_chars >= 0) { + String text = (uppercase) ? TS->string_to_upper(xl_text, lang) : xl_text; + if (visible_chars >= 0 && visible_chars_behavior == VC_CHARS_BEFORE_SHAPING) { text = text.substr(0, visible_chars); } - TS->shaped_text_add_string(text_rid, text, font->get_rids(), font_size, opentype_features, (!language.is_empty()) ? language : TranslationServer::get_singleton()->get_tool_locale()); + if (dirty) { + TS->shaped_text_add_string(text_rid, text, font->get_rids(), font_size, opentype_features, lang); + } else { + int spans = TS->shaped_get_span_count(text_rid); + for (int i = 0; i < spans; i++) { + TS->shaped_set_span_update_font(text_rid, i, font->get_rids(), font_size, opentype_features); + } + } TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, text)); dirty = false; + font_dirty = false; lines_dirty = true; } if (lines_dirty) { for (int i = 0; i < lines_rid.size(); i++) { - TS->free(lines_rid[i]); + TS->free_rid(lines_rid[i]); } lines_rid.clear(); @@ -183,11 +194,9 @@ void Label::_shape() { 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++) { @@ -254,253 +263,288 @@ inline void draw_glyph_outline(const Glyph &p_gl, const RID &p_canvas, const Col } void Label::_notification(int p_what) { - if (p_what == NOTIFICATION_TRANSLATION_CHANGED) { - String new_text = atr(text); - if (new_text == xl_text) { - return; // Nothing new. - } - xl_text = new_text; - if (percent_visible < 1) { - visible_chars = get_total_character_count() * percent_visible; - } - dirty = true; + switch (p_what) { + case NOTIFICATION_TRANSLATION_CHANGED: { + String new_text = atr(text); + if (new_text == xl_text) { + return; // Nothing new. + } + xl_text = new_text; + if (percent_visible < 1) { + visible_chars = get_total_character_count() * percent_visible; + } + dirty = true; - update(); - } + update(); + } break; - if (p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED) { - update(); - } + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + update(); + } break; - if (p_what == NOTIFICATION_DRAW) { - if (clip) { - RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), true); - } + case NOTIFICATION_DRAW: { + if (clip) { + RenderingServer::get_singleton()->canvas_item_set_clip(get_canvas_item(), true); + } - if (dirty || lines_dirty) { - _shape(); - } + if (dirty || font_dirty || lines_dirty) { + _shape(); + } - RID ci = get_canvas_item(); - - Size2 string_size; - Size2 size = get_size(); - 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); - bool rtl_layout = is_layout_rtl(); - - style->draw(ci, Rect2(Point2(0, 0), get_size())); - - float total_h = 0.0; - int lines_visible = 0; - - // 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; + RID ci = get_canvas_item(); + + Size2 string_size; + Size2 size = get_size(); + 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_inferred_direction(text_rid) == TextServer::DIRECTION_RTL); + bool rtl_layout = is_layout_rtl(); + + style->draw(ci, Rect2(Point2(0, 0), get_size())); + + float total_h = 0.0; + int lines_visible = 0; + + // 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++; } - lines_visible++; - } - if (max_lines_visible >= 0 && lines_visible > max_lines_visible) { - lines_visible = max_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); + int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped); + bool trim_chars = (visible_chars >= 0) && (visible_chars_behavior == VC_CHARS_AFTER_SHAPING); + bool trim_glyphs_ltr = (visible_chars >= 0) && ((visible_chars_behavior == VC_GLYPHS_LTR) || ((visible_chars_behavior == VC_GLYPHS_AUTO) && !rtl_layout)); + bool trim_glyphs_rtl = (visible_chars >= 0) && ((visible_chars_behavior == VC_GLYPHS_RTL) || ((visible_chars_behavior == VC_GLYPHS_AUTO) && rtl_layout)); + + // Get real total height. + int total_glyphs = 0; + 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_glyphs += TS->shaped_text_get_glyph_count(lines_rid[i]) + TS->shaped_text_get_ellipsis_glyph_count(lines_rid[i]); + } + int visible_glyphs = total_glyphs * percent_visible; + int processed_glyphs = 0; + total_h += style->get_margin(SIDE_TOP) + style->get_margin(SIDE_BOTTOM); + + int vbegin = 0, vsep = 0; + if (lines_visible > 0) { + switch (vertical_alignment) { + case VERTICAL_ALIGNMENT_TOP: { + // Nothing. + } break; + case VERTICAL_ALIGNMENT_CENTER: { + vbegin = (size.y - (total_h - line_spacing)) / 2; + vsep = 0; - // 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 (vertical_alignment) { - case VERTICAL_ALIGNMENT_TOP: { - // Nothing. - } break; - case VERTICAL_ALIGNMENT_CENTER: { - vbegin = (size.y - (total_h - line_spacing)) / 2; - vsep = 0; - - } break; - case VERTICAL_ALIGNMENT_BOTTOM: { - vbegin = size.y - (total_h - line_spacing); - vsep = 0; - - } break; - case VERTICAL_ALIGNMENT_FILL: { - vbegin = 0; - if (lines_visible > 1) { - vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1); - } else { + } break; + case VERTICAL_ALIGNMENT_BOTTOM: { + vbegin = size.y - (total_h - line_spacing); vsep = 0; - } - } break; - } - } + } break; + case VERTICAL_ALIGNMENT_FILL: { + vbegin = 0; + if (lines_visible > 1) { + vsep = (size.y - (total_h - line_spacing)) / (lines_visible - 1); + } else { + vsep = 0; + } - 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 (horizontal_alignment) { - case HORIZONTAL_ALIGNMENT_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 HORIZONTAL_ALIGNMENT_LEFT: { - if (rtl_layout) { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); - } else { - ofs.x = style->get_offset().x; - } - } break; - case HORIZONTAL_ALIGNMENT_CENTER: { - ofs.x = int(size.width - line_size.width) / 2; - } break; - case HORIZONTAL_ALIGNMENT_RIGHT: { - if (rtl_layout) { - ofs.x = style->get_offset().x; - } else { - ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); - } - } break; + } break; + } } - const Glyph *glyphs = TS->shaped_text_get_glyphs(lines_rid[i]); - int gl_size = TS->shaped_text_get_glyph_count(lines_rid[i]); - - int ellipsis_pos = TS->shaped_text_get_ellipsis_pos(lines_rid[i]); - int trim_pos = TS->shaped_text_get_trim_pos(lines_rid[i]); - - const Glyph *ellipsis_glyphs = TS->shaped_text_get_ellipsis_glyphs(lines_rid[i]); - int ellipsis_gl_size = TS->shaped_text_get_ellipsis_glyph_count(lines_rid[i]); + 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 (horizontal_alignment) { + case HORIZONTAL_ALIGNMENT_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 HORIZONTAL_ALIGNMENT_LEFT: { + if (rtl_layout) { + ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); + } else { + ofs.x = style->get_offset().x; + } + } break; + case HORIZONTAL_ALIGNMENT_CENTER: { + ofs.x = int(size.width - line_size.width) / 2; + } break; + case HORIZONTAL_ALIGNMENT_RIGHT: { + if (rtl_layout) { + ofs.x = style->get_offset().x; + } else { + ofs.x = int(size.width - style->get_margin(SIDE_RIGHT) - line_size.width); + } + } break; + } - // Draw outline. Note: Do not merge this into the single loop with the main text, to prevent overlaps. - if ((outline_size > 0 && font_outline_color.a != 0) || (font_shadow_color.a != 0)) { - Vector2 offset = ofs; - // Draw RTL ellipsis string when necessary. - if (rtl && ellipsis_pos >= 0) { - for (int gl_idx = ellipsis_gl_size - 1; gl_idx >= 0; gl_idx--) { - for (int j = 0; j < ellipsis_glyphs[gl_idx].repeat; j++) { - //Draw glyph outlines and shadow. - draw_glyph_outline(ellipsis_glyphs[gl_idx], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); - offset.x += ellipsis_glyphs[gl_idx].advance; + const Glyph *glyphs = TS->shaped_text_get_glyphs(lines_rid[i]); + int gl_size = TS->shaped_text_get_glyph_count(lines_rid[i]); + + int ellipsis_pos = TS->shaped_text_get_ellipsis_pos(lines_rid[i]); + int trim_pos = TS->shaped_text_get_trim_pos(lines_rid[i]); + + const Glyph *ellipsis_glyphs = TS->shaped_text_get_ellipsis_glyphs(lines_rid[i]); + int ellipsis_gl_size = TS->shaped_text_get_ellipsis_glyph_count(lines_rid[i]); + + // Draw outline. Note: Do not merge this into the single loop with the main text, to prevent overlaps. + int processed_glyphs_ol = processed_glyphs; + if ((outline_size > 0 && font_outline_color.a != 0) || (font_shadow_color.a != 0)) { + Vector2 offset = ofs; + // Draw RTL ellipsis string when necessary. + if (rtl && ellipsis_pos >= 0) { + for (int gl_idx = ellipsis_gl_size - 1; gl_idx >= 0; gl_idx--) { + for (int j = 0; j < ellipsis_glyphs[gl_idx].repeat; j++) { + bool skip = (trim_chars && ellipsis_glyphs[gl_idx].end > visible_chars) || (trim_glyphs_ltr && (processed_glyphs_ol >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs_ol < total_glyphs - visible_glyphs)); + //Draw glyph outlines and shadow. + if (!skip) { + draw_glyph_outline(ellipsis_glyphs[gl_idx], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); + } + processed_glyphs_ol++; + offset.x += ellipsis_glyphs[gl_idx].advance; + } } } - } - // Draw main text. - for (int j = 0; j < gl_size; j++) { - for (int k = 0; k < glyphs[j].repeat; k++) { + // Draw main text. + for (int j = 0; j < gl_size; j++) { // Trim when necessary. if (trim_pos >= 0) { if (rtl) { - if (j < trim_pos && (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + if (j < trim_pos) { continue; } } else { - if (j >= trim_pos && (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + if (j >= trim_pos) { break; } } } + for (int k = 0; k < glyphs[j].repeat; k++) { + bool skip = (trim_chars && glyphs[j].end > visible_chars) || (trim_glyphs_ltr && (processed_glyphs_ol >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs_ol < total_glyphs - visible_glyphs)); - // Draw glyph outlines and shadow. - draw_glyph_outline(glyphs[j], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); - offset.x += glyphs[j].advance; + // Draw glyph outlines and shadow. + if (!skip) { + draw_glyph_outline(glyphs[j], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); + } + processed_glyphs_ol++; + offset.x += glyphs[j].advance; + } } - } - // Draw LTR ellipsis string when necessary. - if (!rtl && ellipsis_pos >= 0) { - for (int gl_idx = 0; gl_idx < ellipsis_gl_size; gl_idx++) { - for (int j = 0; j < ellipsis_glyphs[gl_idx].repeat; j++) { - //Draw glyph outlines and shadow. - draw_glyph_outline(ellipsis_glyphs[gl_idx], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); - offset.x += ellipsis_glyphs[gl_idx].advance; + // Draw LTR ellipsis string when necessary. + if (!rtl && ellipsis_pos >= 0) { + for (int gl_idx = 0; gl_idx < ellipsis_gl_size; gl_idx++) { + for (int j = 0; j < ellipsis_glyphs[gl_idx].repeat; j++) { + bool skip = (trim_chars && ellipsis_glyphs[gl_idx].end > visible_chars) || (trim_glyphs_ltr && (processed_glyphs_ol >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs_ol < total_glyphs - visible_glyphs)); + //Draw glyph outlines and shadow. + if (!skip) { + draw_glyph_outline(ellipsis_glyphs[gl_idx], ci, font_color, font_shadow_color, font_outline_color, shadow_outline_size, outline_size, offset, shadow_ofs); + } + processed_glyphs_ol++; + offset.x += ellipsis_glyphs[gl_idx].advance; + } } } } - } - // Draw main text. Note: Do not merge this into the single loop with the outline, to prevent overlaps. + // Draw main text. Note: Do not merge this into the single loop with the outline, to prevent overlaps. - // Draw RTL ellipsis string when necessary. - if (rtl && ellipsis_pos >= 0) { - for (int gl_idx = ellipsis_gl_size - 1; gl_idx >= 0; gl_idx--) { - for (int j = 0; j < ellipsis_glyphs[gl_idx].repeat; j++) { - //Draw glyph outlines and shadow. - draw_glyph(ellipsis_glyphs[gl_idx], ci, font_color, ofs); - ofs.x += ellipsis_glyphs[gl_idx].advance; + // Draw RTL ellipsis string when necessary. + if (rtl && ellipsis_pos >= 0) { + for (int gl_idx = ellipsis_gl_size - 1; gl_idx >= 0; gl_idx--) { + for (int j = 0; j < ellipsis_glyphs[gl_idx].repeat; j++) { + bool skip = (trim_chars && ellipsis_glyphs[gl_idx].end > visible_chars) || (trim_glyphs_ltr && (processed_glyphs >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs < total_glyphs - visible_glyphs)); + //Draw glyph outlines and shadow. + if (!skip) { + draw_glyph(ellipsis_glyphs[gl_idx], ci, font_color, ofs); + } + processed_glyphs++; + ofs.x += ellipsis_glyphs[gl_idx].advance; + } } } - } - // Draw main text. - for (int j = 0; j < gl_size; j++) { - for (int k = 0; k < glyphs[j].repeat; k++) { + // Draw main text. + for (int j = 0; j < gl_size; j++) { // Trim when necessary. if (trim_pos >= 0) { if (rtl) { - if (j < trim_pos && (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + if (j < trim_pos) { continue; } } else { - if (j >= trim_pos && (glyphs[j].flags & TextServer::GRAPHEME_IS_VIRTUAL) != TextServer::GRAPHEME_IS_VIRTUAL) { + if (j >= trim_pos) { break; } } } + for (int k = 0; k < glyphs[j].repeat; k++) { + bool skip = (trim_chars && glyphs[j].end > visible_chars) || (trim_glyphs_ltr && (processed_glyphs >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs < total_glyphs - visible_glyphs)); - // Draw glyph outlines and shadow. - draw_glyph(glyphs[j], ci, font_color, ofs); - ofs.x += glyphs[j].advance; + // Draw glyph outlines and shadow. + if (!skip) { + draw_glyph(glyphs[j], ci, font_color, ofs); + } + processed_glyphs++; + ofs.x += glyphs[j].advance; + } } - } - // Draw LTR ellipsis string when necessary. - if (!rtl && ellipsis_pos >= 0) { - for (int gl_idx = 0; gl_idx < ellipsis_gl_size; gl_idx++) { - for (int j = 0; j < ellipsis_glyphs[gl_idx].repeat; j++) { - //Draw glyph outlines and shadow. - draw_glyph(ellipsis_glyphs[gl_idx], ci, font_color, ofs); - ofs.x += ellipsis_glyphs[gl_idx].advance; + // Draw LTR ellipsis string when necessary. + if (!rtl && ellipsis_pos >= 0) { + for (int gl_idx = 0; gl_idx < ellipsis_gl_size; gl_idx++) { + for (int j = 0; j < ellipsis_glyphs[gl_idx].repeat; j++) { + bool skip = (trim_chars && ellipsis_glyphs[gl_idx].end > visible_chars) || (trim_glyphs_ltr && (processed_glyphs >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs < total_glyphs - visible_glyphs)); + //Draw glyph outlines and shadow. + if (!skip) { + draw_glyph(ellipsis_glyphs[gl_idx], ci, font_color, ofs); + } + processed_glyphs++; + ofs.x += ellipsis_glyphs[gl_idx].advance; + } } } + ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing + font->get_spacing(TextServer::SPACING_BOTTOM); } - ofs.y += TS->shaped_text_get_descent(lines_rid[i]) + vsep + line_spacing + font->get_spacing(TextServer::SPACING_BOTTOM); - } - } + } break; - if (p_what == NOTIFICATION_THEME_CHANGED) { - dirty = true; - update(); - } - if (p_what == NOTIFICATION_RESIZED) { - lines_dirty = true; + case NOTIFICATION_THEME_CHANGED: { + font_dirty = true; + update(); + } break; + + case NOTIFICATION_RESIZED: { + lines_dirty = true; + } break; } } Size2 Label::get_minimum_size() const { // don't want to mutable everything - if (dirty || lines_dirty) { + if (dirty || font_dirty || lines_dirty) { const_cast<Label *>(this)->_shape(); } @@ -524,7 +568,7 @@ int Label::get_line_count() const { if (!is_inside_tree()) { return 1; } - if (dirty || lines_dirty) { + if (dirty || font_dirty || lines_dirty) { const_cast<Label *>(this)->_shape(); } @@ -599,12 +643,12 @@ 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; + font_dirty = true; update(); } } -void Label::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { +void Label::set_structured_text_bidi_override(TextServer::StructuredTextParser p_parser) { if (st_parser != p_parser) { st_parser = p_parser; dirty = true; @@ -612,7 +656,7 @@ void Label::set_structured_text_bidi_override(Control::StructuredTextParser p_pa } } -Control::StructuredTextParser Label::get_structured_text_bidi_override() const { +TextServer::StructuredTextParser Label::get_structured_text_bidi_override() const { return st_parser; } @@ -632,7 +676,7 @@ Control::TextDirection Label::get_text_direction() const { void Label::clear_opentype_features() { opentype_features.clear(); - dirty = true; + font_dirty = true; update(); } @@ -640,7 +684,7 @@ 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; + font_dirty = true; update(); } } @@ -702,7 +746,9 @@ void Label::set_visible_characters(int p_amount) { } else { percent_visible = 1.0; } - dirty = true; + if (visible_chars_behavior == VC_CHARS_BEFORE_SHAPING) { + dirty = true; + } update(); } } @@ -720,7 +766,9 @@ void Label::set_percent_visible(float p_percent) { visible_chars = get_total_character_count() * p_percent; percent_visible = p_percent; } - dirty = true; + if (visible_chars_behavior == VC_CHARS_BEFORE_SHAPING) { + dirty = true; + } update(); } } @@ -729,6 +777,18 @@ float Label::get_percent_visible() const { return percent_visible; } +Label::VisibleCharactersBehavior Label::get_visible_characters_behavior() const { + return visible_chars_behavior; +} + +void Label::set_visible_characters_behavior(Label::VisibleCharactersBehavior p_behavior) { + if (visible_chars_behavior != p_behavior) { + visible_chars_behavior = p_behavior; + dirty = true; + update(); + } +} + void Label::set_lines_skipped(int p_lines) { ERR_FAIL_COND(p_lines < 0); lines_skipped = p_lines; @@ -751,7 +811,7 @@ int Label::get_max_lines_visible() const { } int Label::get_total_character_count() const { - if (dirty || lines_dirty) { + if (dirty || font_dirty || lines_dirty) { const_cast<Label *>(this)->_shape(); } @@ -763,17 +823,17 @@ bool Label::_set(const StringName &p_name, const Variant &p_value) { if (str.begins_with("opentype_features/")) { String name = str.get_slicec('/', 1); int32_t tag = TS->name_to_tag(name); - double value = p_value; + int value = p_value; if (value == -1) { if (opentype_features.has(tag)) { opentype_features.erase(tag); - dirty = true; + font_dirty = true; update(); } } else { - if ((double)opentype_features[tag] != value) { + if (!opentype_features.has(tag) || (int)opentype_features[tag] != value) { opentype_features[tag] = value; - dirty = true; + font_dirty = true; update(); } } @@ -803,7 +863,7 @@ bool Label::_get(const StringName &p_name, Variant &r_ret) const { 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::INT, "opentype_features/" + name)); } p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); } @@ -836,6 +896,8 @@ void Label::_bind_methods() { ClassDB::bind_method(D_METHOD("get_total_character_count"), &Label::get_total_character_count); ClassDB::bind_method(D_METHOD("set_visible_characters", "amount"), &Label::set_visible_characters); ClassDB::bind_method(D_METHOD("get_visible_characters"), &Label::get_visible_characters); + ClassDB::bind_method(D_METHOD("get_visible_characters_behavior"), &Label::get_visible_characters_behavior); + ClassDB::bind_method(D_METHOD("set_visible_characters_behavior", "behavior"), &Label::set_visible_characters_behavior); ClassDB::bind_method(D_METHOD("set_percent_visible", "percent_visible"), &Label::set_percent_visible); ClassDB::bind_method(D_METHOD("get_percent_visible"), &Label::get_percent_visible); ClassDB::bind_method(D_METHOD("set_lines_skipped", "lines_skipped"), &Label::set_lines_skipped); @@ -858,20 +920,31 @@ void Label::_bind_methods() { BIND_ENUM_CONSTANT(OVERRUN_TRIM_ELLIPSIS); BIND_ENUM_CONSTANT(OVERRUN_TRIM_WORD_ELLIPSIS); + BIND_ENUM_CONSTANT(VC_CHARS_BEFORE_SHAPING); + BIND_ENUM_CONSTANT(VC_CHARS_AFTER_SHAPING); + BIND_ENUM_CONSTANT(VC_GLYPHS_AUTO); + BIND_ENUM_CONSTANT(VC_GLYPHS_LTR); + BIND_ENUM_CONSTANT(VC_GLYPHS_RTL); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text"); - ADD_GROUP("Locale", ""); - 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, "horizontal_alignment", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_horizontal_alignment", "get_horizontal_alignment"); ADD_PROPERTY(PropertyInfo(Variant::INT, "vertical_alignment", PROPERTY_HINT_ENUM, "Top,Center,Bottom,Fill"), "set_vertical_alignment", "get_vertical_alignment"); 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"), "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"); + + // Note: "visible_characters" and "percent_visible" should be set after "text" to be correctly applied. + ADD_PROPERTY(PropertyInfo(Variant::INT, "visible_characters", PROPERTY_HINT_RANGE, "-1,128000,1"), "set_visible_characters", "get_visible_characters"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "visible_characters_behavior", PROPERTY_HINT_ENUM, "Characters Before Shaping,Characters After Shaping,Glyphs (Layout Direction),Glyphs (Left-to-Right),Glyphs (Right-to-Left)"), "set_visible_characters_behavior", "get_visible_characters_behavior"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "percent_visible", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_percent_visible", "get_percent_visible"); + + ADD_GROUP("Locale", ""); + 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", PROPERTY_HINT_LOCALE_ID, ""), "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"); @@ -887,8 +960,8 @@ Label::Label(const String &p_text) { Label::~Label() { for (int i = 0; i < lines_rid.size(); i++) { - TS->free(lines_rid[i]); + TS->free_rid(lines_rid[i]); } lines_rid.clear(); - TS->free(text_rid); + TS->free_rid(text_rid); } diff --git a/scene/gui/label.h b/scene/gui/label.h index 2ad8ae8818..f7b725928f 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +52,14 @@ public: OVERRUN_TRIM_WORD_ELLIPSIS, }; + enum VisibleCharactersBehavior { + VC_CHARS_BEFORE_SHAPING, + VC_CHARS_AFTER_SHAPING, + VC_GLYPHS_AUTO, + VC_GLYPHS_LTR, + VC_GLYPHS_RTL, + }; + private: HorizontalAlignment horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT; VerticalAlignment vertical_alignment = VERTICAL_ALIGNMENT_TOP; @@ -65,17 +73,19 @@ private: bool lines_dirty = true; bool dirty = true; + bool font_dirty = true; RID text_rid; Vector<RID> lines_rid; Dictionary opentype_features; String language; TextDirection text_direction = TEXT_DIRECTION_AUTO; - Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + TextServer::StructuredTextParser st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; Array st_args; float percent_visible = 1.0; + VisibleCharactersBehavior visible_chars_behavior = VC_CHARS_BEFORE_SHAPING; int visible_chars = -1; int lines_skipped = 0; int max_lines_visible = -1; @@ -114,8 +124,8 @@ public: 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(TextServer::StructuredTextParser p_parser); + TextServer::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; @@ -126,6 +136,9 @@ public: void set_uppercase(bool p_uppercase); bool is_uppercase() const; + VisibleCharactersBehavior get_visible_characters_behavior() const; + void set_visible_characters_behavior(VisibleCharactersBehavior p_behavior); + void set_visible_characters(int p_amount); int get_visible_characters() const; int get_total_character_count() const; @@ -155,5 +168,6 @@ public: VARIANT_ENUM_CAST(Label::AutowrapMode); VARIANT_ENUM_CAST(Label::OverrunBehavior); +VARIANT_ENUM_CAST(Label::VisibleCharactersBehavior); #endif diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 810781995e..fa600d24ef 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -215,6 +215,27 @@ void LineEdit::_delete(bool p_word, bool p_all_to_right) { } } +void LineEdit::unhandled_key_input(const Ref<InputEvent> &p_event) { + Ref<InputEventKey> k = p_event; + + if (k.is_valid()) { + if (!k->is_pressed()) { + return; + } + // Handle Unicode (with modifiers active, process after shortcuts). + if (has_focus() && editable && (k->get_unicode() >= 32)) { + 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(); + } + accept_event(); + } + } +} + void LineEdit::gui_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); @@ -385,9 +406,45 @@ void LineEdit::gui_input(const Ref<InputEvent> &p_event) { if (k.is_valid()) { if (!k->is_pressed()) { + if (alt_start && k->get_keycode() == Key::ALT) { + alt_start = false; + if ((alt_code > 0x31 && alt_code < 0xd800) || (alt_code > 0xdfff && alt_code <= 0x10ffff)) { + char32_t ucodestr[2] = { (char32_t)alt_code, 0 }; + insert_text_at_caret(ucodestr); + } + accept_event(); + return; + } return; } + // Alt+ Unicode input: + if (k->is_alt_pressed()) { + if (!alt_start) { + if (k->get_keycode() == Key::KP_ADD) { + alt_start = true; + alt_code = 0; + accept_event(); + return; + } + } else { + if (k->get_keycode() >= Key::KEY_0 && k->get_keycode() <= Key::KEY_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::KEY_0); + } + if (k->get_keycode() >= Key::KP_0 && k->get_keycode() <= Key::KP_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::KP_0); + } + if (k->get_keycode() >= Key::A && k->get_keycode() <= Key::F) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::A) + 10; + } + accept_event(); + return; + } + } + if (context_menu_enabled) { if (k->is_action("ui_menu", true)) { _ensure_menu(); @@ -638,7 +695,7 @@ bool LineEdit::_is_over_clear_button(const Point2 &p_pos) const { return false; } Ref<Texture2D> icon = Control::get_theme_icon(SNAME("clear")); - int x_ofs = get_theme_stylebox(SNAME("normal"))->get_offset().x; + int x_ofs = get_theme_stylebox(SNAME("normal"))->get_margin(SIDE_RIGHT); return p_pos.x > get_size().width - icon->get_width() - x_ofs; } @@ -647,8 +704,8 @@ void LineEdit::_notification(int p_what) { #ifdef TOOLS_ENABLED case NOTIFICATION_ENTER_TREE: { if (Engine::get_singleton()->is_editor_hint() && !get_tree()->is_node_being_edited(this)) { - set_caret_blink_enabled(EDITOR_DEF("text_editor/appearance/caret/caret_blink", false)); - set_caret_blink_speed(EDITOR_DEF("text_editor/appearance/caret/caret_blink_speed", 0.65)); + set_caret_blink_enabled(EDITOR_GET("text_editor/appearance/caret/caret_blink")); + set_caret_blink_speed(EDITOR_GET("text_editor/appearance/caret/caret_blink_speed")); 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)); @@ -656,31 +713,37 @@ void LineEdit::_notification(int p_what) { } } break; #endif + case NOTIFICATION_RESIZED: { _fit_to_width(); scroll_offset = 0; 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 = atr(placeholder); _shape(); update(); } break; + case NOTIFICATION_WM_WINDOW_FOCUS_IN: { window_has_focus = true; draw_caret = true; update(); } break; + case NOTIFICATION_WM_WINDOW_FOCUS_OUT: { window_has_focus = false; draw_caret = false; update(); } break; + case NOTIFICATION_DRAW: { if ((!has_focus() && !(menu && menu->has_focus()) && !caret_force_displayed) || !window_has_focus) { draw_caret = false; @@ -752,7 +815,7 @@ void LineEdit::_notification(int p_what) { // Draw placeholder color. if (using_placeholder) { - font_color.a *= placeholder_alpha; + font_color = get_theme_color(SNAME("font_placeholder_color")); } bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; @@ -780,8 +843,6 @@ void LineEdit::_notification(int p_what) { ofs_max -= r_icon->get_width(); } - int caret_width = Math::round(1 * get_theme_default_base_scale()); - // Draw selections rects. Vector2 ofs = Point2(x_ofs + scroll_offset, y_ofs); if (selection.enabled) { @@ -843,6 +904,9 @@ void LineEdit::_notification(int p_what) { // Draw carets. ofs.x = x_ofs + scroll_offset; if (draw_caret || drag_caret_force_displayed) { + // Prevent carets from disappearing at theme scales below 1.0 (if the caret width is 1). + const int caret_width = get_theme_constant(SNAME("caret_width")) * MAX(1, get_theme_default_base_scale()); + if (ime_text.length() == 0) { // Normal caret. CaretInfo caret = TS->shaped_text_get_carets(text_rid, caret_column); @@ -923,6 +987,7 @@ void LineEdit::_notification(int p_what) { } } } break; + case NOTIFICATION_FOCUS_ENTER: { if (!caret_force_displayed) { if (caret_blink_enabled) { @@ -942,6 +1007,7 @@ void LineEdit::_notification(int p_what) { show_virtual_keyboard(); } break; + case NOTIFICATION_FOCUS_EXIT: { if (caret_blink_enabled && !caret_force_displayed) { caret_blink_timer->stop(); @@ -964,6 +1030,7 @@ void LineEdit::_notification(int p_what) { deselect(); } } break; + case MainLoop::NOTIFICATION_OS_IME_UPDATE: { if (has_focus()) { ime_text = DisplayServer::get_singleton()->ime_get_text(); @@ -974,10 +1041,12 @@ void LineEdit::_notification(int p_what) { update(); } } break; - case Control::NOTIFICATION_DRAG_BEGIN: { + + case NOTIFICATION_DRAG_BEGIN: { drag_action = true; } break; - case Control::NOTIFICATION_DRAG_END: { + + case NOTIFICATION_DRAG_END: { if (is_drag_successful()) { if (selection.drag_attempt) { selection.drag_attempt = false; @@ -1419,7 +1488,7 @@ bool LineEdit::get_draw_control_chars() const { return draw_control_chars; } -void LineEdit::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { +void LineEdit::set_structured_text_bidi_override(TextServer::StructuredTextParser p_parser) { if (st_parser != p_parser) { st_parser = p_parser; _shape(); @@ -1427,7 +1496,7 @@ void LineEdit::set_structured_text_bidi_override(Control::StructuredTextParser p } } -Control::StructuredTextParser LineEdit::get_structured_text_bidi_override() const { +TextServer::StructuredTextParser LineEdit::get_structured_text_bidi_override() const { return st_parser; } @@ -1476,15 +1545,6 @@ String LineEdit::get_placeholder() const { return placeholder; } -void LineEdit::set_placeholder_alpha(float p_alpha) { - placeholder_alpha = p_alpha; - update(); -} - -float LineEdit::get_placeholder_alpha() const { - return placeholder_alpha; -} - void LineEdit::set_caret_column(int p_column) { if (p_column > (int)text.length()) { p_column = text.length(); @@ -1615,7 +1675,7 @@ Size2 LineEdit::get_minimum_size() const { Size2 min_size; // Minimum size of text. - int em_space_size = font->get_char_size('M', 0, font_size).x; + float 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) { @@ -1626,13 +1686,17 @@ Size2 LineEdit::get_minimum_size() const { min_size.height = MAX(TS->shaped_text_get_size(text_rid).y + font->get_spacing(TextServer::SPACING_TOP) + font->get_spacing(TextServer::SPACING_BOTTOM), font->get_height(font_size)); // Take icons into account. - bool using_placeholder = text.is_empty() && ime_text.is_empty(); - bool display_clear_icon = !using_placeholder && is_editable() && clear_button_enabled; - if (right_icon.is_valid() || display_clear_icon) { - Ref<Texture2D> r_icon = display_clear_icon ? Control::get_theme_icon(SNAME("clear")) : right_icon; - min_size.width += r_icon->get_width(); - min_size.height = MAX(min_size.height, r_icon->get_height()); + int icon_max_width = 0; + if (right_icon.is_valid()) { + min_size.height = MAX(min_size.height, right_icon->get_height()); + icon_max_width = right_icon->get_width(); + } + if (clear_button_enabled) { + Ref<Texture2D> clear_icon = Control::get_theme_icon(SNAME("clear")); + min_size.height = MAX(min_size.height, clear_icon->get_height()); + icon_max_width = MAX(icon_max_width, clear_icon->get_width()); } + min_size.width += icon_max_width; return style->get_minimum_size() + min_size; } @@ -1941,8 +2005,8 @@ PopupMenu *LineEdit::get_menu() const { void LineEdit::_editor_settings_changed() { #ifdef TOOLS_ENABLED - 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)); + set_caret_blink_enabled(EDITOR_GET("text_editor/appearance/caret/caret_blink")); + set_caret_blink_speed(EDITOR_GET("text_editor/appearance/caret/caret_blink_speed")); #endif } @@ -2164,7 +2228,7 @@ bool LineEdit::_set(const StringName &p_name, const Variant &p_value) { if (str.begins_with("opentype_features/")) { String name = str.get_slicec('/', 1); int32_t tag = TS->name_to_tag(name); - double value = p_value; + int value = p_value; if (value == -1) { if (opentype_features.has(tag)) { opentype_features.erase(tag); @@ -2172,7 +2236,7 @@ bool LineEdit::_set(const StringName &p_name, const Variant &p_value) { update(); } } else { - if ((double)opentype_features[tag] != value) { + if (!opentype_features.has(tag) || (int)opentype_features[tag] != value) { opentype_features[tag] = value; _shape(); update(); @@ -2204,7 +2268,7 @@ bool LineEdit::_get(const StringName &p_name, Variant &r_ret) const { 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::INT, "opentype_features/" + name)); } p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); } @@ -2245,8 +2309,6 @@ void LineEdit::_bind_methods() { 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_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); @@ -2328,6 +2390,7 @@ void LineEdit::_bind_methods() { BIND_ENUM_CONSTANT(MENU_MAX); ADD_PROPERTY(PropertyInfo(Variant::STRING, "text"), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text"), "set_placeholder", "get_placeholder"); ADD_PROPERTY(PropertyInfo(Variant::INT, "alignment", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_horizontal_alignment", "get_horizontal_alignment"); 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"); @@ -2344,14 +2407,11 @@ void LineEdit::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "right_icon", PROPERTY_HINT_RESOURCE_TYPE, "Texture"), "set_right_icon", "get_right_icon"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flat"), "set_flat", "is_flat"); 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::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "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"), "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"); @@ -2438,7 +2498,7 @@ void LineEdit::_ensure_menu() { } } -LineEdit::LineEdit() { +LineEdit::LineEdit(const String &p_placeholder) { text_rid = TS->create_shaped_text(); _create_undo_state(); @@ -2446,6 +2506,7 @@ LineEdit::LineEdit() { set_focus_mode(FOCUS_ALL); set_default_cursor_shape(CURSOR_IBEAM); set_mouse_filter(MOUSE_FILTER_STOP); + set_process_unhandled_key_input(true); caret_blink_timer = memnew(Timer); add_child(caret_blink_timer, false, INTERNAL_MODE_FRONT); @@ -2453,9 +2514,11 @@ LineEdit::LineEdit() { caret_blink_timer->connect("timeout", callable_mp(this, &LineEdit::_toggle_draw_caret)); set_caret_blink_enabled(false); + set_placeholder(p_placeholder); + set_editable(true); // Initialise to opposite first, so we get past the early-out in set_editable. } LineEdit::~LineEdit() { - TS->free(text_rid); + TS->free_rid(text_rid); } diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index 854e54e8f1..0fb178fca4 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -77,12 +77,14 @@ private: bool pass = false; bool text_changed_dirty = false; + bool alt_start = false; + uint32_t alt_code = 0; + String undo_text; String text; String placeholder; String placeholder_translated; String secret_character = "*"; - float placeholder_alpha = 0.6; String ime_text; Point2 ime_selection; @@ -97,7 +99,7 @@ private: PopupMenu *menu_dir = nullptr; PopupMenu *menu_ctl = nullptr; - bool caret_mid_grapheme_enabled = false; + bool caret_mid_grapheme_enabled = true; int caret_column = 0; int scroll_offset = 0; @@ -107,7 +109,7 @@ private: String language; TextDirection text_direction = TEXT_DIRECTION_AUTO; TextDirection input_direction = TEXT_DIRECTION_LTR; - Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + TextServer::StructuredTextParser st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; Array st_args; bool draw_control_chars = false; @@ -203,6 +205,7 @@ private: protected: void _notification(int p_what); static void _bind_methods(); + virtual void unhandled_key_input(const Ref<InputEvent> &p_event) override; virtual void gui_input(const Ref<InputEvent> &p_event) override; bool _set(const StringName &p_name, const Variant &p_value); @@ -253,8 +256,8 @@ public: 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(TextServer::StructuredTextParser p_parser); + TextServer::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; @@ -262,9 +265,6 @@ public: void set_placeholder(String p_text); String get_placeholder() const; - void set_placeholder_alpha(float p_alpha); - float get_placeholder_alpha() const; - void set_caret_column(int p_column); int get_caret_column() const; @@ -336,7 +336,7 @@ public: void show_virtual_keyboard(); - LineEdit(); + LineEdit(const String &p_placeholder = String()); ~LineEdit(); }; diff --git a/scene/gui/link_button.cpp b/scene/gui/link_button.cpp index b59f4fb7d4..dca6437519 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +29,7 @@ /*************************************************************************/ #include "link_button.h" + #include "core/string/translation.h" void LinkButton::_shape() { @@ -60,7 +61,7 @@ String LinkButton::get_text() const { return text; } -void LinkButton::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { +void LinkButton::set_structured_text_bidi_override(TextServer::StructuredTextParser p_parser) { if (st_parser != p_parser) { st_parser = p_parser; _shape(); @@ -68,7 +69,7 @@ void LinkButton::set_structured_text_bidi_override(Control::StructuredTextParser } } -Control::StructuredTextParser LinkButton::get_structured_text_bidi_override() const { +TextServer::StructuredTextParser LinkButton::get_structured_text_bidi_override() const { return st_parser; } @@ -148,18 +149,20 @@ void LinkButton::_notification(int p_what) { case NOTIFICATION_TRANSLATION_CHANGED: { xl_text = atr(text); _shape(); - update_minimum_size(); update(); } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { update(); } break; + case NOTIFICATION_THEME_CHANGED: { _shape(); update_minimum_size(); update(); } break; + case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); Size2 size = get_size(); @@ -230,7 +233,6 @@ void LinkButton::_notification(int p_what) { draw_line(Vector2(0, y), Vector2(width, y), color, text_buf->get_line_underline_thickness()); } } - } break; } } @@ -240,7 +242,7 @@ bool LinkButton::_set(const StringName &p_name, const Variant &p_value) { if (str.begins_with("opentype_features/")) { String name = str.get_slicec('/', 1); int32_t tag = TS->name_to_tag(name); - double value = p_value; + int value = p_value; if (value == -1) { if (opentype_features.has(tag)) { opentype_features.erase(tag); @@ -248,7 +250,7 @@ bool LinkButton::_set(const StringName &p_name, const Variant &p_value) { update(); } } else { - if ((double)opentype_features[tag] != value) { + if (!opentype_features.has(tag) || (int)opentype_features[tag] != value) { opentype_features[tag] = value; _shape(); update(); @@ -280,7 +282,7 @@ bool LinkButton::_get(const StringName &p_name, Variant &r_ret) const { 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::INT, "opentype_features/" + name)); } p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); } @@ -308,15 +310,17 @@ void LinkButton::_bind_methods() { 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::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "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() { +LinkButton::LinkButton(const String &p_text) { text_buf.instantiate(); set_focus_mode(FOCUS_NONE); set_default_cursor_shape(CURSOR_POINTING_HAND); + + set_text(p_text); } diff --git a/scene/gui/link_button.h b/scene/gui/link_button.h index 231543c63c..6d2dcbde84 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 @@ #define LINKBUTTON_H #include "scene/gui/base_button.h" -#include "scene/resources/bit_map.h" #include "scene/resources/text_line.h" class LinkButton : public BaseButton { @@ -54,7 +53,7 @@ private: Dictionary opentype_features; String language; TextDirection text_direction = TEXT_DIRECTION_AUTO; - Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + TextServer::StructuredTextParser st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; Array st_args; void _shape(); @@ -72,8 +71,8 @@ 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(TextServer::StructuredTextParser p_parser); + TextServer::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; @@ -91,7 +90,7 @@ public: void set_underline_mode(UnderlineMode p_underline_mode); UnderlineMode get_underline_mode() const; - LinkButton(); + LinkButton(const String &p_text = String()); }; VARIANT_ENUM_CAST(LinkButton::UnderlineMode); diff --git a/scene/gui/margin_container.cpp b/scene/gui/margin_container.cpp index af239d67ae..fac37a8634 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -65,6 +65,24 @@ Size2 MarginContainer::get_minimum_size() const { return max; } +Vector<int> MarginContainer::get_allowed_size_flags_horizontal() const { + Vector<int> flags; + flags.append(SIZE_FILL); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + +Vector<int> MarginContainer::get_allowed_size_flags_vertical() const { + Vector<int> flags; + flags.append(SIZE_FILL); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + void MarginContainer::_notification(int p_what) { switch (p_what) { case NOTIFICATION_SORT_CHILDREN: { @@ -89,6 +107,7 @@ void MarginContainer::_notification(int p_what) { fit_child_in_rect(c, Rect2(margin_left, margin_top, w, h)); } } break; + case NOTIFICATION_THEME_CHANGED: { update_minimum_size(); } break; diff --git a/scene/gui/margin_container.h b/scene/gui/margin_container.h index b782976ada..f8a3c5bb11 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +42,9 @@ protected: public: virtual Size2 get_minimum_size() const override; + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + MarginContainer(); }; diff --git a/scene/gui/menu_button.cpp b/scene/gui/menu_button.cpp index 32501b65a0..316fee53fe 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 @@ #include "core/os/keyboard.h" #include "scene/main/window.h" -void MenuButton::unhandled_key_input(const Ref<InputEvent> &p_event) { +void MenuButton::shortcut_input(const Ref<InputEvent> &p_event) { ERR_FAIL_COND(p_event.is_null()); - if (!_is_focus_owner_in_shorcut_context()) { + if (!_is_focus_owner_in_shortcut_context()) { return; } @@ -98,7 +98,13 @@ void MenuButton::pressed() { popup->set_position(gp); popup->set_parent_rect(Rect2(Point2(gp - popup->get_position()), size)); - popup->take_mouse_focus(); + // If not triggered by the mouse, start the popup with its first item selected. + if (popup->get_item_count() > 0 && + ((get_action_mode() == ActionMode::ACTION_MODE_BUTTON_PRESS && Input::get_singleton()->is_action_just_pressed("ui_accept")) || + (get_action_mode() == ActionMode::ACTION_MODE_BUTTON_RELEASE && Input::get_singleton()->is_action_just_released("ui_accept")))) { + popup->set_current_index(0); + } + popup->popup(); } @@ -130,11 +136,16 @@ int MenuButton::get_item_count() const { void MenuButton::_notification(int p_what) { switch (p_what) { + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + popup->set_layout_direction((Window::LayoutDirection)get_layout_direction()); + } break; + 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)); @@ -176,7 +187,7 @@ void MenuButton::_get_property_list(List<PropertyInfo> *p_list) const { pi.usage &= ~(popup->get_item_icon(i).is_null() ? PROPERTY_USAGE_STORAGE : 0); p_list->push_back(pi); - pi = PropertyInfo(Variant::INT, vformat("popup/item_%d/checkable", i), PROPERTY_HINT_ENUM, "No,As checkbox,As radio button"); + pi = PropertyInfo(Variant::INT, vformat("popup/item_%d/checkable", i), PROPERTY_HINT_ENUM, "No,As Checkbox,As Radio Button"); pi.usage &= ~(!popup->is_item_checkable(i) ? PROPERTY_USAGE_STORAGE : 0); p_list->push_back(pi); @@ -184,7 +195,7 @@ void MenuButton::_get_property_list(List<PropertyInfo> *p_list) const { pi.usage &= ~(!popup->is_item_checked(i) ? PROPERTY_USAGE_STORAGE : 0); p_list->push_back(pi); - pi = PropertyInfo(Variant::INT, vformat("popup/item_%d/id", i), PROPERTY_HINT_RANGE, "1,10,1,or_greater"); + pi = PropertyInfo(Variant::INT, vformat("popup/item_%d/id", i), PROPERTY_HINT_RANGE, "0,10,1,or_greater"); p_list->push_back(pi); pi = PropertyInfo(Variant::BOOL, vformat("popup/item_%d/disabled", i)); @@ -216,11 +227,12 @@ void MenuButton::set_disable_shortcuts(bool p_disabled) { disable_shortcuts = p_disabled; } -MenuButton::MenuButton() { +MenuButton::MenuButton(const String &p_text) : + Button(p_text) { set_flat(true); set_toggle_mode(true); set_disable_shortcuts(false); - set_process_unhandled_key_input(true); + set_process_shortcut_input(true); set_focus_mode(FOCUS_NONE); set_action_mode(ACTION_MODE_BUTTON_PRESS); diff --git a/scene/gui/menu_button.h b/scene/gui/menu_button.h index 455b7dc870..0a6b46c796 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 @@ class MenuButton : public Button { bool clicked = false; bool switch_on_hover = false; bool disable_shortcuts = false; - PopupMenu *popup; + PopupMenu *popup = nullptr; Vector2i mouse_pos_adjusted; @@ -54,7 +54,7 @@ protected: bool _get(const StringName &p_name, Variant &r_ret) const; void _get_property_list(List<PropertyInfo> *p_list) const; static void _bind_methods(); - virtual void unhandled_key_input(const Ref<InputEvent> &p_event) override; + virtual void shortcut_input(const Ref<InputEvent> &p_event) override; public: virtual void pressed() override; @@ -67,7 +67,7 @@ public: void set_item_count(int p_count); int get_item_count() const; - MenuButton(); + MenuButton(const String &p_text = String()); ~MenuButton(); }; diff --git a/scene/gui/nine_patch_rect.cpp b/scene/gui/nine_patch_rect.cpp index ea5c82306d..4f34ece86f 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,18 +34,20 @@ #include "servers/rendering_server.h" void NinePatchRect::_notification(int p_what) { - if (p_what == NOTIFICATION_DRAW) { - if (texture.is_null()) { - return; - } + switch (p_what) { + case NOTIFICATION_DRAW: { + if (texture.is_null()) { + return; + } - Rect2 rect = Rect2(Point2(), get_size()); - Rect2 src_rect = region_rect; + Rect2 rect = Rect2(Point2(), get_size()); + Rect2 src_rect = region_rect; - texture->get_rect_region(rect, src_rect, rect, src_rect); + 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[SIDE_LEFT], margin[SIDE_TOP]), Vector2(margin[SIDE_RIGHT], margin[SIDE_BOTTOM]), RS::NinePatchAxisMode(axis_h), RS::NinePatchAxisMode(axis_v), draw_center); + RID ci = get_canvas_item(); + 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); + } break; } } @@ -93,10 +95,6 @@ void NinePatchRect::set_texture(const Ref<Texture2D> &p_tex) { } texture = p_tex; update(); - /* - if (texture.is_valid()) - texture->set_flags(texture->get_flags()&(~Texture::FLAG_REPEAT)); //remove repeat from texture, it looks bad in sprites - */ update_minimum_size(); emit_signal(SceneStringNames::get_singleton()->texture_changed); } diff --git a/scene/gui/nine_patch_rect.h b/scene/gui/nine_patch_rect.h index f9a3f31fe5..23aebbb782 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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/option_button.cpp b/scene/gui/option_button.cpp index cbbda09261..a86f2bdbc1 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,8 @@ #include "core/string/print_string.h" +static const int NONE_SELECTED = -1; + Size2 OptionButton::get_minimum_size() const { Size2 minsize = Button::get_minimum_size(); @@ -40,7 +42,7 @@ Size2 OptionButton::get_minimum_size() const { 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(SNAME("hseparation")); + content_size.width += arrow_size.width + get_theme_constant(SNAME("h_separation")); content_size.height = MAX(content_size.height, arrow_size.height); minsize = content_size + padding; @@ -89,8 +91,12 @@ void OptionButton::_notification(int p_what) { } arrow->draw(ci, ofs, clr); } break; + case NOTIFICATION_TRANSLATION_CHANGED: - case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + popup->set_layout_direction((Window::LayoutDirection)get_layout_direction()); + [[fallthrough]]; + } case NOTIFICATION_THEME_CHANGED: { if (has_theme_icon(SNAME("arrow"))) { if (is_layout_rtl()) { @@ -102,6 +108,7 @@ void OptionButton::_notification(int p_what) { } } } break; + case NOTIFICATION_VISIBILITY_CHANGED: { if (!is_visible_in_tree()) { popup->hide(); @@ -110,6 +117,65 @@ void OptionButton::_notification(int p_what) { } } +bool OptionButton::_set(const StringName &p_name, const Variant &p_value) { + Vector<String> components = String(p_name).split("/", true, 2); + if (components.size() >= 2 && components[0] == "popup") { + String property = components[2]; + if (property != "text" && property != "icon" && property != "id" && property != "disabled" && property != "separator") { + return false; + } + + bool valid; + popup->set(String(p_name).trim_prefix("popup/"), p_value, &valid); + + int idx = components[1].get_slice("_", 1).to_int(); + if (idx == current) { + // Force refreshing currently displayed item. + current = NONE_SELECTED; + _select(idx, false); + } + + return valid; + } + return false; +} + +bool OptionButton::_get(const StringName &p_name, Variant &r_ret) const { + Vector<String> components = String(p_name).split("/", true, 2); + if (components.size() >= 2 && components[0] == "popup") { + String property = components[2]; + if (property != "text" && property != "icon" && property != "id" && property != "disabled" && property != "separator") { + return false; + } + + bool valid; + r_ret = popup->get(String(p_name).trim_prefix("popup/"), &valid); + return valid; + } + return false; +} + +void OptionButton::_get_property_list(List<PropertyInfo> *p_list) const { + for (int i = 0; i < popup->get_item_count(); i++) { + p_list->push_back(PropertyInfo(Variant::STRING, vformat("popup/item_%d/text", i))); + + PropertyInfo pi = PropertyInfo(Variant::OBJECT, vformat("popup/item_%d/icon", i), PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"); + pi.usage &= ~(popup->get_item_icon(i).is_null() ? PROPERTY_USAGE_STORAGE : 0); + p_list->push_back(pi); + + pi = PropertyInfo(Variant::INT, vformat("popup/item_%d/id", i), PROPERTY_HINT_RANGE, "0,10,1,or_greater"); + p_list->push_back(pi); + + pi = PropertyInfo(Variant::BOOL, vformat("popup/item_%d/disabled", i)); + pi.usage &= ~(!popup->is_item_disabled(i) ? PROPERTY_USAGE_STORAGE : 0); + p_list->push_back(pi); + + pi = PropertyInfo(Variant::BOOL, vformat("popup/item_%d/separator", i)); + pi.usage &= ~(!popup->is_item_separator(i) ? PROPERTY_USAGE_STORAGE : 0); + p_list->push_back(pi); + } +} + void OptionButton::_focused(int p_which) { emit_signal(SNAME("item_focused"), p_which); } @@ -122,20 +188,33 @@ void OptionButton::pressed() { Size2 size = get_size() * get_viewport()->get_canvas_transform().get_scale(); popup->set_position(get_screen_position() + Size2(0, size.height * get_global_transform().get_scale().y)); popup->set_size(Size2(size.width, 0)); + + // If not triggered by the mouse, start the popup with the checked item selected. + if (popup->get_item_count() > 0) { + if ((get_action_mode() == ActionMode::ACTION_MODE_BUTTON_PRESS && Input::get_singleton()->is_action_just_pressed("ui_accept")) || + (get_action_mode() == ActionMode::ACTION_MODE_BUTTON_RELEASE && Input::get_singleton()->is_action_just_released("ui_accept"))) { + popup->set_current_index(current > -1 ? current : 0); + } else { + popup->scroll_to_item(current > -1 ? current : 0); + } + } + popup->popup(); } void OptionButton::add_icon_item(const Ref<Texture2D> &p_icon, const String &p_label, int p_id) { + bool first_selectable = !has_selectable_items(); popup->add_icon_radio_check_item(p_icon, p_label, p_id); - if (popup->get_item_count() == 1) { - select(0); + if (first_selectable) { + select(get_item_count() - 1); } } void OptionButton::add_item(const String &p_label, int p_id) { + bool first_selectable = !has_selectable_items(); popup->add_radio_check_item(p_label, p_id); - if (popup->get_item_count() == 1) { - select(0); + if (first_selectable) { + select(get_item_count() - 1); } } @@ -163,6 +242,10 @@ void OptionButton::set_item_metadata(int p_idx, const Variant &p_metadata) { popup->set_item_metadata(p_idx, p_metadata); } +void OptionButton::set_item_tooltip(int p_idx, const String &p_tooltip) { + popup->set_item_tooltip(p_idx, p_tooltip); +} + void OptionButton::set_item_disabled(int p_idx, bool p_disabled) { popup->set_item_disabled(p_idx, p_disabled); } @@ -176,6 +259,10 @@ Ref<Texture2D> OptionButton::get_item_icon(int p_idx) const { } int OptionButton::get_item_id(int p_idx) const { + if (p_idx == NONE_SELECTED) { + return NONE_SELECTED; + } + return popup->get_item_id(p_idx); } @@ -187,41 +274,99 @@ Variant OptionButton::get_item_metadata(int p_idx) const { return popup->get_item_metadata(p_idx); } +String OptionButton::get_item_tooltip(int p_idx) const { + return popup->get_item_tooltip(p_idx); +} + bool OptionButton::is_item_disabled(int p_idx) const { return popup->is_item_disabled(p_idx); } +bool OptionButton::is_item_separator(int p_idx) const { + return popup->is_item_separator(p_idx); +} +void OptionButton::set_item_count(int p_count) { + ERR_FAIL_COND(p_count < 0); + + int count_old = get_item_count(); + if (p_count == count_old) { + return; + } + + popup->set_item_count(p_count); + + if (p_count > count_old) { + for (int i = count_old; i < p_count; i++) { + popup->set_item_as_radio_checkable(i, true); + } + } + + notify_property_list_changed(); +} + +bool OptionButton::has_selectable_items() const { + for (int i = 0; i < get_item_count(); i++) { + if (!is_item_disabled(i) && !is_item_separator(i)) { + return true; + } + } + return false; +} +int OptionButton::get_selectable_item(bool p_from_last) const { + if (!p_from_last) { + for (int i = 0; i < get_item_count(); i++) { + if (!is_item_disabled(i) && !is_item_separator(i)) { + return i; + } + } + } else { + for (int i = get_item_count() - 1; i >= 0; i--) { + if (!is_item_disabled(i) && !is_item_separator(i)) { + return i; + } + } + } + return -1; +} + int OptionButton::get_item_count() const { return popup->get_item_count(); } -void OptionButton::add_separator() { - popup->add_separator(); +void OptionButton::add_separator(const String &p_text) { + popup->add_separator(p_text); } void OptionButton::clear() { popup->clear(); set_text(""); - current = -1; + current = NONE_SELECTED; } void OptionButton::_select(int p_which, bool p_emit) { - if (p_which < 0) { - return; - } if (p_which == current) { return; } - ERR_FAIL_INDEX(p_which, popup->get_item_count()); + if (p_which == NONE_SELECTED) { + for (int i = 0; i < popup->get_item_count(); i++) { + popup->set_item_checked(i, false); + } - for (int i = 0; i < popup->get_item_count(); i++) { - popup->set_item_checked(i, i == p_which); - } + current = NONE_SELECTED; + set_text(""); + set_icon(nullptr); + } else { + ERR_FAIL_INDEX(p_which, popup->get_item_count()); - current = p_which; - set_text(popup->get_item_text(current)); - set_icon(popup->get_item_icon(current)); + for (int i = 0; i < popup->get_item_count(); i++) { + popup->set_item_checked(i, i == p_which); + } + + current = p_which; + set_text(popup->get_item_text(current)); + set_icon(popup->get_item_icon(current)); + } if (is_inside_tree() && p_emit) { emit_signal(SNAME("item_selected"), current); @@ -229,7 +374,7 @@ void OptionButton::_select(int p_which, bool p_emit) { } void OptionButton::_select_int(int p_which) { - if (p_which < 0 || p_which >= popup->get_item_count()) { + if (p_which < NONE_SELECTED || p_which >= popup->get_item_count()) { return; } _select(p_which, false); @@ -244,10 +389,6 @@ int OptionButton::get_selected() const { } int OptionButton::get_selected_id() const { - int idx = get_selected(); - if (idx < 0) { - return 0; - } return get_item_id(current); } @@ -261,48 +402,25 @@ Variant OptionButton::get_selected_metadata() const { void OptionButton::remove_item(int p_idx) { popup->remove_item(p_idx); + if (current == p_idx) { + _select(NONE_SELECTED); + } } PopupMenu *OptionButton::get_popup() const { return popup; } -Array OptionButton::_get_items() const { - Array items; - for (int i = 0; i < get_item_count(); i++) { - items.push_back(get_item_text(i)); - items.push_back(get_item_icon(i)); - items.push_back(is_item_disabled(i)); - items.push_back(get_item_id(i)); - items.push_back(get_item_metadata(i)); - } - - return items; +void OptionButton::get_translatable_strings(List<String> *p_strings) const { + popup->get_translatable_strings(p_strings); } -void OptionButton::_set_items(const Array &p_items) { - ERR_FAIL_COND(p_items.size() % 5); - clear(); - - for (int i = 0; i < p_items.size(); i += 5) { - String text = p_items[i + 0]; - Ref<Texture2D> icon = p_items[i + 1]; - bool disabled = p_items[i + 2]; - int id = p_items[i + 3]; - Variant meta = p_items[i + 4]; - - int idx = get_item_count(); - add_item(text, id); - set_item_icon(idx, icon); - set_item_disabled(idx, disabled); - set_item_metadata(idx, meta); +void OptionButton::_validate_property(PropertyInfo &property) const { + if (property.name == "text" || property.name == "icon") { + property.usage = PROPERTY_USAGE_NONE; } } -void OptionButton::get_translatable_strings(List<String> *p_strings) const { - popup->get_translatable_strings(p_strings); -} - void OptionButton::_bind_methods() { ClassDB::bind_method(D_METHOD("add_item", "label", "id"), &OptionButton::add_item, DEFVAL(-1)); ClassDB::bind_method(D_METHOD("add_icon_item", "texture", "label", "id"), &OptionButton::add_icon_item, DEFVAL(-1)); @@ -311,14 +429,16 @@ void OptionButton::_bind_methods() { ClassDB::bind_method(D_METHOD("set_item_disabled", "idx", "disabled"), &OptionButton::set_item_disabled); ClassDB::bind_method(D_METHOD("set_item_id", "idx", "id"), &OptionButton::set_item_id); ClassDB::bind_method(D_METHOD("set_item_metadata", "idx", "metadata"), &OptionButton::set_item_metadata); + ClassDB::bind_method(D_METHOD("set_item_tooltip", "idx", "tooltip"), &OptionButton::set_item_tooltip); ClassDB::bind_method(D_METHOD("get_item_text", "idx"), &OptionButton::get_item_text); ClassDB::bind_method(D_METHOD("get_item_icon", "idx"), &OptionButton::get_item_icon); ClassDB::bind_method(D_METHOD("get_item_id", "idx"), &OptionButton::get_item_id); ClassDB::bind_method(D_METHOD("get_item_index", "id"), &OptionButton::get_item_index); ClassDB::bind_method(D_METHOD("get_item_metadata", "idx"), &OptionButton::get_item_metadata); + ClassDB::bind_method(D_METHOD("get_item_tooltip", "idx"), &OptionButton::get_item_tooltip); ClassDB::bind_method(D_METHOD("is_item_disabled", "idx"), &OptionButton::is_item_disabled); - ClassDB::bind_method(D_METHOD("get_item_count"), &OptionButton::get_item_count); - ClassDB::bind_method(D_METHOD("add_separator"), &OptionButton::add_separator); + ClassDB::bind_method(D_METHOD("is_item_separator", "idx"), &OptionButton::is_item_separator); + ClassDB::bind_method(D_METHOD("add_separator", "text"), &OptionButton::add_separator, DEFVAL(String())); ClassDB::bind_method(D_METHOD("clear"), &OptionButton::clear); ClassDB::bind_method(D_METHOD("select", "idx"), &OptionButton::select); ClassDB::bind_method(D_METHOD("get_selected"), &OptionButton::get_selected); @@ -329,17 +449,20 @@ void OptionButton::_bind_methods() { ClassDB::bind_method(D_METHOD("get_popup"), &OptionButton::get_popup); - ClassDB::bind_method(D_METHOD("_set_items"), &OptionButton::_set_items); - ClassDB::bind_method(D_METHOD("_get_items"), &OptionButton::_get_items); + ClassDB::bind_method(D_METHOD("set_item_count", "count"), &OptionButton::set_item_count); + ClassDB::bind_method(D_METHOD("get_item_count"), &OptionButton::get_item_count); + ClassDB::bind_method(D_METHOD("has_selectable_items"), &OptionButton::has_selectable_items); + ClassDB::bind_method(D_METHOD("get_selectable_item", "from_last"), &OptionButton::get_selectable_item, DEFVAL(false)); - ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "items", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_items", "_get_items"); - // "selected" property must come after "items", otherwise GH-10213 occurs. + // "selected" property must come after "item_count", otherwise GH-10213 occurs. + ADD_ARRAY_COUNT("Items", "item_count", "set_item_count", "get_item_count", "popup/item_"); ADD_PROPERTY(PropertyInfo(Variant::INT, "selected"), "_select_int", "get_selected"); ADD_SIGNAL(MethodInfo("item_selected", PropertyInfo(Variant::INT, "index"))); ADD_SIGNAL(MethodInfo("item_focused", PropertyInfo(Variant::INT, "index"))); } -OptionButton::OptionButton() { +OptionButton::OptionButton(const String &p_text) : + Button(p_text) { set_toggle_mode(true); set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT); if (is_layout_rtl()) { diff --git a/scene/gui/option_button.h b/scene/gui/option_button.h index 953337ecce..7896132626 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,7 +37,7 @@ class OptionButton : public Button { GDCLASS(OptionButton, Button); - PopupMenu *popup; + PopupMenu *popup = nullptr; int current = -1; void _focused(int p_which); @@ -45,14 +45,15 @@ class OptionButton : public Button { void _select(int p_which, bool p_emit = false); void _select_int(int p_which); - Array _get_items() const; - void _set_items(const Array &p_items); - virtual void pressed() override; protected: Size2 get_minimum_size() const override; void _notification(int p_what); + 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; + virtual void _validate_property(PropertyInfo &property) const override; static void _bind_methods(); public: @@ -68,6 +69,7 @@ public: void set_item_id(int p_idx, int p_id); void set_item_metadata(int p_idx, const Variant &p_metadata); void set_item_disabled(int p_idx, bool p_disabled); + void set_item_tooltip(int p_idx, const String &p_tooltip); String get_item_text(int p_idx) const; Ref<Texture2D> get_item_icon(int p_idx) const; @@ -75,10 +77,16 @@ public: int get_item_index(int p_id) const; Variant get_item_metadata(int p_idx) const; bool is_item_disabled(int p_idx) const; + bool is_item_separator(int p_idx) const; + String get_item_tooltip(int p_idx) const; + + bool has_selectable_items() const; + int get_selectable_item(bool p_from_last = false) const; + void set_item_count(int p_count); int get_item_count() const; - void add_separator(); + void add_separator(const String &p_text = ""); void clear(); @@ -93,7 +101,7 @@ public: virtual void get_translatable_strings(List<String> *p_strings) const override; - OptionButton(); + OptionButton(const String &p_text = String()); ~OptionButton(); }; diff --git a/scene/gui/panel.cpp b/scene/gui/panel.cpp index e8e7e3d997..1ac6cf57ab 100644 --- a/scene/gui/panel.cpp +++ b/scene/gui/panel.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,35 +30,16 @@ #include "panel.h" -#include "core/string/print_string.h" - 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(SNAME("panel")) : get_theme_stylebox(SNAME("panel_fg")); - style->draw(ci, Rect2(Point2(), get_size())); + switch (p_what) { + case NOTIFICATION_DRAW: { + RID ci = get_canvas_item(); + Ref<StyleBox> style = get_theme_stylebox(SNAME("panel")); + style->draw(ci, Rect2(Point2(), get_size())); + } break; } } -void Panel::set_mode(Mode p_mode) { - mode = p_mode; - update(); -} - -Panel::Mode Panel::get_mode() const { - return mode; -} - -void Panel::_bind_methods() { - ClassDB::bind_method(D_METHOD("set_mode", "mode"), &Panel::set_mode); - ClassDB::bind_method(D_METHOD("get_mode"), &Panel::get_mode); - - ADD_PROPERTY(PropertyInfo(Variant::INT, "mode", PROPERTY_HINT_ENUM, "Background,Foreground"), "set_mode", "get_mode"); - - BIND_ENUM_CONSTANT(MODE_BACKGROUND); - BIND_ENUM_CONSTANT(MODE_FOREGROUND); -} - Panel::Panel() { // Has visible stylebox, so stop by default. set_mouse_filter(MOUSE_FILTER_STOP); diff --git a/scene/gui/panel.h b/scene/gui/panel.h index 84fd6aaead..5d2e912680 100644 --- a/scene/gui/panel.h +++ b/scene/gui/panel.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,26 +36,11 @@ class Panel : public Control { GDCLASS(Panel, Control); -public: - enum Mode { - MODE_BACKGROUND, - MODE_FOREGROUND, - }; - -private: - Mode mode = MODE_BACKGROUND; - protected: void _notification(int p_what); - static void _bind_methods(); public: - void set_mode(Mode p_mode); - Mode get_mode() const; - Panel(); }; -VARIANT_ENUM_CAST(Panel::Mode) - #endif // PANEL_H diff --git a/scene/gui/panel_container.cpp b/scene/gui/panel_container.cpp index d910e1e882..fe01712a89 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,47 +60,67 @@ Size2 PanelContainer::get_minimum_size() const { return ms; } -void PanelContainer::_notification(int p_what) { - if (p_what == NOTIFICATION_DRAW) { - RID ci = get_canvas_item(); - Ref<StyleBox> style; - - if (has_theme_stylebox(SNAME("panel"))) { - style = get_theme_stylebox(SNAME("panel")); - } else { - style = get_theme_stylebox(SNAME("panel"), SNAME("PanelContainer")); - } +Vector<int> PanelContainer::get_allowed_size_flags_horizontal() const { + Vector<int> flags; + flags.append(SIZE_FILL); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} - style->draw(ci, Rect2(Point2(), get_size())); - } +Vector<int> PanelContainer::get_allowed_size_flags_vertical() const { + Vector<int> flags; + flags.append(SIZE_FILL); + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} - if (p_what == NOTIFICATION_SORT_CHILDREN) { - Ref<StyleBox> style; +void PanelContainer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_DRAW: { + RID ci = get_canvas_item(); + Ref<StyleBox> style; - if (has_theme_stylebox(SNAME("panel"))) { - style = get_theme_stylebox(SNAME("panel")); - } else { - style = get_theme_stylebox(SNAME("panel"), SNAME("PanelContainer")); - } + if (has_theme_stylebox(SNAME("panel"))) { + style = get_theme_stylebox(SNAME("panel")); + } else { + style = get_theme_stylebox(SNAME("panel"), SNAME("PanelContainer")); + } - Size2 size = get_size(); - Point2 ofs; - if (style.is_valid()) { - size -= style->get_minimum_size(); - ofs += style->get_offset(); - } + style->draw(ci, Rect2(Point2(), get_size())); + } break; - 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; + case NOTIFICATION_SORT_CHILDREN: { + Ref<StyleBox> style; + + if (has_theme_stylebox(SNAME("panel"))) { + style = get_theme_stylebox(SNAME("panel")); + } else { + style = get_theme_stylebox(SNAME("panel"), SNAME("PanelContainer")); } - if (c->is_set_as_top_level()) { - continue; + + Size2 size = get_size(); + Point2 ofs; + if (style.is_valid()) { + size -= style->get_minimum_size(); + ofs += style->get_offset(); } - fit_child_in_rect(c, Rect2(ofs, size)); - } + 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; + } + + fit_child_in_rect(c, Rect2(ofs, size)); + } + } break; } } diff --git a/scene/gui/panel_container.h b/scene/gui/panel_container.h index f27ca7fad7..8f07ce38eb 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +42,9 @@ protected: public: virtual Size2 get_minimum_size() const override; + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + PanelContainer(); }; diff --git a/scene/gui/popup.cpp b/scene/gui/popup.cpp index a48ad0f770..6532fc5934 100644 --- a/scene/gui/popup.cpp +++ b/scene/gui/popup.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,26 +42,30 @@ void Popup::_input_from_window(const Ref<InputEvent> &p_event) { } void Popup::_initialize_visible_parents() { - visible_parents.clear(); - - Window *parent_window = this; - while (parent_window) { - parent_window = parent_window->get_parent_visible_window(); - if (parent_window) { - visible_parents.push_back(parent_window); - parent_window->connect("focus_entered", callable_mp(this, &Popup::_parent_focused)); - parent_window->connect("tree_exited", callable_mp(this, &Popup::_deinitialize_visible_parents)); + if (is_embedded()) { + visible_parents.clear(); + + Window *parent_window = this; + while (parent_window) { + parent_window = parent_window->get_parent_visible_window(); + if (parent_window) { + visible_parents.push_back(parent_window); + parent_window->connect("focus_entered", callable_mp(this, &Popup::_parent_focused)); + parent_window->connect("tree_exited", callable_mp(this, &Popup::_deinitialize_visible_parents)); + } } } } void Popup::_deinitialize_visible_parents() { - for (uint32_t i = 0; i < visible_parents.size(); ++i) { - visible_parents[i]->disconnect("focus_entered", callable_mp(this, &Popup::_parent_focused)); - visible_parents[i]->disconnect("tree_exited", callable_mp(this, &Popup::_deinitialize_visible_parents)); - } + if (is_embedded()) { + for (uint32_t i = 0; i < visible_parents.size(); ++i) { + visible_parents[i]->disconnect("focus_entered", callable_mp(this, &Popup::_parent_focused)); + visible_parents[i]->disconnect("tree_exited", callable_mp(this, &Popup::_deinitialize_visible_parents)); + } - visible_parents.clear(); + visible_parents.clear(); + } } void Popup::_notification(int p_what) { @@ -74,19 +78,19 @@ void Popup::_notification(int p_what) { emit_signal(SNAME("popup_hide")); popped_up = false; } - } break; + case NOTIFICATION_WM_WINDOW_FOCUS_IN: { if (has_focus()) { popped_up = true; } } break; + case NOTIFICATION_EXIT_TREE: { _deinitialize_visible_parents(); } break; - case NOTIFICATION_WM_CLOSE_REQUEST: { - _close_pressed(); - } break; + + case NOTIFICATION_WM_CLOSE_REQUEST: case NOTIFICATION_APPLICATION_FOCUS_OUT: { _close_pressed(); } break; @@ -94,7 +98,7 @@ void Popup::_notification(int p_what) { } void Popup::_parent_focused() { - if (popped_up && close_on_parent_focus) { + if (popped_up && get_flag(FLAG_POPUP)) { _close_pressed(); } } @@ -104,27 +108,25 @@ void Popup::_close_pressed() { _deinitialize_visible_parents(); - call_deferred(SNAME("hide")); + // Hide after returning to process events, but only if we don't + // get popped up in the interim. + call_deferred(SNAME("_popup_conditional_hide")); } -void Popup::set_as_minsize() { - set_size(get_contents_minimum_size()); +void Popup::_post_popup() { + Window::_post_popup(); + popped_up = true; } -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::_popup_conditional_hide() { + if (!popped_up) { + hide(); + } } 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")); + ClassDB::bind_method(D_METHOD("_popup_conditional_hide"), &Popup::_popup_conditional_hide); } Rect2i Popup::_popup_adjust_rect() const { @@ -184,6 +186,7 @@ Popup::Popup() { set_transient(true); set_flag(FLAG_BORDERLESS, true); set_flag(FLAG_RESIZE_DISABLED, true); + set_flag(FLAG_POPUP, true); connect("window_input", callable_mp(this, &Popup::_input_from_window)); } @@ -241,13 +244,20 @@ void PopupPanel::_update_child_rects() { } void PopupPanel::_notification(int p_what) { - if (p_what == NOTIFICATION_THEME_CHANGED) { - panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), get_class_name())); - } else if (p_what == NOTIFICATION_READY || p_what == NOTIFICATION_ENTER_TREE) { - panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), get_class_name())); - _update_child_rects(); - } else if (p_what == NOTIFICATION_WM_SIZE_CHANGED) { - _update_child_rects(); + switch (p_what) { + case NOTIFICATION_THEME_CHANGED: { + panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), get_class_name())); + } break; + + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_READY: { + panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("panel"), get_class_name())); + _update_child_rects(); + } break; + + case NOTIFICATION_WM_SIZE_CHANGED: { + _update_child_rects(); + } break; } } diff --git a/scene/gui/popup.h b/scene/gui/popup.h index 8458a75eef..27f46d4a97 100644 --- a/scene/gui/popup.h +++ b/scene/gui/popup.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 Popup : public Window { LocalVector<Window *> visible_parents; bool popped_up = false; - bool close_on_parent_focus = true; void _input_from_window(const Ref<InputEvent> &p_event); @@ -58,12 +57,11 @@ protected: void _notification(int p_what); static void _bind_methods(); -public: - void set_as_minsize(); + void _popup_conditional_hide(); - void set_close_on_parent_focus(bool p_close); - bool get_close_on_parent_focus(); + virtual void _post_popup() override; +public: Popup(); ~Popup(); }; @@ -71,7 +69,7 @@ public: class PopupPanel : public Popup { GDCLASS(PopupPanel, Popup); - Panel *panel; + Panel *panel = nullptr; protected: void _update_child_rects(); diff --git a/scene/gui/popup_menu.cpp b/scene/gui/popup_menu.cpp index a61d77e95c..8303d6db57 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "popup_menu.h" +#include "core/config/project_settings.h" #include "core/input/input.h" #include "core/os/keyboard.h" #include "core/os/os.h" @@ -46,11 +47,11 @@ String PopupMenu::_get_accel_text(const Item &p_item) const { } Size2 PopupMenu::_get_contents_minimum_size() const { - int vseparation = get_theme_constant(SNAME("vseparation")); - int hseparation = get_theme_constant(SNAME("hseparation")); + int vseparation = get_theme_constant(SNAME("v_separation")); + int hseparation = get_theme_constant(SNAME("h_separation")); 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 + minsize.x += scroll_container->get_v_scroll_bar()->get_size().width * 2; // Adds a buffer so that the scrollbar does not render over the top of content float max_w = 0.0; float icon_w = 0.0; @@ -67,7 +68,7 @@ Size2 PopupMenu::_get_contents_minimum_size() const { size.width += items[i].h_ofs; - if (items[i].checkable_type) { + if (items[i].checkable_type && !items[i].separator) { has_check = true; } @@ -108,10 +109,9 @@ Size2 PopupMenu::_get_contents_minimum_size() const { 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) { + if (items[p_item].checkable_type && !items[p_item].separator) { icon_height = MAX(icon_height, MAX(get_theme_icon(SNAME("checked"))->get_height(), get_theme_icon(SNAME("radio_checked"))->get_height())); } @@ -129,7 +129,7 @@ int PopupMenu::_get_item_height(int p_item) const { } int PopupMenu::_get_items_total_height() const { - int vsep = get_theme_constant(SNAME("vseparation")); + int vsep = get_theme_constant(SNAME("v_separation")); // Get total height of all items by taking max of icon height and font height int items_total_height = 0; @@ -141,23 +141,6 @@ int PopupMenu::_get_items_total_height() const { return items_total_height - vsep; } -void PopupMenu::_scroll_to_item(int p_item) { - ERR_FAIL_INDEX(p_item, items.size()); - ERR_FAIL_COND(p_item < 0); - - // Scroll item into view (upwards) - if (items[p_item]._ofs_cache < -control->get_position().y) { - int amnt_over = items[p_item]._ofs_cache + control->get_position().y; - scroll_container->set_v_scroll(scroll_container->get_v_scroll() + amnt_over); - } - - // Scroll item into view (downwards) - if (items[p_item]._ofs_cache + items[p_item]._height_cache > -control->get_position().y + scroll_container->get_size().height) { - int amnt_over = items[p_item]._ofs_cache + items[p_item]._height_cache + control->get_position().y - scroll_container->get_size().height; - scroll_container->set_v_scroll(scroll_container->get_v_scroll() + amnt_over); - } -} - int PopupMenu::_get_mouse_over(const Point2 &p_over) const { if (p_over.x < 0 || p_over.x >= get_size().width) { return -1; @@ -165,7 +148,7 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { Ref<StyleBox> style = get_theme_stylebox(SNAME("panel")); // Accounts for margin in the margin container - int vseparation = get_theme_constant(SNAME("vseparation")); + int vseparation = get_theme_constant(SNAME("v_separation")); Point2 ofs = style->get_offset() + Point2(0, vseparation / 2); @@ -186,32 +169,34 @@ int PopupMenu::_get_mouse_over(const Point2 &p_over) const { return -1; } -void PopupMenu::_activate_submenu(int p_over) { +void PopupMenu::_activate_submenu(int p_over, bool p_by_keyboard) { 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[p_over].submenu + "."); if (submenu_popup->is_visible()) { - return; //already visible! + return; // Already visible. } Ref<StyleBox> style = get_theme_stylebox(SNAME("panel")); - int vsep = get_theme_constant(SNAME("vseparation")); + int vsep = get_theme_constant(SNAME("v_separation")); Point2 this_pos = get_position(); Rect2 this_rect(this_pos, get_size()); float scroll_offset = control->get_position().y; - Point2 submenu_pos; + submenu_popup->reset_size(); // Shrink the popup size to its contents. Size2 submenu_size = submenu_popup->get_size(); + + Point2 submenu_pos; 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 + // 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; } @@ -220,27 +205,41 @@ void PopupMenu::_activate_submenu(int p_over) { 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 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[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[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)); - } + if (!submenu_pum) { + submenu_popup->popup(); + return; + } + + submenu_pum->activated_by_keyboard = p_by_keyboard; + + // If not triggered by the mouse, start the popup with its first item selected. + if (submenu_pum->get_item_count() > 0 && p_by_keyboard) { + submenu_pum->set_current_index(0); + } + + submenu_pum->popup(); + + // Set autohide areas. + + Rect2 safe_area = this_rect; + safe_area.position.y += items[p_over]._ofs_cache + scroll_offset + style->get_offset().height - vsep / 2; + safe_area.size.y = items[p_over]._height_cache; + DisplayServer::get_singleton()->window_set_popup_safe_rect(submenu_popup->get_window_id(), safe_area); + + // 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[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[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)); } } @@ -266,7 +265,7 @@ void PopupMenu::gui_input(const Ref<InputEvent> &p_event) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; emit_signal(SNAME("id_focused"), i); - _scroll_to_item(i); + scroll_to_item(i); control->update(); set_input_as_handled(); match_found = true; @@ -280,7 +279,7 @@ void PopupMenu::gui_input(const Ref<InputEvent> &p_event) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; emit_signal(SNAME("id_focused"), i); - _scroll_to_item(i); + scroll_to_item(i); control->update(); set_input_as_handled(); break; @@ -298,7 +297,7 @@ void PopupMenu::gui_input(const Ref<InputEvent> &p_event) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; emit_signal(SNAME("id_focused"), i); - _scroll_to_item(i); + scroll_to_item(i); control->update(); set_input_as_handled(); match_found = true; @@ -312,7 +311,7 @@ void PopupMenu::gui_input(const Ref<InputEvent> &p_event) { if (!items[i].separator && !items[i].disabled) { mouse_over = i; emit_signal(SNAME("id_focused"), i); - _scroll_to_item(i); + scroll_to_item(i); control->update(); set_input_as_handled(); break; @@ -326,14 +325,14 @@ void PopupMenu::gui_input(const Ref<InputEvent> &p_event) { set_input_as_handled(); } } else if (p_event->is_action("ui_right") && p_event->is_pressed()) { - if (mouse_over >= 0 && mouse_over < items.size() && !!items[mouse_over].separator && items[mouse_over].submenu.is_empty() && submenu_over != mouse_over) { - _activate_submenu(mouse_over); + if (mouse_over >= 0 && mouse_over < items.size() && !items[mouse_over].separator && !items[mouse_over].submenu.is_empty() && submenu_over != mouse_over) { + _activate_submenu(mouse_over, true); set_input_as_handled(); } } else if (p_event->is_action("ui_accept") && p_event->is_pressed()) { if (mouse_over >= 0 && mouse_over < items.size() && !items[mouse_over].separator) { if (!items[mouse_over].submenu.is_empty() && submenu_over != mouse_over) { - _activate_submenu(mouse_over); + _activate_submenu(mouse_over, true); } else { activate_item(mouse_over); } @@ -343,11 +342,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()) { + if (scroll_container->get_v_scroll_bar()->is_visible_in_tree()) { if (is_layout_rtl()) { - item_clickable_area.position.x += scroll_container->get_v_scrollbar()->get_size().width; + item_clickable_area.position.x += scroll_container->get_v_scroll_bar()->get_size().width; } else { - item_clickable_area.size.width -= scroll_container->get_v_scrollbar()->get_size().width; + item_clickable_area.size.width -= scroll_container->get_v_scroll_bar()->get_size().width; } } @@ -399,6 +398,11 @@ void PopupMenu::gui_input(const Ref<InputEvent> &p_event) { Ref<InputEventMouseMotion> m = p_event; if (m.is_valid()) { + if (m->get_velocity().is_equal_approx(Vector2())) { + return; + } + activated_by_keyboard = false; + for (const Rect2 &E : autohide_areas) { if (!Rect2(Point2(), get_size()).has_point(m->get_position()) && E.has_point(m->get_position())) { _close_pressed(); @@ -462,7 +466,7 @@ void PopupMenu::gui_input(const Ref<InputEvent> &p_event) { if (items[i].text.findn(search_string) == 0) { mouse_over = i; emit_signal(SNAME("id_focused"), i); - _scroll_to_item(i); + scroll_to_item(i); control->update(); set_input_as_handled(); break; @@ -486,7 +490,7 @@ void PopupMenu::_draw_items() { 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) + // In Item::checkable_type enum order (less the non-checkable member). 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; @@ -500,21 +504,25 @@ void PopupMenu::_draw_items() { 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")); + int vseparation = get_theme_constant(SNAME("v_separation")); + int hseparation = get_theme_constant(SNAME("h_separation")); 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 scroll_width = scroll_container->get_v_scroll_bar()->is_visible_in_tree() ? scroll_container->get_v_scroll_bar()->get_size().width : 0; float display_width = control->get_size().width - scroll_width; // Find the widest icon and whether any items have a checkbox, and store the offsets for each. float icon_ofs = 0.0; bool has_check = false; for (int i = 0; i < items.size(); i++) { + if (items[i].separator) { + continue; + } + icon_ofs = MAX(items[i].get_icon_size().width, icon_ofs); if (items[i].checkable_type) { @@ -556,31 +564,39 @@ void PopupMenu::_draw_items() { // Separator 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.is_empty()) { - 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, sep_ofs), Size2(MAX(0, text_left - item_ofs.x), sep_h))); + if (!text.is_empty() || !items[i].icon.is_null()) { + int content_size = items[i].text_buf->get_size().width + hseparation * 2; + if (!items[i].icon.is_null()) { + content_size += icon_size.width + hseparation; + } + + int content_center = display_width / 2; + int content_left = content_center - content_size / 2; + int content_right = content_center + content_size / 2; + if (content_left > item_ofs.x) { + int sep_h = labeled_separator_left->get_center_size().height + labeled_separator_left->get_minimum_size().height; + int sep_ofs = Math::floor((h - sep_h) / 2.0); + labeled_separator_left->draw(ci, Rect2(item_ofs + Point2(0, sep_ofs), Size2(MAX(0, content_left - item_ofs.x), sep_h))); } - if (text_right < display_width) { - labeled_separator_right->draw(ci, Rect2(Point2(text_right, item_ofs.y + sep_ofs), Size2(MAX(0, display_width - text_right), sep_h))); + if (content_right < display_width) { + int sep_h = labeled_separator_right->get_center_size().height + labeled_separator_right->get_minimum_size().height; + int sep_ofs = Math::floor((h - sep_h) / 2.0); + labeled_separator_right->draw(ci, Rect2(Point2(content_right, item_ofs.y + sep_ofs), Size2(MAX(0, display_width - content_right), sep_h))); } } else { + int sep_h = separator->get_center_size().height + separator->get_minimum_size().height; + int sep_ofs = Math::floor((h - sep_h) / 2.0); 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); + Color icon_color(1, 1, 1, items[i].disabled && !items[i].separator ? 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) { + if (items[i].checkable_type && !items[i].separator) { Texture2D *icon = (items[i].checked ? check[items[i].checkable_type - 1] : uncheck[items[i].checkable_type - 1]).ptr(); 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); @@ -589,16 +605,28 @@ void PopupMenu::_draw_items() { } } + int separator_ofs = (display_width - items[i].text_buf->get_size().width) / 2; + // Icon if (!items[i].icon.is_null()) { - 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); + if (items[i].separator) { + separator_ofs -= (icon_size.width + hseparation) / 2; + + if (rtl) { + items[i].icon->draw(ci, Size2(control->get_size().width - item_ofs.x - separator_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(separator_ofs, 0) + 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); + 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 + // Submenu arrow on right hand side. if (!items[i].submenu.is_empty()) { 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); @@ -607,20 +635,28 @@ void PopupMenu::_draw_items() { } } - // Text Color font_outline_color = get_theme_color(SNAME("font_outline_color")); int outline_size = get_theme_constant(SNAME("outline_size")); + + // Text if (items[i].separator) { + Color font_separator_outline_color = get_theme_color(SNAME("font_separator_outline_color")); + int separator_outline_size = get_theme_constant(SNAME("separator_outline_size")); + if (!text.is_empty()) { - 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); + Vector2 text_pos = Point2(separator_ofs, item_ofs.y + Math::floor((h - items[i].text_buf->get_size().y) / 2.0)); + if (!rtl && !items[i].icon.is_null()) { + text_pos.x += icon_size.width + hseparation; + } + + if (separator_outline_size > 0 && font_separator_outline_color.a > 0) { + items[i].text_buf->draw_outline(ci, text_pos, separator_outline_size, font_separator_outline_color); } items[i].text_buf->draw(ci, text_pos, font_separator_color); } } else { item_ofs.x += icon_ofs + check_ofs; + 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) { @@ -650,7 +686,7 @@ void PopupMenu::_draw_items() { 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 + // Cache the item vertical offset from the first item and the height. items.write[i]._ofs_cache = ofs.y; items.write[i]._height_cache = h; @@ -667,7 +703,7 @@ void PopupMenu::_draw_background() { 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())) { + if (!activated_by_keyboard && !get_visible_rect().has_point(get_mouse_position())) { _close_pressed(); } } @@ -693,8 +729,8 @@ void PopupMenu::_shape_item(int p_item) { if (items.write[p_item].dirty) { items.write[p_item].text_buf->clear(); - Ref<Font> font = get_theme_font(SNAME("font")); - int font_size = get_theme_font_size(SNAME("font_size")); + Ref<Font> font = get_theme_font(items[p_item].separator ? SNAME("font_separator") : SNAME("font")); + int font_size = get_theme_font_size(items[p_item].separator ? SNAME("font_separator_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); @@ -715,11 +751,12 @@ void PopupMenu::_notification(int p_what) { case NOTIFICATION_ENTER_TREE: { PopupMenu *pm = Object::cast_to<PopupMenu>(get_parent()); if (pm) { - // Inherit submenu's popup delay time from parent menu + // Inherit submenu's popup delay time from parent menu. float pm_delay = pm->get_submenu_popup_delay(); set_submenu_popup_delay(pm_delay); } } break; + case NOTIFICATION_THEME_CHANGED: case Control::NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { @@ -732,24 +769,26 @@ void PopupMenu::_notification(int p_what) { child_controls_changed(); control->update(); } break; + case NOTIFICATION_WM_MOUSE_ENTER: { grab_focus(); } break; + case NOTIFICATION_WM_MOUSE_EXIT: { if (mouse_over >= 0 && (items[mouse_over].submenu.is_empty() || submenu_over != -1)) { mouse_over = -1; control->update(); } } break; + case NOTIFICATION_POST_POPUP: { initial_button_mask = Input::get_singleton()->get_mouse_button_mask(); during_grabbed_click = (bool)initial_button_mask; } break; - case NOTIFICATION_WM_SIZE_CHANGED: { - } break; + case NOTIFICATION_INTERNAL_PROCESS: { - //only used when using operating system windows - if (!is_embedded() && autohide_areas.size()) { + // Only used when using operating system windows. + if (!activated_by_keyboard && !is_embedded() && autohide_areas.size()) { Point2 mouse_pos = DisplayServer::get_singleton()->mouse_get_position(); mouse_pos -= get_position(); @@ -761,6 +800,7 @@ void PopupMenu::_notification(int p_what) { } } } break; + case NOTIFICATION_VISIBILITY_CHANGED: { if (!is_visible()) { if (mouse_over >= 0) { @@ -794,10 +834,10 @@ void PopupMenu::_notification(int p_what) { // Set margin on the margin container 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_top", panel_style->get_margin(Side::SIDE_TOP)); margin_container->add_theme_constant_override("margin_right", panel_style->get_margin(Side::SIDE_RIGHT)); + margin_container->add_theme_constant_override("margin_bottom", panel_style->get_margin(Side::SIDE_BOTTOM)); } } break; } @@ -975,9 +1015,16 @@ void PopupMenu::add_submenu_item(const String &p_label, const String &p_submenu, /* Methods to modify existing items. */ void PopupMenu::set_item_text(int p_idx, const String &p_text) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); + if (items[p_idx].text == p_text) { + return; + } items.write[p_idx].text = p_text; items.write[p_idx].xl_text = atr(p_text); + items.write[p_idx].dirty = true; _shape_item(p_idx); control->update(); @@ -985,6 +1032,9 @@ void PopupMenu::set_item_text(int p_idx, const String &p_text) { } void PopupMenu::set_item_text_direction(int p_item, Control::TextDirection p_text_direction) { + if (p_item < 0) { + p_item += get_item_count(); + } 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) { @@ -995,6 +1045,9 @@ void PopupMenu::set_item_text_direction(int p_item, Control::TextDirection p_tex } void PopupMenu::clear_item_opentype_features(int p_item) { + if (p_item < 0) { + p_item += get_item_count(); + } ERR_FAIL_INDEX(p_item, items.size()); items.write[p_item].opentype_features.clear(); items.write[p_item].dirty = true; @@ -1002,6 +1055,9 @@ void PopupMenu::clear_item_opentype_features(int p_item) { } void PopupMenu::set_item_opentype_feature(int p_item, const String &p_name, int p_value) { + if (p_item < 0) { + p_item += get_item_count(); + } 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) { @@ -1012,6 +1068,9 @@ void PopupMenu::set_item_opentype_feature(int p_item, const String &p_name, int } void PopupMenu::set_item_language(int p_item, const String &p_language) { + if (p_item < 0) { + p_item += get_item_count(); + } ERR_FAIL_INDEX(p_item, items.size()); if (items[p_item].language != p_language) { items.write[p_item].language = p_language; @@ -1021,6 +1080,9 @@ void PopupMenu::set_item_language(int p_item, const String &p_language) { } void PopupMenu::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].icon = p_icon; @@ -1029,6 +1091,9 @@ void PopupMenu::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) { } void PopupMenu::set_item_checked(int p_idx, bool p_checked) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].checked = p_checked; @@ -1038,6 +1103,9 @@ void PopupMenu::set_item_checked(int p_idx, bool p_checked) { } void PopupMenu::set_item_id(int p_idx, int p_id) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].id = p_id; @@ -1046,6 +1114,9 @@ void PopupMenu::set_item_id(int p_idx, int p_id) { } void PopupMenu::set_item_accelerator(int p_idx, Key p_accel) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].accel = p_accel; items.write[p_idx].dirty = true; @@ -1055,6 +1126,9 @@ void PopupMenu::set_item_accelerator(int p_idx, Key p_accel) { } void PopupMenu::set_item_metadata(int p_idx, const Variant &p_meta) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].metadata = p_meta; control->update(); @@ -1062,6 +1136,9 @@ void PopupMenu::set_item_metadata(int p_idx, const Variant &p_meta) { } void PopupMenu::set_item_disabled(int p_idx, bool p_disabled) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].disabled = p_disabled; control->update(); @@ -1069,6 +1146,9 @@ void PopupMenu::set_item_disabled(int p_idx, bool p_disabled) { } void PopupMenu::set_item_submenu(int p_idx, const String &p_submenu) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].submenu = p_submenu; control->update(); @@ -1177,6 +1257,9 @@ int PopupMenu::get_item_state(int p_idx) const { } void PopupMenu::set_item_as_separator(int p_idx, bool p_separator) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].separator = p_separator; control->update(); @@ -1188,24 +1271,36 @@ bool PopupMenu::is_item_separator(int p_idx) const { } void PopupMenu::set_item_as_checkable(int p_idx, bool p_checkable) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].checkable_type = p_checkable ? Item::CHECKABLE_TYPE_CHECK_BOX : Item::CHECKABLE_TYPE_NONE; control->update(); } void PopupMenu::set_item_as_radio_checkable(int p_idx, bool p_radio_checkable) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].checkable_type = p_radio_checkable ? Item::CHECKABLE_TYPE_RADIO_BUTTON : Item::CHECKABLE_TYPE_NONE; control->update(); } void PopupMenu::set_item_tooltip(int p_idx, const String &p_tooltip) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].tooltip = p_tooltip; control->update(); } void PopupMenu::set_item_shortcut(int p_idx, const Ref<Shortcut> &p_shortcut, bool p_global) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); if (items[p_idx].shortcut.is_valid()) { _unref_shortcut(items[p_idx].shortcut); @@ -1222,6 +1317,9 @@ void PopupMenu::set_item_shortcut(int p_idx, const Ref<Shortcut> &p_shortcut, bo } void PopupMenu::set_item_h_offset(int p_idx, int p_offset) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].h_ofs = p_offset; control->update(); @@ -1229,12 +1327,18 @@ void PopupMenu::set_item_h_offset(int p_idx, int p_offset) { } void PopupMenu::set_item_multistate(int p_idx, int p_state) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].state = p_state; control->update(); } void PopupMenu::set_item_shortcut_disabled(int p_idx, bool p_disabled) { + if (p_idx < 0) { + p_idx += get_item_count(); + } ERR_FAIL_INDEX(p_idx, items.size()); items.write[p_idx].shortcut_is_disabled = p_disabled; control->update(); @@ -1269,13 +1373,28 @@ bool PopupMenu::is_item_shortcut_disabled(int p_idx) const { return items[p_idx].shortcut_is_disabled; } +void PopupMenu::set_current_index(int p_idx) { + ERR_FAIL_INDEX(p_idx, items.size()); + mouse_over = p_idx; + scroll_to_item(mouse_over); + control->update(); +} + int PopupMenu::get_current_index() const { return mouse_over; } void PopupMenu::set_item_count(int p_count) { ERR_FAIL_COND(p_count < 0); + int prev_size = items.size(); items.resize(p_count); + + if (prev_size < p_count) { + for (int i = prev_size; i < p_count; i++) { + items.write[i].id = i; + } + } + control->update(); child_controls_changed(); notify_property_list_changed(); @@ -1285,6 +1404,20 @@ int PopupMenu::get_item_count() const { return items.size(); } +void PopupMenu::scroll_to_item(int p_item) { + ERR_FAIL_INDEX(p_item, items.size()); + + // Scroll item into view (upwards). + if (items[p_item]._ofs_cache - scroll_container->get_v_scroll() < -control->get_position().y) { + scroll_container->set_v_scroll(items[p_item]._ofs_cache + control->get_position().y); + } + + // Scroll item into view (downwards). + if (items[p_item]._ofs_cache + items[p_item]._height_cache - scroll_container->get_v_scroll() > -control->get_position().y + scroll_container->get_size().height) { + scroll_container->set_v_scroll(items[p_item]._ofs_cache + items[p_item]._height_cache + control->get_position().y); + } +} + bool PopupMenu::activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only) { Key code = Key::NONE; Ref<InputEventKey> k = p_event; @@ -1388,12 +1521,12 @@ void PopupMenu::activate_item(int p_item) { need_hide = false; } - emit_signal(SNAME("id_pressed"), id); - emit_signal(SNAME("index_pressed"), p_item); - if (need_hide) { hide(); } + + emit_signal(SNAME("id_pressed"), id); + emit_signal(SNAME("index_pressed"), p_item); } void PopupMenu::remove_item(int p_idx) { @@ -1558,7 +1691,7 @@ bool PopupMenu::_set(const StringName &p_name, const Variant &p_value) { } else if (property == "id") { set_item_id(item_index, p_value); return true; - } else if (components[1] == "disabled") { + } else if (property == "disabled") { set_item_disabled(item_index, p_value); return true; } else if (property == "separator") { @@ -1631,7 +1764,7 @@ bool PopupMenu::_get(const StringName &p_name, Variant &r_ret) const { } else if (property == "id") { r_ret = get_item_id(item_index); return true; - } else if (components[1] == "disabled") { + } else if (property == "disabled") { r_ret = is_item_disabled(item_index); return true; } else if (property == "separator") { @@ -1658,7 +1791,7 @@ void PopupMenu::_get_property_list(List<PropertyInfo> *p_list) const { pi.usage &= ~(!is_item_checked(i) ? PROPERTY_USAGE_STORAGE : 0); p_list->push_back(pi); - pi = PropertyInfo(Variant::INT, vformat("item_%d/id", i), PROPERTY_HINT_RANGE, "1,10,1,or_greater"); + pi = PropertyInfo(Variant::INT, vformat("item_%d/id", i), PROPERTY_HINT_RANGE, "0,10,1,or_greater"); p_list->push_back(pi); pi = PropertyInfo(Variant::BOOL, vformat("item_%d/disabled", i)); @@ -1732,10 +1865,13 @@ void PopupMenu::_bind_methods() { ClassDB::bind_method(D_METHOD("get_item_tooltip", "index"), &PopupMenu::get_item_tooltip); ClassDB::bind_method(D_METHOD("get_item_shortcut", "index"), &PopupMenu::get_item_shortcut); + ClassDB::bind_method(D_METHOD("set_current_index", "index"), &PopupMenu::set_current_index); ClassDB::bind_method(D_METHOD("get_current_index"), &PopupMenu::get_current_index); ClassDB::bind_method(D_METHOD("set_item_count", "count"), &PopupMenu::set_item_count); ClassDB::bind_method(D_METHOD("get_item_count"), &PopupMenu::get_item_count); + ClassDB::bind_method(D_METHOD("scroll_to_item", "index"), &PopupMenu::scroll_to_item); + ClassDB::bind_method(D_METHOD("remove_item", "index"), &PopupMenu::remove_item); ClassDB::bind_method(D_METHOD("add_separator", "label", "id"), &PopupMenu::add_separator, DEFVAL(String()), DEFVAL(-1)); diff --git a/scene/gui/popup_menu.h b/scene/gui/popup_menu.h index 22912fb59c..12587b7e73 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -87,9 +87,10 @@ class PopupMenu : public Popup { }; bool close_allowed = false; + bool activated_by_keyboard = false; Timer *minimum_lifetime_timer = nullptr; - Timer *submenu_timer; + Timer *submenu_timer = nullptr; List<Rect2> autohide_areas; Vector<Item> items; MouseButton initial_button_mask = MouseButton::NONE; @@ -103,12 +104,11 @@ class PopupMenu : public Popup { int _get_item_height(int p_item) const; int _get_items_total_height() const; - void _scroll_to_item(int p_item); void _shape_item(int p_item); virtual void gui_input(const Ref<InputEvent> &p_event); - void _activate_submenu(int p_over); + void _activate_submenu(int p_over, bool p_by_keyboard = false); void _submenu_timeout(); uint64_t popup_time_msec = 0; @@ -117,7 +117,7 @@ class PopupMenu : public Popup { bool hide_on_multistate_item_selection = false; Vector2 moved; - Map<Ref<Shortcut>, int> shortcut_refcount; + HashMap<Ref<Shortcut>, int> shortcut_refcount; void _ref_shortcut(Ref<Shortcut> p_sc); void _unref_shortcut(Ref<Shortcut> p_sc); @@ -126,9 +126,9 @@ class PopupMenu : public Popup { uint64_t search_time_msec = 0; String search_string = ""; - MarginContainer *margin_container; - ScrollContainer *scroll_container; - Control *control; + MarginContainer *margin_container = nullptr; + ScrollContainer *scroll_container = nullptr; + Control *control = nullptr; void _draw_items(); void _draw_background(); @@ -212,11 +212,14 @@ public: Ref<Shortcut> get_item_shortcut(int p_idx) const; int get_item_state(int p_idx) const; + void set_current_index(int p_idx); int get_current_index() const; void set_item_count(int p_count); int get_item_count() const; + void scroll_to_item(int p_item); + bool activate_item_by_event(const Ref<InputEvent> &p_event, bool p_for_global_only = false); void activate_item(int p_item); diff --git a/scene/gui/progress_bar.cpp b/scene/gui/progress_bar.cpp index 2cfaaa2fde..f36682942f 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +29,7 @@ /*************************************************************************/ #include "progress_bar.h" + #include "scene/resources/text_line.h" Size2 ProgressBar::get_minimum_size() const { @@ -52,39 +53,78 @@ Size2 ProgressBar::get_minimum_size() const { } void ProgressBar::_notification(int p_what) { - if (p_what == NOTIFICATION_DRAW) { - 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) { - 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))); + switch (p_what) { + case NOTIFICATION_DRAW: { + 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(); + + switch (mode) { + case FILL_BEGIN_TO_END: + case FILL_END_TO_BEGIN: { + int mp = fg->get_minimum_size().width; + int p = round(r * (get_size().width - mp)); + // We want FILL_BEGIN_TO_END to map to right to left when UI layout is RTL, + // and left to right otherwise. And likewise for FILL_END_TO_BEGIN. + bool right_to_left = is_layout_rtl() ? (mode == FILL_BEGIN_TO_END) : (mode == FILL_END_TO_BEGIN); + if (p > 0) { + if (right_to_left) { + int p_remaining = round((1.0 - r) * (get_size().width - mp)); + draw_style_box(fg, Rect2(Point2(p_remaining, 0), Size2(p + 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))); + } + } + } break; + case FILL_TOP_TO_BOTTOM: + case FILL_BOTTOM_TO_TOP: { + int mp = fg->get_minimum_size().height; + int p = round(r * (get_size().height - mp)); + + if (p > 0) { + if (mode == FILL_TOP_TO_BOTTOM) { + draw_style_box(fg, Rect2(Point2(0, 0), Size2(get_size().width, p + fg->get_minimum_size().height))); + } else { + int p_remaining = round((1.0 - r) * (get_size().height - mp)); + draw_style_box(fg, Rect2(Point2(0, p_remaining), Size2(get_size().width, p + fg->get_minimum_size().height))); + } + } + } break; + case FILL_MODE_MAX: + break; } - } - - if (percent_visible) { - 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); + + if (percent_visible) { + 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); } - tl.draw(get_canvas_item(), text_pos, font_color); - } + } break; } } +void ProgressBar::set_fill_mode(int p_fill) { + ERR_FAIL_INDEX(p_fill, FILL_MODE_MAX); + mode = (FillMode)p_fill; + update(); +} + +int ProgressBar::get_fill_mode() { + return mode; +} + void ProgressBar::set_percent_visible(bool p_visible) { percent_visible = p_visible; update(); @@ -95,10 +135,18 @@ bool ProgressBar::is_percent_visible() const { } void ProgressBar::_bind_methods() { + ClassDB::bind_method(D_METHOD("set_fill_mode", "mode"), &ProgressBar::set_fill_mode); + ClassDB::bind_method(D_METHOD("get_fill_mode"), &ProgressBar::get_fill_mode); ClassDB::bind_method(D_METHOD("set_percent_visible", "visible"), &ProgressBar::set_percent_visible); ClassDB::bind_method(D_METHOD("is_percent_visible"), &ProgressBar::is_percent_visible); - ADD_GROUP("Percent", "percent_"); + + ADD_PROPERTY(PropertyInfo(Variant::INT, "fill_mode", PROPERTY_HINT_ENUM, "Begin to End,End to Begin,Top to Bottom,Bottom to Top"), "set_fill_mode", "get_fill_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "percent_visible"), "set_percent_visible", "is_percent_visible"); + + BIND_ENUM_CONSTANT(FILL_BEGIN_TO_END); + BIND_ENUM_CONSTANT(FILL_END_TO_BEGIN); + BIND_ENUM_CONSTANT(FILL_TOP_TO_BOTTOM); + BIND_ENUM_CONSTANT(FILL_BOTTOM_TO_TOP); } ProgressBar::ProgressBar() { diff --git a/scene/gui/progress_bar.h b/scene/gui/progress_bar.h index fb6060d932..5ba21ad7d5 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,11 +43,27 @@ protected: static void _bind_methods(); public: + enum FillMode { + FILL_BEGIN_TO_END, + FILL_END_TO_BEGIN, + FILL_TOP_TO_BOTTOM, + FILL_BOTTOM_TO_TOP, + FILL_MODE_MAX + }; + + void set_fill_mode(int p_fill); + int get_fill_mode(); + void set_percent_visible(bool p_visible); bool is_percent_visible() const; Size2 get_minimum_size() const override; ProgressBar(); + +private: + FillMode mode = FILL_BEGIN_TO_END; }; +VARIANT_ENUM_CAST(ProgressBar::FillMode); + #endif // PROGRESS_BAR_H diff --git a/scene/gui/range.cpp b/scene/gui/range.cpp index c4f05a7975..fae6688452 100644 --- a/scene/gui/range.cpp +++ b/scene/gui/range.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,12 +34,15 @@ TypedArray<String> Range::get_configuration_warnings() const { TypedArray<String> warnings = Node::get_configuration_warnings(); if (shared->exp_ratio && shared->min <= 0) { - warnings.push_back(TTR("If \"Exp Edit\" is enabled, \"Min Value\" must be greater than 0.")); + warnings.push_back(RTR("If \"Exp Edit\" is enabled, \"Min Value\" must be greater than 0.")); } return warnings; } +void Range::_value_changed(double p_value) { + GDVIRTUAL_CALL(_value_changed, p_value); +} void Range::_value_changed_notify() { _value_changed(shared->val); emit_signal(SNAME("value_changed"), shared->val); @@ -47,8 +50,8 @@ void Range::_value_changed_notify() { } void Range::Shared::emit_value_changed() { - for (Set<Range *>::Element *E = owners.front(); E; E = E->next()) { - Range *r = E->get(); + for (Range *E : owners) { + Range *r = E; if (!r->is_inside_tree()) { continue; } @@ -67,8 +70,8 @@ void Range::_validate_values() { } void Range::Shared::emit_changed(const char *p_what) { - for (Set<Range *>::Element *E = owners.front(); E; E = E->next()) { - Range *r = E->get(); + for (Range *E : owners) { + Range *r = E; if (!r->is_inside_tree()) { continue; } @@ -279,6 +282,8 @@ void Range::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_greater"), "set_allow_greater", "is_greater_allowed"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_lesser"), "set_allow_lesser", "is_lesser_allowed"); + GDVIRTUAL_BIND(_value_changed, "new_value"); + ADD_LINKED_PROPERTY("min_value", "value"); ADD_LINKED_PROPERTY("min_value", "max_value"); ADD_LINKED_PROPERTY("min_value", "page"); diff --git a/scene/gui/range.h b/scene/gui/range.h index 0dc702b19c..1274821bd1 100644 --- a/scene/gui/range.h +++ b/scene/gui/range.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,12 +45,12 @@ class Range : public Control { bool exp_ratio = false; bool allow_greater = false; bool allow_lesser = false; - Set<Range *> owners; + HashSet<Range *> owners; void emit_value_changed(); void emit_changed(const char *p_what = ""); }; - Shared *shared; + Shared *shared = nullptr; void _ref_shared(Shared *p_shared); void _unref_shared(); @@ -62,12 +62,14 @@ class Range : public Control { void _validate_values(); protected: - virtual void _value_changed(double) {} + virtual void _value_changed(double p_value); static void _bind_methods(); bool _rounded_values = false; + GDVIRTUAL1(_value_changed, double) + public: void set_value(double p_val); void set_min(double p_min); diff --git a/scene/gui/reference_rect.cpp b/scene/gui/reference_rect.cpp index 6d7f2cfd57..ed79da5c22 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,15 @@ #include "core/config/engine.h" void ReferenceRect::_notification(int p_what) { - if (p_what == NOTIFICATION_DRAW) { - if (!is_inside_tree()) { - return; - } - if (Engine::get_singleton()->is_editor_hint() || !editor_only) { - draw_rect(Rect2(Point2(), get_size()), border_color, false, border_width); - } + switch (p_what) { + case NOTIFICATION_DRAW: { + if (!is_inside_tree()) { + return; + } + if (Engine::get_singleton()->is_editor_hint() || !editor_only) { + draw_rect(Rect2(Point2(), get_size()), border_color, false, border_width); + } + } break; } } diff --git a/scene/gui/reference_rect.h b/scene/gui/reference_rect.h index 7097e83a15..4a2d328162 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 076fa132c0..c9516ed6b9 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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.h b/scene/gui/rich_text_effect.h index 5681f9b193..4532a812ee 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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_label.cpp b/scene/gui/rich_text_label.cpp index 7ed8056826..cf7bf930a5 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,9 +30,11 @@ #include "rich_text_label.h" +#include "core/input/input_map.h" #include "core/math/math_defs.h" #include "core/os/keyboard.h" #include "core/os/os.h" +#include "label.h" #include "scene/scene_string_names.h" #include "servers/display_server.h" @@ -141,7 +143,7 @@ RichTextLabel::Item *RichTextLabel::_get_item_at_pos(RichTextLabel::Item *p_item 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; + ItemText *t = static_cast<ItemText *>(it); offset += t->text.length(); if (offset > p_position) { return it; @@ -165,16 +167,16 @@ String RichTextLabel::_roman(int p_num, bool p_capitalize) const { }; 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" }; + const String M[] = { "", "M", "MM", "MMM" }; + const String C[] = { "", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM" }; + const String X[] = { "", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC" }; + const 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" }; + const String M[] = { "", "m", "mm", "mmm" }; + const String C[] = { "", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm" }; + const String X[] = { "", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc" }; + const 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]; } return s; @@ -205,11 +207,56 @@ String RichTextLabel::_letters(int p_num, bool p_capitalize) const { return s; } -void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width) { +void RichTextLabel::_update_line_font(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size) { ERR_FAIL_COND(p_frame == nullptr); - ERR_FAIL_COND(p_line < 0 || p_line >= p_frame->lines.size()); + ERR_FAIL_COND(p_line < 0 || p_line >= (int)p_frame->lines.size()); - Line &l = p_frame->lines.write[p_line]; + Line &l = p_frame->lines[p_line]; + MutexLock lock(l.text_buf->get_mutex()); + + RID t = l.text_buf->get_rid(); + int spans = TS->shaped_get_span_count(t); + for (int i = 0; i < spans; i++) { + ItemText *it = reinterpret_cast<ItemText *>((uint64_t)TS->shaped_get_span_meta(t, i)); + if (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); + TS->shaped_set_span_update_font(t, i, font->get_rids(), font_size, font_ftr); + } + } + + Item *it_to = (p_line + 1 < (int)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); + 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 < (int)frame->lines.size(); i++) { + _update_line_font(frame, i, p_base_font, p_base_font_size); + } + } + } break; + default: + break; + } + } +} + +float RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width, float p_h) { + ERR_FAIL_COND_V(p_frame == nullptr, p_h); + ERR_FAIL_COND_V(p_line < 0 || p_line >= (int)p_frame->lines.size(), p_h); + + Line &l = p_frame->lines[p_line]; + MutexLock lock(l.text_buf->get_mutex()); l.offset.x = _find_margin(l.from, p_base_font, p_base_font_size); l.text_buf->set_width(p_width - l.offset.x); @@ -220,25 +267,28 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> l.text_buf->tab_align(tabs); } - Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + Item *it_to = (p_line + 1 < (int)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 hseparation = get_theme_constant(SNAME("table_h_separation")); + int vseparation = get_theme_constant(SNAME("table_v_separation")); int col_count = table->columns.size(); for (int i = 0; i < col_count; i++) { - table->columns.write[i].width = 0; + table->columns[i].width = 0; } 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); + float prev_h = 0; + for (int i = 0; i < (int)frame->lines.size(); i++) { + MutexLock sub_lock(frame->lines[i].text_buf->get_mutex()); + int w = _find_margin(frame->lines[i].from, p_base_font, p_base_font_size) + 1; + prev_h = _resize_line(frame, i, p_base_font, p_base_font_size, w, prev_h); } idx++; } @@ -254,7 +304,7 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> 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; + table->columns[i].expand = true; } if (table->columns[i].expand) { total_ratio += table->columns[i].expand_ratio; @@ -263,9 +313,9 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> // 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->columns[i].width = table->columns[i].min_width; + if (table->columns[i].expand && total_ratio > 0 && remaining_width > 0) { + table->columns[i].width += table->columns[i].expand_ratio * remaining_width / total_ratio; } table->total_width += table->columns[i].width + hseparation; } @@ -282,7 +332,7 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> 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->columns[i].width = table->columns[i].max_width; table->total_width -= dif; total_ratio -= table->columns[i].expand_ratio; } @@ -296,7 +346,7 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> 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->columns[i].width += incr; table->total_width += incr; } } @@ -320,18 +370,19 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> 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)); + float prev_h = 0; + for (int i = 0; i < (int)frame->lines.size(); i++) { + MutexLock sub_lock(frame->lines[i].text_buf->get_mutex()); + frame->lines[i].text_buf->set_width(table->columns[column].width); + table->columns[column].width = MAX(table->columns[column].width, ceil(frame->lines[i].text_buf->get_size().x)); + + frame->lines[i].offset.y = prev_h; + frame->lines[i].offset += offset; + float h = frame->lines[i].text_buf->get_size().y + (frame->lines[i].text_buf->get_line_count() - 1) * get_theme_constant(SNAME("line_separation")); 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; + h += get_theme_constant(SNAME("line_separation")); } - frame->lines.write[i].offset += offset; - - 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); } @@ -339,6 +390,7 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> h = MIN(h, frame->max_size_over.y); } yofs += h; + prev_h = frame->lines[i].offset.y + frame->lines[i].text_buf->get_size().y + frame->lines[i].text_buf->get_line_count() * get_theme_constant(SNAME("line_separation")); } yofs += frame->padding.size.y; offset.x += table->columns[column].width + hseparation + frame->padding.size.x; @@ -361,22 +413,35 @@ void RichTextLabel::_resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> } } - if (p_line > 0) { - l.offset.y = p_frame->lines[p_line - 1].offset.y + p_frame->lines[p_line - 1].text_buf->get_size().y + get_theme_constant(SNAME("line_separation")); - } else { - l.offset.y = 0; - } + l.offset.y = p_h; + return _calculate_line_vertical_offset(l); } -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()); +float RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width, float p_h, int *r_char_offset) { + ERR_FAIL_COND_V(p_frame == nullptr, p_h); + ERR_FAIL_COND_V(p_line < 0 || p_line >= (int)p_frame->lines.size(), p_h); - Line &l = p_frame->lines.write[p_line]; + Line &l = p_frame->lines[p_line]; + MutexLock lock(l.text_buf->get_mutex()); + + uint16_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; + } // 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.text_buf->set_flags(autowrap_flags | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_TRIM_EDGE_SPACES); l.char_offset = *r_char_offset; l.char_count = 0; @@ -394,24 +459,20 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> // Shape current paragraph. String text; - Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + Item *it_to = (p_line + 1 < (int)p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; int remaining_characters = visible_characters - l.char_offset; for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { - if (visible_characters >= 0 && remaining_characters <= 0) { + if (visible_chars_behavior == VC_CHARS_BEFORE_SHAPING && visible_characters >= 0 && remaining_characters <= 0) { 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(); - } + const ItemDropcap *dc = static_cast<ItemDropcap *>(it); + 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; } break; case ITEM_NEWLINE: { Ref<Font> font = _find_font(it); @@ -428,7 +489,7 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> remaining_characters--; } break; case ITEM_TEXT: { - ItemText *t = (ItemText *)it; + ItemText *t = static_cast<ItemText *>(it); Ref<Font> font = _find_font(it); if (font.is_null()) { font = p_base_font; @@ -440,33 +501,33 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> Dictionary font_ftr = _find_font_features(it); String lang = _find_language(it); String tx = t->text; - if (visible_characters >= 0 && remaining_characters >= 0) { + if (visible_chars_behavior == VC_CHARS_BEFORE_SHAPING && visible_characters >= 0 && remaining_characters >= 0) { tx = tx.substr(0, remaining_characters); } remaining_characters -= tx.length(); - l.text_buf->add_string(tx, font, font_size, font_ftr, lang); + l.text_buf->add_string(tx, font, font_size, font_ftr, lang, (uint64_t)it); 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); + ItemImage *img = static_cast<ItemImage *>(it); + l.text_buf->add_object((uint64_t)it, img->size, img->inline_align, 1); text += String::chr(0xfffc); l.char_count++; remaining_characters--; } 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 hseparation = get_theme_constant(SNAME("table_h_separation")); + int vseparation = get_theme_constant(SNAME("table_v_separation")); 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; + table->columns[i].min_width = 0; + table->columns[i].max_width = 0; + table->columns[i].width = 0; } // Compute minimum width for each cell. const int available_width = p_width - hseparation * (col_count - 1); @@ -477,16 +538,20 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> ItemFrame *frame = static_cast<ItemFrame *>(E); int column = idx % col_count; - for (int i = 0; i < frame->lines.size(); i++) { + float prev_h = 0; + for (int i = 0; i < (int)frame->lines.size(); i++) { + MutexLock sub_lock(frame->lines[i].text_buf->get_mutex()); + int char_offset = l.char_offset + l.char_count; - _shape_line(frame, i, p_base_font, p_base_font_size, 1, &char_offset); + int w = _find_margin(frame->lines[i].from, p_base_font, p_base_font_size) + 1; + prev_h = _shape_line(frame, i, p_base_font, p_base_font_size, w, prev_h, &char_offset); int cell_ch = (char_offset - (l.char_offset + l.char_count)); l.char_count += cell_ch; t_char_count += cell_ch; remaining_characters -= 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_wrapped_size().x)); + table->columns[column].min_width = MAX(table->columns[column].min_width, ceil(frame->lines[i].text_buf->get_size().x)); + table->columns[column].max_width = MAX(table->columns[column].max_width, ceil(frame->lines[i].text_buf->get_non_wrapped_size().x)); } idx++; } @@ -499,7 +564,7 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> 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; + table->columns[i].expand = true; } if (table->columns[i].expand) { total_ratio += table->columns[i].expand_ratio; @@ -508,9 +573,9 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> // 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->columns[i].width = table->columns[i].min_width; + if (table->columns[i].expand && total_ratio > 0 && remaining_width > 0) { + table->columns[i].width += table->columns[i].expand_ratio * remaining_width / total_ratio; } table->total_width += table->columns[i].width + hseparation; } @@ -527,7 +592,7 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> 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->columns[i].width = table->columns[i].max_width; table->total_width -= dif; total_ratio -= table->columns[i].expand_ratio; } @@ -541,7 +606,7 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> 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->columns[i].width += incr; table->total_width += incr; } } @@ -565,18 +630,20 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> 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)); + float prev_h = 0; + for (int i = 0; i < (int)frame->lines.size(); i++) { + MutexLock sub_lock(frame->lines[i].text_buf->get_mutex()); + + frame->lines[i].text_buf->set_width(table->columns[column].width); + table->columns[column].width = MAX(table->columns[column].width, ceil(frame->lines[i].text_buf->get_size().x)); + frame->lines[i].offset.y = prev_h; + frame->lines[i].offset += offset; + + float h = frame->lines[i].text_buf->get_size().y + (frame->lines[i].text_buf->get_line_count() - 1) * get_theme_constant(SNAME("line_separation")); 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; + h += get_theme_constant(SNAME("line_separation")); } - frame->lines.write[i].offset += offset; - - 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); } @@ -584,6 +651,7 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> h = MIN(h, frame->max_size_over.y); } yofs += h; + prev_h = frame->lines[i].offset.y + frame->lines[i].text_buf->get_size().y + frame->lines[i].text_buf->get_line_count() * get_theme_constant(SNAME("line_separation")); } yofs += frame->padding.size.y; offset.x += table->columns[column].width + hseparation + frame->padding.size.x; @@ -614,24 +682,22 @@ void RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> *r_char_offset = l.char_offset + l.char_count; - if (p_line > 0) { - l.offset.y = p_frame->lines[p_line - 1].offset.y + p_frame->lines[p_line - 1].text_buf->get_size().y + get_theme_constant(SNAME("line_separation")); - } else { - l.offset.y = 0; - } + l.offset.y = p_h; + return _calculate_line_vertical_offset(l); } -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, int p_shadow_outline_size, const Point2 &p_shadow_ofs) { - Vector2 off; - +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, int p_shadow_outline_size, const Point2 &p_shadow_ofs, int &r_processed_glyphs) { ERR_FAIL_COND_V(p_frame == nullptr, 0); - ERR_FAIL_COND_V(p_line < 0 || p_line >= p_frame->lines.size(), 0); + ERR_FAIL_COND_V(p_line < 0 || p_line >= (int)p_frame->lines.size(), 0); - Line &l = p_frame->lines.write[p_line]; + Vector2 off; + int line_spacing = get_theme_constant(SNAME("line_separation")); + + Line &l = p_frame->lines[p_line]; + MutexLock lock(l.text_buf->get_mutex()); 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; + Item *it_to = (p_line + 1 < (int)p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; if (it_from == nullptr) { return 0; @@ -641,6 +707,12 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o bool rtl = (l.text_buf->get_direction() == TextServer::DIRECTION_RTL); bool lrtl = is_layout_rtl(); + bool trim_chars = (visible_characters >= 0) && (visible_chars_behavior == VC_CHARS_AFTER_SHAPING); + bool trim_glyphs_ltr = (visible_characters >= 0) && ((visible_chars_behavior == VC_GLYPHS_LTR) || ((visible_chars_behavior == VC_GLYPHS_AUTO) && !lrtl)); + bool trim_glyphs_rtl = (visible_characters >= 0) && ((visible_chars_behavior == VC_GLYPHS_RTL) || ((visible_chars_behavior == VC_GLYPHS_AUTO) && lrtl)); + int total_glyphs = (trim_glyphs_ltr || trim_glyphs_rtl) ? get_total_glyph_count() : 0; + int visible_glyphs = total_glyphs * percent_visible; + Vector<int> list_index; Vector<ItemList *> list_items; _find_list(l.from, list_index, list_items); @@ -706,6 +778,10 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o Size2 ctrl_size = get_size(); // Draw text. for (int line = 0; line < l.text_buf->get_line_count(); line++) { + if (line > 0) { + off.y += line_spacing; + } + RID rid = l.text_buf->get_line_rid(line); if (p_ofs.y + off.y >= ctrl_size.height) { break; @@ -765,7 +841,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o // 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]; + Item *it = reinterpret_cast<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 @@ -779,7 +855,7 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o 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 hseparation = get_theme_constant(SNAME("table_h_separation")); int col_count = table->columns.size(); int row_count = table->rows.size(); @@ -803,8 +879,8 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o 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); } - 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_outline_size, p_shadow_ofs); + for (int j = 0; j < (int)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_outline_size, p_shadow_ofs, r_processed_glyphs); } idx++; } @@ -820,12 +896,14 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o Vector2 gloff = off; // Draw oulines and shadow. + int processed_glyphs_ol = r_processed_glyphs; 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); + Color font_color = _find_color(it, p_base_color); + Color font_outline_color = _find_outline_color(it, p_outline_color); Color font_shadow_color = p_font_shadow_color; - if ((size <= 0 || font_color.a == 0) && (font_shadow_color.a == 0)) { + if ((size <= 0 || font_outline_color.a == 0) && (font_shadow_color.a == 0)) { gloff.x += glyphs[i].advance; continue; } @@ -865,17 +943,17 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } //Apply fx. - float faded_visibility = 1.0f; if (fade) { + float faded_visibility = 1.0f; 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; + font_outline_color.a = faded_visibility; font_shadow_color.a = faded_visibility; } - bool visible = (font_color.a != 0) || (font_shadow_color.a != 0); + bool visible = (font_outline_color.a != 0) || (font_shadow_color.a != 0); for (int j = 0; j < fx_stack.size(); j++) { ItemFX *item_fx = fx_stack[j]; @@ -945,19 +1023,23 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } // Draw glyph outlines. + const Color modulated_outline_color = font_outline_color * Color(1, 1, 1, font_color.a); + const Color modulated_shadow_color = font_shadow_color * Color(1, 1, 1, font_color.a); for (int j = 0; j < glyphs[i].repeat; j++) { if (visible) { - if (frid != RID()) { - if (font_shadow_color.a > 0) { - TS->font_draw_glyph(frid, ci, glyphs[i].font_size, p_ofs + fx_offset + gloff + p_shadow_ofs, gl, font_shadow_color); + bool skip = (trim_chars && l.char_offset + glyphs[i].end > visible_characters) || (trim_glyphs_ltr && (processed_glyphs_ol >= visible_glyphs)) || (trim_glyphs_rtl && (processed_glyphs_ol < total_glyphs - visible_glyphs)); + if (!skip && frid != RID()) { + if (modulated_shadow_color.a > 0) { + TS->font_draw_glyph(frid, ci, glyphs[i].font_size, p_ofs + fx_offset + gloff + p_shadow_ofs, gl, modulated_shadow_color); } - if (font_shadow_color.a > 0 && p_shadow_outline_size > 0) { - TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, p_shadow_outline_size, p_ofs + fx_offset + gloff + p_shadow_ofs, gl, font_shadow_color); + if (modulated_shadow_color.a > 0 && p_shadow_outline_size > 0) { + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, p_shadow_outline_size, p_ofs + fx_offset + gloff + p_shadow_ofs, gl, modulated_shadow_color); } - if (font_color.a != 0.0 && size > 0) { - TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff, gl, font_color); + if (modulated_outline_color.a != 0.0 && size > 0) { + TS->font_draw_glyph_outline(frid, ci, glyphs[i].font_size, size, p_ofs + fx_offset + gloff, gl, modulated_outline_color); } } + processed_glyphs_ol++; } gloff.x += glyphs[i].advance; } @@ -986,22 +1068,60 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } } + Vector2 ul_start; + bool ul_started = false; + Color ul_color; + + Vector2 dot_ul_start; + bool dot_ul_started = false; + Color dot_ul_color; + + Vector2 st_start; + bool st_started = false; + Color st_color; + 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; + if (_find_underline(it) || (_find_meta(it, nullptr) && underline_meta)) { + if (!ul_started) { + ul_started = true; + ul_start = p_ofs + Vector2(off.x, off.y); + ul_color = font_color; + ul_color.a *= 0.5; + } + } else if (ul_started) { + ul_started = false; + float y_off = TS->shaped_text_get_underline_position(rid); + float underline_width = TS->shaped_text_get_underline_thickness(rid) * get_theme_default_base_scale(); + draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), ul_color, underline_width); + } + if (_find_hint(it, nullptr) && underline_hint) { + if (!dot_ul_started) { + dot_ul_started = true; + dot_ul_start = p_ofs + Vector2(off.x, off.y); + dot_ul_color = font_color; + dot_ul_color.a *= 0.5; + } + } else if (dot_ul_started) { + dot_ul_started = false; float y_off = TS->shaped_text_get_underline_position(rid); float underline_width = TS->shaped_text_get_underline_thickness(rid) * get_theme_default_base_scale(); - 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; + draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), dot_ul_color, underline_width, underline_width * 2); + } + if (_find_strikethrough(it)) { + if (!st_started) { + st_started = true; + st_start = p_ofs + Vector2(off.x, off.y); + st_color = font_color; + st_color.a *= 0.5; + } + } else if (st_started) { + st_started = false; 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) * get_theme_default_base_scale(); - 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); + draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), st_color, underline_width); } // Get FX. @@ -1039,8 +1159,8 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o } //Apply fx. - float faded_visibility = 1.0f; if (fade) { + float faded_visibility = 1.0f; 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; @@ -1124,15 +1244,37 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o // 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); + bool skip = (trim_chars && l.char_offset + glyphs[i].end > visible_characters) || (trim_glyphs_ltr && (r_processed_glyphs >= visible_glyphs)) || (trim_glyphs_rtl && (r_processed_glyphs < total_glyphs - visible_glyphs)); + if (!skip) { + 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); + } } + r_processed_glyphs++; } off.x += glyphs[i].advance; } } + if (ul_started) { + ul_started = false; + float y_off = TS->shaped_text_get_underline_position(rid); + float underline_width = TS->shaped_text_get_underline_thickness(rid) * get_theme_default_base_scale(); + draw_line(ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), ul_color, underline_width); + } + if (dot_ul_started) { + dot_ul_started = false; + float y_off = TS->shaped_text_get_underline_position(rid); + float underline_width = TS->shaped_text_get_underline_thickness(rid) * get_theme_default_base_scale(); + draw_dashed_line(dot_ul_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), dot_ul_color, underline_width, underline_width * 2); + } + if (st_started) { + st_started = false; + 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) * get_theme_default_base_scale(); + draw_line(st_start + Vector2(0, y_off), p_ofs + Vector2(off.x, off.y + y_off), st_color, underline_width); + } // Draw foreground color box _draw_fbg_boxes(ci, rid, fbg_line_off, it_from, it_to, chr_range.x, chr_range.y, 1); @@ -1159,24 +1301,14 @@ void RichTextLabel::_find_click(ItemFrame *p_frame, const Point2i &p_click, Item int vofs = vscroll->get_value(); // Search for the first line. - int from_line = 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++; - } - - if (from_line >= main->lines.size()) { - return; - } + int to_line = main->first_invalid_line.load(); + int from_line = _find_first_line(0, to_line, vofs); 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()) { + while (ofs.y < size.height && from_line < to_line) { + MutexLock lock(main->lines[from_line].text_buf->get_mutex()); _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")); + ofs.y += main->lines[from_line].text_buf->get_size().y + main->lines[from_line].text_buf->get_line_count() * 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; @@ -1187,14 +1319,24 @@ void RichTextLabel::_find_click(ItemFrame *p_frame, const Point2i &p_click, Item } } -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) { +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, bool p_table) { Vector2 off; int char_pos = -1; - Line &l = p_frame->lines.write[p_line]; + Line &l = p_frame->lines[p_line]; + MutexLock lock(l.text_buf->get_mutex()); + bool rtl = (l.text_buf->get_direction() == TextServer::DIRECTION_RTL); bool lrtl = is_layout_rtl(); + + // Table hit test results. bool table_hit = false; + Vector2i table_range; + float table_offy = 0.f; + ItemFrame *table_click_frame = nullptr; + int table_click_line = -1; + Item *table_click_item = nullptr; + int table_click_char = -1; for (int line = 0; line < l.text_buf->get_line_count(); line++) { RID rid = l.text_buf->get_line_rid(line); @@ -1235,19 +1377,18 @@ float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const V Array objects = TS->shaped_text_get_objects(rid); for (int i = 0; i < objects.size(); i++) { - Item *it = (Item *)(uint64_t)objects[i]; + Item *it = reinterpret_cast<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)) { + rect.position += p_ofs + off; + if (p_click.y >= rect.position.y && p_click.y <= rect.position.y + rect.size.y) { switch (it->type) { case ITEM_TABLE: { - int hseparation = get_theme_constant(SNAME("table_hseparation")); - int vseparation = get_theme_constant(SNAME("table_vseparation")); + int hseparation = get_theme_constant(SNAME("table_h_separation")); + int vseparation = get_theme_constant(SNAME("table_v_separation")); ItemTable *table = static_cast<ItemTable *>(it); - table_hit = true; - int idx = 0; int col_count = table->columns.size(); int row_count = table->rows.size(); @@ -1263,7 +1404,7 @@ float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const V 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); + Rect2 crect = Rect2(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; @@ -1273,10 +1414,20 @@ float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const V } } 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; + for (int j = 0; j < (int)frame->lines.size(); j++) { + _find_click_in_line(frame, j, rect.position + Vector2(0, frame->lines[j].offset.y), rect.size.x, p_click, &table_click_frame, &table_click_line, &table_click_item, &table_click_char, true); + if (table_click_frame && table_click_item) { + // Save cell detected cell hit data. + table_range = Vector2i(INT32_MAX, 0); + for (Item *F : table->subitems) { + ItemFrame *sub_frame = static_cast<ItemFrame *>(F); + for (int k = 0; k < (int)sub_frame->lines.size(); k++) { + table_range.x = MIN(table_range.x, sub_frame->lines[k].char_offset); + table_range.y = MAX(table_range.y, sub_frame->lines[k].char_offset + sub_frame->lines[k].char_count); + } + } + table_offy = off.y; + table_hit = true; } } } @@ -1290,25 +1441,50 @@ float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const V } } } - 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)); + Rect2 rect = Rect2(p_ofs + off - Vector2(0, TS->shaped_text_get_ascent(rid)) - p_frame->padding.position, TS->shaped_text_get_size(rid) + p_frame->padding.position + p_frame->padding.size); + if (p_table) { + rect.size.y += get_theme_constant(SNAME("table_v_separation")); + } - if (rect.has_point(p_click) && !table_hit) { + if (p_click.y >= rect.position.y && p_click.y <= rect.position.y + rect.size.y) { 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(); + + // If table hit was detected, and line hit is in the table bounds use table hit. + if (table_hit && (((char_pos + p_frame->lines[p_line].char_offset) >= table_range.x && (char_pos + p_frame->lines[p_line].char_offset) <= table_range.y) || char_pos == -1)) { + if (r_click_frame != nullptr) { + *r_click_frame = table_click_frame; + } + + if (r_click_line != nullptr) { + *r_click_line = table_click_line; + } + + if (r_click_item != nullptr) { + *r_click_item = table_click_item; + } + + if (r_click_char != nullptr) { + *r_click_char = table_click_char; + } + return table_offy; + } + + off.y += TS->shaped_text_get_descent(rid) + l.text_buf->get_spacing_bottom() + get_theme_constant(SNAME("line_separation")); } + // Text line hit. 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; + Item *it_to = (p_line + 1 < (int)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)) { + for (Item *i = it; i; i = _get_next_item(i)) { *r_click_item = i; } } @@ -1350,28 +1526,6 @@ void RichTextLabel::_scroll_changed(double) { update(); } -void RichTextLabel::_update_scroll() { - int total_height = get_content_height(); - - bool exceeds = total_height > get_size().height && scroll_active; - - if (exceeds != scroll_visible) { - if (exceeds) { - scroll_visible = true; - scroll_w = vscroll->get_combined_minimum_size().width; - vscroll->show(); - vscroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -scroll_w); - } else { - scroll_visible = false; - scroll_w = 0; - vscroll->hide(); - } - - main->first_resized_line = 0; //invalidate ALL - _validate_line_caches(main); - } -} - void RichTextLabel::_update_fx(RichTextLabel::ItemFrame *p_frame, double p_delta_time) { Item *it = p_frame; while (it) { @@ -1406,6 +1560,26 @@ void RichTextLabel::_update_fx(RichTextLabel::ItemFrame *p_frame, double p_delta } } +int RichTextLabel::_find_first_line(int p_from, int p_to, int p_vofs) const { + int l = p_from; + int r = p_to; + while (l < r) { + int m = Math::floor(double(l + r) / 2.0); + MutexLock lock(main->lines[m].text_buf->get_mutex()); + int ofs = _calculate_line_vertical_offset(main->lines[m]); + if (ofs < p_vofs) { + l = m + 1; + } else { + r = m; + } + } + return l; +} + +_FORCE_INLINE_ float RichTextLabel::_calculate_line_vertical_offset(const RichTextLabel::Line &line) const { + return line.get_height(get_theme_constant(SNAME("line_separation"))); +} + void RichTextLabel::_notification(int p_what) { switch (p_what) { case NOTIFICATION_MOUSE_EXIT: { @@ -1416,33 +1590,48 @@ void RichTextLabel::_notification(int p_what) { update(); } } break; + case NOTIFICATION_RESIZED: { - main->first_resized_line = 0; //invalidate ALL + _stop_thread(); + main->first_resized_line.store(0); //invalidate ALL update(); + } break; + case NOTIFICATION_THEME_CHANGED: { + _stop_thread(); + main->first_invalid_font_line.store(0); //invalidate ALL + update(); } break; - case NOTIFICATION_THEME_CHANGED: + case NOTIFICATION_ENTER_TREE: { + _stop_thread(); if (!text.is_empty()) { set_text(text); } - main->first_invalid_line = 0; //invalidate ALL + main->first_invalid_line.store(0); //invalidate ALL update(); } break; + + case NOTIFICATION_PREDELETE: + case NOTIFICATION_EXIT_TREE: { + _stop_thread(); + } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: { - main->first_invalid_line = 0; //invalidate ALL + _stop_thread(); + main->first_invalid_line.store(0); //invalidate ALL update(); } break; - case NOTIFICATION_DRAW: { - _validate_line_caches(main); - _update_scroll(); - RID ci = get_canvas_item(); + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + update(); + } break; + case NOTIFICATION_DRAW: { + RID ci = get_canvas_item(); Size2 size = get_size(); - Rect2 text_rect = _get_text_rect(); draw_style_box(get_theme_stylebox(SNAME("normal")), Rect2(Point2(), size)); @@ -1452,22 +1641,42 @@ void RichTextLabel::_notification(int p_what) { RenderingServer::get_singleton()->canvas_item_add_clip_ignore(ci, false); } + // Start text shaping. + if (_validate_line_caches()) { + set_physics_process_internal(false); // Disable auto refresh, if text is fully processed. + } else { + // Draw loading progress bar. + if ((progress_delay > 0) && (OS::get_singleton()->get_ticks_msec() - loading_started >= (uint64_t)progress_delay)) { + Ref<StyleBox> bg = get_theme_stylebox(SNAME("bg"), SNAME("ProgressBar")); + Ref<StyleBox> fg = get_theme_stylebox(SNAME("fg"), SNAME("ProgressBar")); + Ref<StyleBox> style = get_theme_stylebox(SNAME("normal")); + + Vector2 p_size = Vector2(size.width - (style->get_offset().x + vscroll->get_combined_minimum_size().width) * 2, vscroll->get_combined_minimum_size().width); + Vector2 p_pos = Vector2(style->get_offset().x, size.height - style->get_offset().y - vscroll->get_combined_minimum_size().width); + + draw_style_box(bg, Rect2(p_pos, p_size)); + + bool right_to_left = is_layout_rtl(); + double r = loaded.load(); + int mp = fg->get_minimum_size().width; + int p = round(r * (p_size.width - mp)); + if (right_to_left) { + int p_remaining = round((1.0 - r) * (p_size.width - mp)); + draw_style_box(fg, Rect2(p_pos + Point2(p_remaining, 0), Size2(p + fg->get_minimum_size().width, p_size.height))); + } else { + draw_style_box(fg, Rect2(p_pos, Size2(p + fg->get_minimum_size().width, p_size.height))); + } + } + } + + // Draw main text. + Rect2 text_rect = _get_text_rect(); float vofs = vscroll->get_value(); // Search for the first line. - int from_line = 0; + int to_line = main->first_invalid_line.load(); + int from_line = _find_first_line(0, to_line, vofs); - //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++; - } - - if (from_line >= main->lines.size()) { - break; //nothing to draw - } 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")); @@ -1481,26 +1690,37 @@ void RichTextLabel::_notification(int p_what) { // 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()) { + int processed_glyphs = 0; + while (ofs.y < size.height && from_line < to_line) { + MutexLock lock(main->lines[from_line].text_buf->get_mutex()); + 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, shadow_outline_size, shadow_ofs); - ofs.y += main->lines[from_line].text_buf->get_size().y + get_theme_constant(SNAME("line_separation")); + visible_line_count += _draw_line(main, from_line, ofs, text_rect.size.x, base_color, outline_size, outline_color, font_shadow_color, shadow_outline_size, shadow_ofs, processed_glyphs); + ofs.y += main->lines[from_line].text_buf->get_size().y + main->lines[from_line].text_buf->get_line_count() * get_theme_constant(SNAME("line_separation")); from_line++; } } break; + case NOTIFICATION_INTERNAL_PROCESS: { if (is_visible_in_tree()) { + if (!is_ready()) { + return; + } double dt = get_process_delta_time(); _update_fx(main, dt); update(); } } break; + case NOTIFICATION_FOCUS_EXIT: { if (deselect_on_focus_loss_enabled) { - selection.active = false; - update(); + deselect(); } } break; + + case NOTIFICATION_DRAG_END: { + selection.drag_attempt = false; + } break; } } @@ -1513,22 +1733,13 @@ Control::CursorShape RichTextLabel::get_cursor_shape(const Point2 &p_pos) const return CURSOR_IBEAM; } - if (main->first_invalid_line < main->lines.size()) { - return get_default_cursor_shape(); //invalid - } - - if (main->first_resized_line < main->lines.size()) { - return get_default_cursor_shape(); //invalid - } - Item *item = nullptr; bool outside = true; - ((RichTextLabel *)(this))->_find_click(main, p_pos, nullptr, nullptr, &item, nullptr, &outside); + const_cast<RichTextLabel *>(this)->_find_click(main, p_pos, nullptr, nullptr, &item, nullptr, &outside); - if (item && !outside && ((RichTextLabel *)(this))->_find_meta(item, nullptr)) { + if (item && !outside && const_cast<RichTextLabel *>(this)->_find_meta(item, nullptr)) { return CURSOR_POINTING_HAND; } - return get_default_cursor_shape(); } @@ -1538,13 +1749,6 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { 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() == MouseButton::LEFT) { if (b->is_pressed() && !b->is_double_click()) { scroll_updated = false; @@ -1554,6 +1758,8 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { int c_index = 0; bool outside; + selection.drag_attempt = false; + _find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside); if (c_item != nullptr) { if (selection.enabled) { @@ -1564,17 +1770,20 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { // Erase previous selection. if (selection.active) { - 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(); + if (_is_click_inside_selection()) { + selection.drag_attempt = true; + selection.click_item = nullptr; + } else { + 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; + deselect(); + } } } } @@ -1587,10 +1796,13 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { int c_index = 0; bool outside; + selection.drag_attempt = false; + _find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside); if (c_frame) { const Line &l = c_frame->lines[c_line]; + MutexLock lock(l.text_buf->get_mutex()); PackedInt32Array words = TS->shaped_text_get_word_breaks(l.text_buf->get_rid()); for (int i = 0; i < words.size(); i = i + 2) { if (c_index >= words[i] && c_index < words[i + 1]) { @@ -1618,8 +1830,21 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text()); } selection.click_item = nullptr; - - if (!b->is_double_click() && !scroll_updated) { + if (selection.drag_attempt) { + selection.drag_attempt = false; + if (_is_click_inside_selection()) { + 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; + deselect(); + } + } + if (!b->is_double_click() && !scroll_updated && !selection.active) { Item *c_item = nullptr; bool outside = true; @@ -1646,6 +1871,13 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { vscroll->set_value(vscroll->get_value() + vscroll->get_page() * b->get_factor() * 0.5 / 8); } } + if (b->get_button_index() == MouseButton::RIGHT && context_menu_enabled) { + _generate_context_menu(); + menu->set_position(get_screen_position() + b->get_position()); + menu->reset_size(); + menu->popup(); + grab_focus(); + } } Ref<InputEventPanGesture> pan_gesture = p_event; @@ -1687,8 +1919,24 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { vscroll->set_value(vscroll->get_max()); handled = true; } - if (k->is_action("ui_copy")) { - selection_copy(); + if (is_shortcut_keys_enabled()) { + if (k->is_action("ui_text_select_all")) { + select_all(); + handled = true; + } + if (k->is_action("ui_copy")) { + selection_copy(); + handled = true; + } + } + if (k->is_action("ui_menu", true)) { + if (context_menu_enabled) { + _generate_context_menu(); + menu->set_position(get_screen_position()); + menu->reset_size(); + menu->popup(); + menu->grab_focus(); + } handled = true; } @@ -1699,15 +1947,7 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { } Ref<InputEventMouseMotion> m = p_event; - if (m.is_valid()) { - if (main->first_invalid_line < main->lines.size()) { - return; - } - if (main->first_resized_line < main->lines.size()) { - return; - } - ItemFrame *c_frame = nullptr; int c_line = 0; Item *c_item = nullptr; @@ -1727,14 +1967,13 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { selection.to_char = c_index; bool swap = false; - if (selection.from_item->index > selection.to_item->index) { - swap = true; - } else if (selection.from_item->index == selection.to_item->index) { - if (selection.from_char > selection.to_char) { + if (selection.click_frame && c_frame) { + const Line &l1 = c_frame->lines[c_line]; + const Line &l2 = selection.click_frame->lines[selection.click_line]; + if (l1.char_offset + c_index < l2.char_offset + selection.click_char) { swap = true; - } else if (selection.from_char == selection.to_char) { - selection.active = false; - update(); + } else if (l1.char_offset + c_index == l2.char_offset + selection.click_char) { + deselect(); return; } } @@ -1769,6 +2008,20 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) { } } +String RichTextLabel::get_tooltip(const Point2 &p_pos) const { + Item *c_item = nullptr; + bool outside; + + const_cast<RichTextLabel *>(this)->_find_click(main, p_pos, nullptr, nullptr, &c_item, nullptr, &outside); + + String description; + if (c_item && !outside && const_cast<RichTextLabel *>(this)->_find_hint(c_item, &description)) { + return description; + } else { + return Control::get_tooltip(p_pos); + } +} + void RichTextLabel::_find_frame(Item *p_item, ItemFrame **r_frame, int *r_line) { if (r_frame != nullptr) { *r_frame = nullptr; @@ -1782,7 +2035,7 @@ void RichTextLabel::_find_frame(Item *p_item, ItemFrame **r_frame, int *r_line) while (item) { if (item->parent != nullptr && item->parent->type == ITEM_FRAME) { if (r_frame != nullptr) { - *r_frame = (ItemFrame *)item->parent; + *r_frame = static_cast<ItemFrame *>(item->parent); } if (r_line != nullptr) { *r_line = item->line; @@ -1986,7 +2239,7 @@ TextServer::Direction RichTextLabel::_find_direction(Item *p_item) { } } -Control::StructuredTextParser RichTextLabel::_find_stt(Item *p_item) { +TextServer::StructuredTextParser RichTextLabel::_find_stt(Item *p_item) { Item *item = p_item; while (item) { @@ -2106,6 +2359,24 @@ bool RichTextLabel::_find_meta(Item *p_item, Variant *r_meta, ItemMeta **r_item) return false; } +bool RichTextLabel::_find_hint(Item *p_item, String *r_description) { + Item *item = p_item; + + while (item) { + if (item->type == ITEM_HINT) { + ItemHint *hint = static_cast<ItemHint *>(item); + if (r_description) { + *r_description = hint->description; + } + return true; + } + + item = item->parent; + } + + return false; +} + Color RichTextLabel::_find_bgcolor(Item *p_item) { Item *item = p_item; @@ -2154,91 +2425,218 @@ bool RichTextLabel::_find_layout_subitem(Item *from, Item *to) { return false; } -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; - } +void RichTextLabel::_thread_function(void *self) { + RichTextLabel *rtl = reinterpret_cast<RichTextLabel *>(self); + rtl->_process_line_caches(); + rtl->updating.store(false); + rtl->call_deferred(SNAME("update")); +} - // Resize lines without reshaping. - Size2 size = get_size(); - if (fixed_width != -1) { - size.width = fixed_width; - } +void RichTextLabel::_stop_thread() { + if (threaded) { + stop_thread.store(true); + thread.wait_to_finish(); + } +} + +bool RichTextLabel::is_ready() const { + if (updating.load()) { + return false; + } + return (main->first_invalid_line.load() == (int)main->lines.size() && main->first_resized_line.load() == (int)main->lines.size() && main->first_invalid_font_line.load() == (int)main->lines.size()); +} + +void RichTextLabel::set_threaded(bool p_threaded) { + if (threaded != p_threaded) { + _stop_thread(); + threaded = p_threaded; + update(); + } +} + +bool RichTextLabel::is_threaded() const { + return threaded; +} + +void RichTextLabel::set_progress_bar_delay(int p_delay_ms) { + progress_delay = p_delay_ms; +} + +int RichTextLabel::get_progress_bar_delay() const { + return progress_delay; +} + +bool RichTextLabel::_validate_line_caches() { + if (updating.load()) { + return false; + } + if (main->first_invalid_line.load() == (int)main->lines.size()) { + MutexLock data_lock(data_mutex); 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")); + int ctrl_height = get_size().height; - 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); + // Update fonts. + if (main->first_invalid_font_line.load() != (int)main->lines.size()) { + for (int i = main->first_invalid_font_line.load(); i < (int)main->lines.size(); i++) { + _update_line_font(main, i, base_font, base_font_size); + } + main->first_resized_line.store(main->first_invalid_font_line.load()); + main->first_invalid_font_line.store(main->lines.size()); } - 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; + if (main->first_resized_line.load() == (int)main->lines.size()) { + return true; } - p_frame->first_resized_line = p_frame->lines.size(); + // Resize lines without reshaping. + int fi = main->first_resized_line.load(); + + float total_height = (fi == 0) ? 0 : _calculate_line_vertical_offset(main->lines[fi - 1]); + for (int i = fi; i < (int)main->lines.size(); i++) { + total_height = _resize_line(main, i, base_font, base_font_size, text_rect.get_size().width - scroll_w, total_height); + + updating_scroll = true; + bool exceeds = total_height > ctrl_height && scroll_active; + if (exceeds != scroll_visible) { + if (exceeds) { + scroll_visible = true; + scroll_w = vscroll->get_combined_minimum_size().width; + vscroll->show(); + vscroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -scroll_w); + } else { + scroll_visible = false; + scroll_w = 0; + vscroll->hide(); + } - 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); + main->first_resized_line.store(0); + + total_height = 0; + for (int j = 0; j <= i; j++) { + total_height = _resize_line(main, j, base_font, base_font_size, text_rect.get_size().width - scroll_w, total_height); + + main->first_resized_line.store(j); + } + } + + vscroll->set_max(total_height); + vscroll->set_page(text_rect.size.height); + if (scroll_follow && scroll_following) { + vscroll->set_value(total_height); + } + updating_scroll = false; + + main->first_resized_line.store(i); } - updating_scroll = false; + + main->first_resized_line.store(main->lines.size()); if (fit_content_height) { update_minimum_size(); } - return; + return true; + } + stop_thread.store(false); + if (threaded) { + updating.store(true); + loaded.store(true); + thread.start(RichTextLabel::_thread_function, reinterpret_cast<void *>(this)); + loading_started = OS::get_singleton()->get_ticks_msec(); + set_physics_process_internal(true); + return false; + } else { + _process_line_caches(); + update(); + return true; } +} +void RichTextLabel::_process_line_caches() { // Shape invalid lines. - Size2 size = get_size(); - if (fixed_width != -1) { - size.width = fixed_width; + if (!is_inside_tree()) { + return; } + + MutexLock data_lock(data_mutex); 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")); + int ctrl_height = get_size().height; + int fi = main->first_invalid_line.load(); + int total_chars = (fi == 0) ? 0 : (main->lines[fi].char_offset + main->lines[fi].char_count); - 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++) { - _shape_line(p_frame, i, base_font, base_font_size, text_rect.get_size().width - scroll_w, &total_chars); - } + float total_height = (fi == 0) ? 0 : _calculate_line_vertical_offset(main->lines[fi - 1]); + for (int i = fi; i < (int)main->lines.size(); i++) { + total_height = _shape_line(main, i, base_font, base_font_size, text_rect.get_size().width - scroll_w, total_height, &total_chars); + updating_scroll = true; + bool exceeds = total_height > ctrl_height && scroll_active; + if (exceeds != scroll_visible) { + if (exceeds) { + scroll_visible = true; + scroll_w = vscroll->get_combined_minimum_size().width; + vscroll->show(); + vscroll->set_anchor_and_offset(SIDE_LEFT, ANCHOR_END, -scroll_w); + } else { + scroll_visible = false; + scroll_w = 0; + vscroll->hide(); + } + main->first_invalid_line.store(0); + main->first_resized_line.store(0); + main->first_invalid_font_line.store(0); + + // since scroll was added or removed we need to resize all lines + total_height = 0; + for (int j = 0; j <= i; j++) { + total_height = _resize_line(main, j, base_font, base_font_size, text_rect.get_size().width - scroll_w, total_height); + + main->first_invalid_line.store(j); + main->first_resized_line.store(j); + main->first_invalid_font_line.store(j); + } + } - 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; - } + vscroll->set_max(total_height); + vscroll->set_page(text_rect.size.height); + if (scroll_follow && scroll_following) { + vscroll->set_value(total_height); + } + updating_scroll = false; - p_frame->first_invalid_line = p_frame->lines.size(); - p_frame->first_resized_line = p_frame->lines.size(); + main->first_invalid_line.store(i); + main->first_resized_line.store(i); + main->first_invalid_font_line.store(i); - 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); + if (stop_thread.load()) { + return; + } + loaded.store(double(i) / double(main->lines.size())); } - updating_scroll = false; + + main->first_invalid_line.store(main->lines.size()); + main->first_resized_line.store(main->lines.size()); + main->first_invalid_font_line.store(main->lines.size()); if (fit_content_height) { update_minimum_size(); } + emit_signal(SNAME("finished")); } void RichTextLabel::_invalidate_current_line(ItemFrame *p_frame) { - if (p_frame->lines.size() - 1 <= p_frame->first_invalid_line) { - p_frame->first_invalid_line = p_frame->lines.size() - 1; - update(); + if ((int)p_frame->lines.size() - 1 <= p_frame->first_invalid_line) { + p_frame->first_invalid_line = (int)p_frame->lines.size() - 1; } } void RichTextLabel::add_text(const String &p_text) { + _stop_thread(); + MutexLock data_lock(data_mutex); + if (current->type == ITEM_TABLE) { return; //can't add anything here } @@ -2282,13 +2680,14 @@ void RichTextLabel::add_text(const String &p_text) { _add_item(item, false); current_frame->lines.resize(current_frame->lines.size() + 1); if (item->type != ITEM_NEWLINE) { - current_frame->lines.write[current_frame->lines.size() - 1].from = item; + current_frame->lines[current_frame->lines.size() - 1].from = item; } _invalidate_current_line(current_frame); } pos = end + 1; } + update(); } void RichTextLabel::_add_item(Item *p_item, bool p_enter, bool p_ensure_newline) { @@ -2297,7 +2696,7 @@ void RichTextLabel::_add_item(Item *p_item, bool p_enter, bool p_ensure_newline) p_item->index = current_idx++; p_item->char_ofs = current_char_ofs; if (p_item->type == ITEM_TEXT) { - ItemText *t = (ItemText *)p_item; + ItemText *t = static_cast<ItemText *>(p_item); current_char_ofs += t->text.length(); } else if (p_item->type == ITEM_IMAGE) { current_char_ofs++; @@ -2317,7 +2716,7 @@ void RichTextLabel::_add_item(Item *p_item, bool p_enter, bool p_ensure_newline) } if (current_frame->lines[current_frame->lines.size() - 1].from == nullptr) { - current_frame->lines.write[current_frame->lines.size() - 1].from = p_item; + current_frame->lines[current_frame->lines.size() - 1].from = p_item; } p_item->line = current_frame->lines.size() - 1; @@ -2326,6 +2725,7 @@ void RichTextLabel::_add_item(Item *p_item, bool p_enter, bool p_ensure_newline) if (fixed_width != -1) { update_minimum_size(); } + update(); } void RichTextLabel::_remove_item(Item *p_item, const int p_line, const int p_subitem_line) { @@ -2349,9 +2749,13 @@ void RichTextLabel::_remove_item(Item *p_item, const int p_line, const int p_sub // Then remove the provided item itself. p_item->parent->subitems.erase(p_item); } + memdelete(p_item); } void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, const int p_height, const Color &p_color, InlineAlignment p_alignment) { + _stop_thread(); + MutexLock data_lock(data_mutex); + if (current->type == ITEM_TABLE) { return; } @@ -2391,6 +2795,9 @@ void RichTextLabel::add_image(const Ref<Texture2D> &p_image, const int p_width, } void RichTextLabel::add_newline() { + _stop_thread(); + MutexLock data_lock(data_mutex); + if (current->type == ITEM_TABLE) { return; } @@ -2399,10 +2806,14 @@ void RichTextLabel::add_newline() { _add_item(item, false); current_frame->lines.resize(current_frame->lines.size() + 1); _invalidate_current_line(current_frame); + update(); } bool RichTextLabel::remove_line(const int p_line) { - if (p_line >= current_frame->lines.size() || p_line < 0) { + _stop_thread(); + MutexLock data_lock(data_mutex); + + if (p_line >= (int)current_frame->lines.size() || p_line < 0) { return false; } @@ -2430,16 +2841,19 @@ bool RichTextLabel::remove_line(const int p_line) { } if (p_line == 0 && current->subitems.size() > 0) { - main->lines.write[0].from = main; + main->lines[0].from = main; } - main->first_invalid_line = 0; // p_line ??? + main->first_invalid_line.store(0); 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) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ERR_FAIL_COND(p_string.is_empty()); ERR_FAIL_COND(p_font.is_null()); @@ -2458,6 +2872,9 @@ void RichTextLabel::push_dropcap(const String &p_string, const Ref<Font> &p_font } void RichTextLabel::push_font(const Ref<Font> &p_font) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ERR_FAIL_COND(p_font.is_null()); ItemFont *item = memnew(ItemFont); @@ -2502,6 +2919,9 @@ void RichTextLabel::push_mono() { } void RichTextLabel::push_font_size(int p_font_size) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemFontSize *item = memnew(ItemFontSize); @@ -2510,6 +2930,9 @@ void RichTextLabel::push_font_size(int p_font_size) { } void RichTextLabel::push_font_features(const Dictionary &p_features) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemFontFeatures *item = memnew(ItemFontFeatures); @@ -2518,6 +2941,9 @@ void RichTextLabel::push_font_features(const Dictionary &p_features) { } void RichTextLabel::push_outline_size(int p_font_size) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemOutlineSize *item = memnew(ItemOutlineSize); @@ -2526,6 +2952,9 @@ void RichTextLabel::push_outline_size(int p_font_size) { } void RichTextLabel::push_color(const Color &p_color) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemColor *item = memnew(ItemColor); @@ -2534,6 +2963,9 @@ void RichTextLabel::push_color(const Color &p_color) { } void RichTextLabel::push_outline_color(const Color &p_color) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemOutlineColor *item = memnew(ItemOutlineColor); @@ -2542,6 +2974,9 @@ void RichTextLabel::push_outline_color(const Color &p_color) { } void RichTextLabel::push_underline() { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemUnderline *item = memnew(ItemUnderline); @@ -2549,13 +2984,19 @@ void RichTextLabel::push_underline() { } void RichTextLabel::push_strikethrough() { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemStrikethrough *item = memnew(ItemStrikethrough); _add_item(item, true); } -void RichTextLabel::push_paragraph(HorizontalAlignment p_alignment, Control::TextDirection p_direction, const String &p_language, Control::StructuredTextParser p_st_parser) { +void RichTextLabel::push_paragraph(HorizontalAlignment p_alignment, Control::TextDirection p_direction, const String &p_language, TextServer::StructuredTextParser p_st_parser) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemParagraph *item = memnew(ItemParagraph); @@ -2567,6 +3008,9 @@ void RichTextLabel::push_paragraph(HorizontalAlignment p_alignment, Control::Tex } void RichTextLabel::push_indent(int p_level) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ERR_FAIL_COND(p_level < 0); @@ -2576,6 +3020,9 @@ void RichTextLabel::push_indent(int p_level) { } void RichTextLabel::push_list(int p_level, ListType p_list, bool p_capitalize) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ERR_FAIL_COND(p_level < 0); @@ -2588,6 +3035,9 @@ void RichTextLabel::push_list(int p_level, ListType p_list, bool p_capitalize) { } void RichTextLabel::push_meta(const Variant &p_meta) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemMeta *item = memnew(ItemMeta); @@ -2595,21 +3045,38 @@ void RichTextLabel::push_meta(const Variant &p_meta) { _add_item(item, true); } +void RichTextLabel::push_hint(const String &p_string) { + _stop_thread(); + MutexLock data_lock(data_mutex); + + ERR_FAIL_COND(current->type == ITEM_TABLE); + ItemHint *item = memnew(ItemHint); + + item->description = p_string; + _add_item(item, true); +} + void RichTextLabel::push_table(int p_columns, InlineAlignment p_alignment) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(p_columns < 1); ItemTable *item = memnew(ItemTable); item->columns.resize(p_columns); item->total_width = 0; item->inline_align = p_alignment; - for (int i = 0; i < item->columns.size(); i++) { - item->columns.write[i].expand = false; - item->columns.write[i].expand_ratio = 1; + for (int i = 0; i < (int)item->columns.size(); i++) { + item->columns[i].expand = false; + item->columns[i].expand_ratio = 1; } _add_item(item, true, false); } void RichTextLabel::push_fade(int p_start_index, int p_length) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ItemFade *item = memnew(ItemFade); item->starting_index = p_start_index; item->length = p_length; @@ -2617,6 +3084,9 @@ void RichTextLabel::push_fade(int p_start_index, int p_length) { } void RichTextLabel::push_shake(int p_strength = 10, float p_rate = 24.0f) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ItemShake *item = memnew(ItemShake); item->strength = p_strength; item->rate = p_rate; @@ -2624,6 +3094,9 @@ void RichTextLabel::push_shake(int p_strength = 10, float p_rate = 24.0f) { } void RichTextLabel::push_wave(float p_frequency = 1.0f, float p_amplitude = 10.0f) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ItemWave *item = memnew(ItemWave); item->frequency = p_frequency; item->amplitude = p_amplitude; @@ -2631,6 +3104,9 @@ void RichTextLabel::push_wave(float p_frequency = 1.0f, float p_amplitude = 10.0 } void RichTextLabel::push_tornado(float p_frequency = 1.0f, float p_radius = 10.0f) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ItemTornado *item = memnew(ItemTornado); item->frequency = p_frequency; item->radius = p_radius; @@ -2638,6 +3114,9 @@ void RichTextLabel::push_tornado(float p_frequency = 1.0f, float p_radius = 10.0 } void RichTextLabel::push_rainbow(float p_saturation, float p_value, float p_frequency) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ItemRainbow *item = memnew(ItemRainbow); item->frequency = p_frequency; item->saturation = p_saturation; @@ -2646,6 +3125,9 @@ void RichTextLabel::push_rainbow(float p_saturation, float p_value, float p_freq } void RichTextLabel::push_bgcolor(const Color &p_color) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemBGColor *item = memnew(ItemBGColor); @@ -2654,6 +3136,9 @@ void RichTextLabel::push_bgcolor(const Color &p_color) { } void RichTextLabel::push_fgcolor(const Color &p_color) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type == ITEM_TABLE); ItemFGColor *item = memnew(ItemFGColor); @@ -2662,6 +3147,9 @@ void RichTextLabel::push_fgcolor(const Color &p_color) { } void RichTextLabel::push_customfx(Ref<RichTextEffect> p_custom_effect, Dictionary p_environment) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ItemCustomFX *item = memnew(ItemCustomFX); item->custom_effect = p_custom_effect; item->char_fx_transform->environment = p_environment; @@ -2669,15 +3157,23 @@ void RichTextLabel::push_customfx(Ref<RichTextEffect> p_custom_effect, Dictionar } void RichTextLabel::set_table_column_expand(int p_column, bool p_expand, int p_ratio) { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type != ITEM_TABLE); + ItemTable *table = static_cast<ItemTable *>(current); - ERR_FAIL_INDEX(p_column, table->columns.size()); - table->columns.write[p_column].expand = p_expand; - table->columns.write[p_column].expand_ratio = p_ratio; + ERR_FAIL_INDEX(p_column, (int)table->columns.size()); + table->columns[p_column].expand = p_expand; + table->columns[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) { + _stop_thread(); + MutexLock data_lock(data_mutex); + 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; @@ -2685,14 +3181,22 @@ void RichTextLabel::set_cell_row_background_color(const Color &p_odd_row_bg, con } void RichTextLabel::set_cell_border_color(const Color &p_color) { + _stop_thread(); + MutexLock data_lock(data_mutex); + 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) { + _stop_thread(); + MutexLock data_lock(data_mutex); + 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; @@ -2700,13 +3204,20 @@ void RichTextLabel::set_cell_size_override(const Size2 &p_min_size, const Size2 } void RichTextLabel::set_cell_padding(const Rect2 &p_padding) { + _stop_thread(); + MutexLock data_lock(data_mutex); + 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() { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(current->type != ITEM_TABLE); ItemFrame *item = memnew(ItemFrame); @@ -2715,20 +3226,23 @@ void RichTextLabel::push_cell() { current_frame = item; item->cell = true; item->lines.resize(1); - item->lines.write[0].from = nullptr; - item->first_invalid_line = 0; // parent frame last line ??? + item->lines[0].from = nullptr; + item->first_invalid_line.store(0); // parent frame last line ??? } int RichTextLabel::get_current_table_column() const { ERR_FAIL_COND_V(current->type != ITEM_TABLE, -1); ItemTable *table = static_cast<ItemTable *>(current); - return table->subitems.size() % table->columns.size(); } void RichTextLabel::pop() { + _stop_thread(); + MutexLock data_lock(data_mutex); + ERR_FAIL_COND(!current->parent); + if (current->type == ITEM_FRAME) { current_frame = static_cast<ItemFrame *>(current)->parent_frame; } @@ -2736,17 +3250,19 @@ void RichTextLabel::pop() { } void RichTextLabel::clear() { + _stop_thread(); + MutexLock data_lock(data_mutex); + main->_clear_children(); current = main; current_frame = main; main->lines.clear(); main->lines.resize(1); - main->first_invalid_line = 0; - update(); + main->first_invalid_line.store(0); selection.click_frame = nullptr; selection.click_item = nullptr; - selection.active = false; + deselect(); current_idx = 1; current_char_ofs = 0; @@ -2760,8 +3276,10 @@ void RichTextLabel::clear() { } void RichTextLabel::set_tab_size(int p_spaces) { + _stop_thread(); + tab_size = p_spaces; - main->first_resized_line = 0; + main->first_resized_line.store(0); update(); } @@ -2789,6 +3307,15 @@ bool RichTextLabel::is_meta_underlined() const { return underline_meta; } +void RichTextLabel::set_hint_underline(bool p_underline) { + underline_hint = p_underline; + update(); +} + +bool RichTextLabel::is_hint_underlined() const { + return underline_hint; +} + void RichTextLabel::set_override_selected_font_color(bool p_override_selected_font_color) { override_selected_font_color = p_override_selected_font_color; } @@ -2832,6 +3359,9 @@ void RichTextLabel::parse_bbcode(const String &p_bbcode) { } void RichTextLabel::append_text(const String &p_bbcode) { + _stop_thread(); + MutexLock data_lock(data_mutex); + int pos = 0; List<String> tag_stack; @@ -2847,21 +3377,34 @@ void RichTextLabel::append_text(const String &p_bbcode) { bool in_bold = false; bool in_italics = false; + bool after_list_open_tag = false; + bool after_list_close_tag = false; set_process_internal(false); - while (pos < p_bbcode.length()) { + while (pos <= p_bbcode.length()) { int brk_pos = p_bbcode.find("[", pos); if (brk_pos < 0) { brk_pos = p_bbcode.length(); } - if (brk_pos > pos) { - add_text(p_bbcode.substr(pos, brk_pos - pos)); + String text = brk_pos > pos ? p_bbcode.substr(pos, brk_pos - pos) : ""; + + // Trim the first newline character, it may be added later as needed. + if (after_list_close_tag || after_list_open_tag) { + text = text.trim_prefix("\n"); } if (brk_pos == p_bbcode.length()) { + // For tags that are not properly closed. + if (text.is_empty() && after_list_open_tag) { + text = "\n"; + } + + if (!text.is_empty()) { + add_text(text); + } break; //nothing else to add } @@ -2869,7 +3412,8 @@ void RichTextLabel::append_text(const String &p_bbcode) { if (brk_end == -1) { //no close, add the rest - add_text(p_bbcode.substr(brk_pos, p_bbcode.length() - brk_pos)); + text += p_bbcode.substr(brk_pos, p_bbcode.length() - brk_pos); + add_text(text); break; } @@ -2878,7 +3422,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { // Find optional parameters. String bbcode_name; - typedef Map<String, String> OptionMap; + typedef HashMap<String, String> OptionMap; OptionMap bbcode_options; if (!split_tag_block.is_empty()) { bbcode_name = split_tag_block[0]; @@ -2915,18 +3459,60 @@ void RichTextLabel::append_text(const String &p_bbcode) { } if (!tag_ok) { - add_text("[" + tag); + text += "[" + tag; + add_text(text); + after_list_open_tag = false; + after_list_close_tag = false; pos = brk_end; continue; } + if (text.is_empty() && after_list_open_tag) { + text = "\n"; // Make empty list have at least one item. + } + after_list_open_tag = false; + + if (tag == "/ol" || tag == "/ul") { + if (!text.is_empty()) { + // Make sure text ends with a newline character, that is, the last item + // will wrap at the end of block. + if (!text.ends_with("\n")) { + text += "\n"; + } + } else if (!after_list_close_tag) { + text = "\n"; // Make the innermost list item wrap at the end of lists. + } + after_list_close_tag = true; + } else { + after_list_close_tag = false; + } + + if (!text.is_empty()) { + add_text(text); + } + tag_stack.pop_front(); pos = brk_end + 1; if (tag != "/img" && tag != "/dropcap") { pop(); } + continue; + } - } else if (tag == "b") { + if (tag == "ol" || tag.begins_with("ol ") || tag == "ul" || tag.begins_with("ul ")) { + if (text.is_empty() && after_list_open_tag) { + text = "\n"; // Make each list have at least one item at the beginning. + } + after_list_open_tag = true; + } else { + after_list_open_tag = false; + } + if (!text.is_empty()) { + add_text(text); + } + after_list_close_tag = false; + + if (tag == "b") { //use bold font in_bold = true; if (in_italics) { @@ -3050,6 +3636,12 @@ void RichTextLabel::append_text(const String &p_bbcode) { push_strikethrough(); pos = brk_end + 1; tag_stack.push_front(tag); + } else if (tag == "lb") { + add_text("["); + pos = brk_end + 1; + } else if (tag == "rb") { + add_text("]"); + pos = brk_end + 1; } else if (tag == "lrm") { add_text(String::chr(0x200E)); pos = brk_end + 1; @@ -3106,6 +3698,10 @@ void RichTextLabel::append_text(const String &p_bbcode) { push_paragraph(HORIZONTAL_ALIGNMENT_FILL); pos = brk_end + 1; tag_stack.push_front(tag); + } else if (tag == "left") { + push_paragraph(HORIZONTAL_ALIGNMENT_LEFT); + pos = brk_end + 1; + tag_stack.push_front(tag); } else if (tag == "right") { push_paragraph(HORIZONTAL_ALIGNMENT_RIGHT); pos = brk_end + 1; @@ -3119,7 +3715,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { indent_level++; push_list(indent_level, LIST_NUMBERS, false); pos = brk_end + 1; - tag_stack.push_front(tag); + tag_stack.push_front("ol"); } else if (tag == "ol type=a") { indent_level++; push_list(indent_level, LIST_LETTERS, false); @@ -3154,7 +3750,7 @@ void RichTextLabel::append_text(const String &p_bbcode) { HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT; Control::TextDirection dir = Control::TEXT_DIRECTION_INHERITED; String lang; - Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + TextServer::StructuredTextParser st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; for (int i = 0; i < subtag.size(); i++) { Vector<String> subtag_a = subtag[i].split("="); if (subtag_a.size() == 2) { @@ -3180,19 +3776,19 @@ void RichTextLabel::append_text(const String &p_bbcode) { 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; + st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; } else if (subtag_a[1] == "u" || subtag_a[1] == "uri") { - st_parser = STRUCTURED_TEXT_URI; + st_parser = TextServer::STRUCTURED_TEXT_URI; } else if (subtag_a[1] == "f" || subtag_a[1] == "file") { - st_parser = STRUCTURED_TEXT_FILE; + st_parser = TextServer::STRUCTURED_TEXT_FILE; } else if (subtag_a[1] == "e" || subtag_a[1] == "email") { - st_parser = STRUCTURED_TEXT_EMAIL; + st_parser = TextServer::STRUCTURED_TEXT_EMAIL; } else if (subtag_a[1] == "l" || subtag_a[1] == "list") { - st_parser = STRUCTURED_TEXT_LIST; + st_parser = TextServer::STRUCTURED_TEXT_LIST; } else if (subtag_a[1] == "n" || subtag_a[1] == "none") { - st_parser = STRUCTURED_TEXT_NONE; + st_parser = TextServer::STRUCTURED_TEXT_NONE; } else if (subtag_a[1] == "c" || subtag_a[1] == "custom") { - st_parser = STRUCTURED_TEXT_CUSTOM; + st_parser = TextServer::STRUCTURED_TEXT_CUSTOM; } } } @@ -3216,6 +3812,11 @@ void RichTextLabel::append_text(const String &p_bbcode) { push_meta(url); pos = brk_end + 1; tag_stack.push_front("url"); + } else if (tag.begins_with("hint=")) { + String description = tag.substr(5, tag.length()); + push_hint(description); + pos = brk_end + 1; + tag_stack.push_front("hint"); } else if (tag.begins_with("dropcap")) { Vector<String> subtag = tag.substr(5, tag.length()).split(" "); Ref<Font> f = get_theme_font(SNAME("normal_font")); @@ -3306,9 +3907,9 @@ void RichTextLabel::append_text(const String &p_bbcode) { Ref<Texture2D> texture = ResourceLoader::load(image, "Texture2D"); if (texture.is_valid()) { Color color = Color(1.0, 1.0, 1.0); - OptionMap::Element *color_option = bbcode_options.find("color"); + OptionMap::Iterator color_option = bbcode_options.find("color"); if (color_option) { - color = Color::from_string(color_option->value(), color); + color = Color::from_string(color_option->value, color); } int width = 0; @@ -3322,14 +3923,14 @@ void RichTextLabel::append_text(const String &p_bbcode) { height = bbcode_value.substr(sep + 1).to_int(); } } else { - OptionMap::Element *width_option = bbcode_options.find("width"); + OptionMap::Iterator width_option = bbcode_options.find("width"); if (width_option) { - width = width_option->value().to_int(); + width = width_option->value.to_int(); } - OptionMap::Element *height_option = bbcode_options.find("height"); + OptionMap::Iterator height_option = bbcode_options.find("height"); if (height_option) { - height = height_option->value().to_int(); + height = height_option->value.to_int(); } } @@ -3415,15 +4016,15 @@ void RichTextLabel::append_text(const String &p_bbcode) { } else if (bbcode_name == "fade") { int start_index = 0; - OptionMap::Element *start_option = bbcode_options.find("start"); + OptionMap::Iterator start_option = bbcode_options.find("start"); if (start_option) { - start_index = start_option->value().to_int(); + start_index = start_option->value.to_int(); } int length = 10; - OptionMap::Element *length_option = bbcode_options.find("length"); + OptionMap::Iterator length_option = bbcode_options.find("length"); if (length_option) { - length = length_option->value().to_int(); + length = length_option->value.to_int(); } push_fade(start_index, length); @@ -3431,15 +4032,15 @@ void RichTextLabel::append_text(const String &p_bbcode) { tag_stack.push_front("fade"); } else if (bbcode_name == "shake") { int strength = 5; - OptionMap::Element *strength_option = bbcode_options.find("level"); + OptionMap::Iterator strength_option = bbcode_options.find("level"); if (strength_option) { - strength = strength_option->value().to_int(); + strength = strength_option->value.to_int(); } float rate = 20.0f; - OptionMap::Element *rate_option = bbcode_options.find("rate"); + OptionMap::Iterator rate_option = bbcode_options.find("rate"); if (rate_option) { - rate = rate_option->value().to_float(); + rate = rate_option->value.to_float(); } push_shake(strength, rate); @@ -3448,15 +4049,15 @@ void RichTextLabel::append_text(const String &p_bbcode) { set_process_internal(true); } else if (bbcode_name == "wave") { float amplitude = 20.0f; - OptionMap::Element *amplitude_option = bbcode_options.find("amp"); + OptionMap::Iterator amplitude_option = bbcode_options.find("amp"); if (amplitude_option) { - amplitude = amplitude_option->value().to_float(); + amplitude = amplitude_option->value.to_float(); } float period = 5.0f; - OptionMap::Element *period_option = bbcode_options.find("freq"); + OptionMap::Iterator period_option = bbcode_options.find("freq"); if (period_option) { - period = period_option->value().to_float(); + period = period_option->value.to_float(); } push_wave(period, amplitude); @@ -3465,15 +4066,15 @@ void RichTextLabel::append_text(const String &p_bbcode) { set_process_internal(true); } else if (bbcode_name == "tornado") { float radius = 10.0f; - OptionMap::Element *radius_option = bbcode_options.find("radius"); + OptionMap::Iterator radius_option = bbcode_options.find("radius"); if (radius_option) { - radius = radius_option->value().to_float(); + radius = radius_option->value.to_float(); } float frequency = 1.0f; - OptionMap::Element *frequency_option = bbcode_options.find("freq"); + OptionMap::Iterator frequency_option = bbcode_options.find("freq"); if (frequency_option) { - frequency = frequency_option->value().to_float(); + frequency = frequency_option->value.to_float(); } push_tornado(frequency, radius); @@ -3482,21 +4083,21 @@ void RichTextLabel::append_text(const String &p_bbcode) { set_process_internal(true); } else if (bbcode_name == "rainbow") { float saturation = 0.8f; - OptionMap::Element *saturation_option = bbcode_options.find("sat"); + OptionMap::Iterator saturation_option = bbcode_options.find("sat"); if (saturation_option) { - saturation = saturation_option->value().to_float(); + saturation = saturation_option->value.to_float(); } float value = 0.8f; - OptionMap::Element *value_option = bbcode_options.find("val"); + OptionMap::Iterator value_option = bbcode_options.find("val"); if (value_option) { - value = value_option->value().to_float(); + value = value_option->value.to_float(); } float frequency = 1.0f; - OptionMap::Element *frequency_option = bbcode_options.find("freq"); + OptionMap::Iterator frequency_option = bbcode_options.find("freq"); if (frequency_option) { - frequency = frequency_option->value().to_float(); + frequency = frequency_option->value.to_float(); } push_rainbow(saturation, value, frequency); @@ -3557,9 +4158,13 @@ void RichTextLabel::append_text(const String &p_bbcode) { } 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); + if (p_paragraph <= 0) { + vscroll->set_value(0); + } else if (p_paragraph >= main->first_invalid_line.load()) { + vscroll->set_value(vscroll->get_max()); + } else { + vscroll->set_value(main->lines[p_paragraph].offset.y); + } } int RichTextLabel::get_paragraph_count() const { @@ -3574,25 +4179,57 @@ int RichTextLabel::get_visible_paragraph_count() const { } void RichTextLabel::scroll_to_line(int p_line) { - _validate_line_caches(main); - + if (p_line <= 0) { + vscroll->set_value(0); + return; + } int line_count = 0; - for (int i = 0; i < main->lines.size(); i++) { + int to_line = main->first_invalid_line.load(); + for (int i = 0; i < to_line; i++) { + MutexLock lock(main->lines[i].text_buf->get_mutex()); 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; + line_offset += main->lines[i].text_buf->get_line_size(j).y + get_theme_constant(SNAME("line_separation")); } vscroll->set_value(main->lines[i].offset.y + line_offset); return; } line_count += main->lines[i].text_buf->get_line_count(); } + vscroll->set_value(vscroll->get_max()); +} + +float RichTextLabel::get_line_offset(int p_line) { + int line_count = 0; + int to_line = main->first_invalid_line.load(); + for (int i = 0; i < to_line; i++) { + MutexLock lock(main->lines[i].text_buf->get_mutex()); + if ((line_count <= p_line) && (p_line <= line_count + main->lines[i].text_buf->get_line_count())) { + 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 + get_theme_constant(SNAME("line_separation")); + } + return main->lines[i].offset.y + line_offset; + } + line_count += main->lines[i].text_buf->get_line_count(); + } + return 0; +} + +float RichTextLabel::get_paragraph_offset(int p_paragraph) { + int to_line = main->first_invalid_line.load(); + if (0 <= p_paragraph && p_paragraph < to_line) { + return main->lines[p_paragraph].offset.y; + } + return 0; } int RichTextLabel::get_line_count() const { int line_count = 0; - for (int i = 0; i < main->lines.size(); i++) { + int to_line = main->first_invalid_line.load(); + for (int i = 0; i < to_line; i++) { + MutexLock lock(main->lines[i].text_buf->get_mutex()); line_count += main->lines[i].text_buf->get_line_count(); } return line_count; @@ -3609,8 +4246,7 @@ void RichTextLabel::set_selection_enabled(bool p_enabled) { selection.enabled = p_enabled; if (!p_enabled) { if (selection.active) { - selection.active = false; - update(); + deselect(); } set_focus_mode(FOCUS_NONE); } else { @@ -3621,8 +4257,30 @@ void RichTextLabel::set_selection_enabled(bool p_enabled) { void RichTextLabel::set_deselect_on_focus_loss_enabled(const bool p_enabled) { deselect_on_focus_loss_enabled = p_enabled; if (p_enabled && selection.active && !has_focus()) { - selection.active = false; - update(); + deselect(); + } +} + +Variant RichTextLabel::get_drag_data(const Point2 &p_point) { + if (selection.drag_attempt && selection.enabled) { + String t = get_selected_text(); + Label *l = memnew(Label); + l->set_text(t); + set_drag_preview(l); + return t; + } + + return Variant(); +} + +bool RichTextLabel::_is_click_inside_selection() const { + if (selection.active && selection.enabled && selection.click_frame && selection.from_frame && selection.to_frame) { + const Line &l_click = selection.click_frame->lines[selection.click_line]; + const Line &l_from = selection.from_frame->lines[selection.from_line]; + const Line &l_to = selection.to_frame->lines[selection.to_line]; + return (l_click.char_offset + selection.click_char >= l_from.char_offset + selection.from_char) && (l_click.char_offset + selection.click_char <= l_to.char_offset + selection.to_char); + } else { + return false; } } @@ -3632,13 +4290,13 @@ bool RichTextLabel::_search_table(ItemTable *p_table, List<Item *>::Element *p_f 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--) { + for (int i = (int)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++) { + for (int i = 0; i < (int)frame->lines.size(); i++) { if (_search_line(frame, i, p_string, 0, p_reverse_search)) { return true; } @@ -3651,19 +4309,19 @@ bool RichTextLabel::_search_table(ItemTable *p_table, List<Item *>::Element *p_f 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); + ERR_FAIL_COND_V(p_line < 0 || p_line >= (int)p_frame->lines.size(), false); - Line &l = p_frame->lines.write[p_line]; + Line &l = p_frame->lines[p_line]; String text; - Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + Item *it_to = (p_line + 1 < (int)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; + ItemText *t = static_cast<ItemText *>(it); text += t->text; } break; case ITEM_IMAGE: { @@ -3714,7 +4372,8 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p int char_idx = p_search_previous ? -1 : 0; int current_line = 0; - int ending_line = main->lines.size() - 1; + int to_line = main->first_invalid_line.load(); + int ending_line = to_line - 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; @@ -3763,8 +4422,8 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p 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 = to_line - 1; + } else if (current_line >= to_line) { current_line = 0; } @@ -3786,17 +4445,18 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p 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); + ERR_FAIL_COND_V(p_line < 0 || p_line >= (int)p_frame->lines.size(), text); - Line &l = p_frame->lines.write[p_line]; + Line &l = p_frame->lines[p_line]; - Item *it_to = (p_line + 1 < p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; + Item *it_to = (p_line + 1 < (int)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)) { + for (Item *it = l.from; it; it = _get_next_item(it)) { end_idx = it->index + 1; } } @@ -3806,7 +4466,7 @@ String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line, Selection p 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++) { + for (int i = 0; i < (int)frame->lines.size(); i++) { text += _get_line_text(frame, i, p_selection); } } @@ -3838,18 +4498,50 @@ String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line, Selection p return text; } +void RichTextLabel::set_context_menu_enabled(bool p_enabled) { + context_menu_enabled = p_enabled; +} + +bool RichTextLabel::is_context_menu_enabled() const { + return context_menu_enabled; +} + +void RichTextLabel::set_shortcut_keys_enabled(bool p_enabled) { + shortcut_keys_enabled = p_enabled; +} + +bool RichTextLabel::is_shortcut_keys_enabled() const { + return shortcut_keys_enabled; +} + +// Context menu. +PopupMenu *RichTextLabel::get_menu() const { + const_cast<RichTextLabel *>(this)->_generate_context_menu(); + return menu; +} + +bool RichTextLabel::is_menu_visible() const { + return menu && menu->is_visible(); +} + String RichTextLabel::get_selected_text() const { if (!selection.active || !selection.enabled) { return ""; } String text; - for (int i = 0; i < main->lines.size(); i++) { + int to_line = main->first_invalid_line.load(); + for (int i = 0; i < to_line; i++) { text += _get_line_text(main, i, selection); } return text; } +void RichTextLabel::deselect() { + selection.active = false; + update(); +} + void RichTextLabel::selection_copy() { String text = get_selected_text(); @@ -3858,6 +4550,53 @@ void RichTextLabel::selection_copy() { } } +void RichTextLabel::select_all() { + if (!selection.enabled) { + return; + } + + Item *it = main; + Item *from_item = nullptr; + Item *to_item = nullptr; + + while (it) { + if (it->type != ITEM_FRAME) { + if (!from_item) { + from_item = it; + } else { + to_item = it; + } + } + it = _get_next_item(it, true); + } + if (!from_item || !to_item) { + return; + } + + ItemFrame *from_frame = nullptr; + int from_line = 0; + _find_frame(from_item, &from_frame, &from_line); + if (!from_frame) { + return; + } + ItemFrame *to_frame = nullptr; + int to_line = 0; + _find_frame(to_item, &to_frame, &to_line); + if (!to_frame) { + return; + } + selection.from_line = from_line; + selection.from_frame = from_frame; + selection.from_char = 0; + selection.from_item = from_item; + selection.to_line = to_line; + selection.to_frame = to_frame; + selection.to_char = to_frame->lines[to_line].char_count; + selection.to_item = to_item; + selection.active = true; + update(); +} + bool RichTextLabel::is_selection_enabled() const { return selection.enabled; } @@ -3884,7 +4623,7 @@ int RichTextLabel::get_selection_to() const { void RichTextLabel::set_text(const String &p_bbcode) { text = p_bbcode; - if (is_inside_tree() && use_bbcode) { + if (use_bbcode) { parse_bbcode(p_bbcode); } else { // raw text clear(); @@ -3914,10 +4653,8 @@ String RichTextLabel::get_parsed_text() const { Item *it = main; while (it) { if (it->type == ITEM_DROPCAP) { - const ItemDropcap *dc = (ItemDropcap *)it; - if (dc != nullptr) { - text += dc->text; - } + ItemDropcap *dc = static_cast<ItemDropcap *>(it); + text += dc->text; } else if (it->type == ITEM_TEXT) { ItemText *t = static_cast<ItemText *>(it); text += t->text; @@ -3935,32 +4672,40 @@ String RichTextLabel::get_parsed_text() const { void RichTextLabel::set_text_direction(Control::TextDirection p_text_direction) { ERR_FAIL_COND((int)p_text_direction < -1 || (int)p_text_direction > 3); + _stop_thread(); + if (text_direction != p_text_direction) { text_direction = p_text_direction; - main->first_invalid_line = 0; //invalidate ALL - _validate_line_caches(main); + main->first_invalid_line.store(0); //invalidate ALL + _validate_line_caches(); update(); } } -void RichTextLabel::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { +void RichTextLabel::set_structured_text_bidi_override(TextServer::StructuredTextParser p_parser) { if (st_parser != p_parser) { + _stop_thread(); + st_parser = p_parser; - main->first_invalid_line = 0; //invalidate ALL - _validate_line_caches(main); + main->first_invalid_line.store(0); //invalidate ALL + _validate_line_caches(); update(); } } -Control::StructuredTextParser RichTextLabel::get_structured_text_bidi_override() const { +TextServer::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(); + if (st_args != p_args) { + _stop_thread(); + + st_args = p_args; + main->first_invalid_line.store(0); //invalidate ALL + _validate_line_caches(); + update(); + } } Array RichTextLabel::get_structured_text_bidi_override_options() const { @@ -3973,9 +4718,11 @@ Control::TextDirection RichTextLabel::get_text_direction() const { void RichTextLabel::set_language(const String &p_language) { if (language != p_language) { + _stop_thread(); + language = p_language; - main->first_invalid_line = 0; //invalidate ALL - _validate_line_caches(main); + main->first_invalid_line.store(0); //invalidate ALL + _validate_line_caches(); update(); } } @@ -3984,8 +4731,25 @@ String RichTextLabel::get_language() const { return language; } +void RichTextLabel::set_autowrap_mode(RichTextLabel::AutowrapMode p_mode) { + if (autowrap_mode != p_mode) { + _stop_thread(); + + autowrap_mode = p_mode; + main->first_invalid_line = 0; //invalidate ALL + _validate_line_caches(); + update(); + } +} + +RichTextLabel::AutowrapMode RichTextLabel::get_autowrap_mode() const { + return autowrap_mode; +} + void RichTextLabel::set_percent_visible(float p_percent) { if (percent_visible != p_percent) { + _stop_thread(); + if (p_percent < 0 || p_percent >= 1) { visible_characters = -1; percent_visible = 1; @@ -3993,8 +4757,10 @@ void RichTextLabel::set_percent_visible(float p_percent) { visible_characters = get_total_character_count() * p_percent; percent_visible = p_percent; } - main->first_invalid_line = 0; //invalidate ALL - _validate_line_caches(main); + if (visible_chars_behavior == VC_CHARS_BEFORE_SHAPING) { + main->first_invalid_line.store(0); //invalidate ALL + _validate_line_caches(); + } update(); } } @@ -4028,15 +4794,27 @@ void RichTextLabel::install_effect(const Variant effect) { int RichTextLabel::get_content_height() const { int total_height = 0; - if (main->lines.size()) { - total_height = main->lines[main->lines.size() - 1].offset.y + main->lines[main->lines.size() - 1].text_buf->get_size().y; + int to_line = main->first_invalid_line.load(); + if (to_line) { + MutexLock lock(main->lines[to_line - 1].text_buf->get_mutex()); + total_height = main->lines[to_line - 1].offset.y + main->lines[to_line - 1].text_buf->get_size().y + main->lines[to_line - 1].text_buf->get_line_count() * get_theme_constant(SNAME("line_separation")); } return total_height; } +int RichTextLabel::get_content_width() const { + int total_width = 0; + int to_line = main->first_invalid_line.load(); + for (int i = 0; i < to_line; i++) { + MutexLock lock(main->lines[i].text_buf->get_mutex()); + total_width = MAX(total_width, main->lines[i].offset.x + main->lines[i].text_buf->get_size().x); + } + return total_width; +} + #ifndef DISABLE_DEPRECATED // People will be very angry, if their texts get erased, because of #39148. (3.x -> 4.0) -// Altough some people may not used bbcode_text, so we only overwrite, if bbcode_text is not empty +// Although some people may not used bbcode_text, so we only overwrite, if bbcode_text is not empty. bool RichTextLabel::_set(const StringName &p_name, const Variant &p_value) { if (p_name == "bbcode_text" && !((String)p_value).is_empty()) { set_text(p_value); @@ -4064,10 +4842,11 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("push_color", "color"), &RichTextLabel::push_color); 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", "alignment", "base_direction", "language", "st_parser"), &RichTextLabel::push_paragraph, DEFVAL(TextServer::DIRECTION_AUTO), DEFVAL(""), DEFVAL(STRUCTURED_TEXT_DEFAULT)); + ClassDB::bind_method(D_METHOD("push_paragraph", "alignment", "base_direction", "language", "st_parser"), &RichTextLabel::push_paragraph, DEFVAL(TextServer::DIRECTION_AUTO), DEFVAL(""), DEFVAL(TextServer::STRUCTURED_TEXT_DEFAULT)); ClassDB::bind_method(D_METHOD("push_indent", "level"), &RichTextLabel::push_indent); 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_hint", "description"), &RichTextLabel::push_hint); ClassDB::bind_method(D_METHOD("push_underline"), &RichTextLabel::push_underline); ClassDB::bind_method(D_METHOD("push_strikethrough"), &RichTextLabel::push_strikethrough); ClassDB::bind_method(D_METHOD("push_table", "columns", "inline_align"), &RichTextLabel::push_table, DEFVAL(INLINE_ALIGNMENT_TOP)); @@ -4093,9 +4872,15 @@ void RichTextLabel::_bind_methods() { 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_autowrap_mode", "autowrap_mode"), &RichTextLabel::set_autowrap_mode); + ClassDB::bind_method(D_METHOD("get_autowrap_mode"), &RichTextLabel::get_autowrap_mode); + 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); + ClassDB::bind_method(D_METHOD("set_hint_underline", "enable"), &RichTextLabel::set_hint_underline); + ClassDB::bind_method(D_METHOD("is_hint_underlined"), &RichTextLabel::is_hint_underlined); + ClassDB::bind_method(D_METHOD("set_override_selected_font_color", "override"), &RichTextLabel::set_override_selected_font_color); ClassDB::bind_method(D_METHOD("is_overriding_selected_font_color"), &RichTextLabel::is_overriding_selected_font_color); @@ -4105,7 +4890,7 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("set_scroll_follow", "follow"), &RichTextLabel::set_scroll_follow); ClassDB::bind_method(D_METHOD("is_scroll_following"), &RichTextLabel::is_scroll_following); - ClassDB::bind_method(D_METHOD("get_v_scroll"), &RichTextLabel::get_v_scroll); + ClassDB::bind_method(D_METHOD("get_v_scroll_bar"), &RichTextLabel::get_v_scroll_bar); 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); @@ -4119,25 +4904,46 @@ 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("set_context_menu_enabled", "enabled"), &RichTextLabel::set_context_menu_enabled); + ClassDB::bind_method(D_METHOD("is_context_menu_enabled"), &RichTextLabel::is_context_menu_enabled); + + ClassDB::bind_method(D_METHOD("set_shortcut_keys_enabled", "enabled"), &RichTextLabel::set_shortcut_keys_enabled); + ClassDB::bind_method(D_METHOD("is_shortcut_keys_enabled"), &RichTextLabel::is_shortcut_keys_enabled); + ClassDB::bind_method(D_METHOD("set_deselect_on_focus_loss_enabled", "enable"), &RichTextLabel::set_deselect_on_focus_loss_enabled); ClassDB::bind_method(D_METHOD("is_deselect_on_focus_loss_enabled"), &RichTextLabel::is_deselect_on_focus_loss_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("select_all"), &RichTextLabel::select_all); ClassDB::bind_method(D_METHOD("get_selected_text"), &RichTextLabel::get_selected_text); + ClassDB::bind_method(D_METHOD("deselect"), &RichTextLabel::deselect); ClassDB::bind_method(D_METHOD("parse_bbcode", "bbcode"), &RichTextLabel::parse_bbcode); ClassDB::bind_method(D_METHOD("append_text", "bbcode"), &RichTextLabel::append_text); ClassDB::bind_method(D_METHOD("get_text"), &RichTextLabel::get_text); + ClassDB::bind_method(D_METHOD("is_ready"), &RichTextLabel::is_ready); + + ClassDB::bind_method(D_METHOD("set_threaded", "threaded"), &RichTextLabel::set_threaded); + ClassDB::bind_method(D_METHOD("is_threaded"), &RichTextLabel::is_threaded); + + ClassDB::bind_method(D_METHOD("set_progress_bar_delay", "delay_ms"), &RichTextLabel::set_progress_bar_delay); + ClassDB::bind_method(D_METHOD("get_progress_bar_delay"), &RichTextLabel::get_progress_bar_delay); + ClassDB::bind_method(D_METHOD("set_visible_characters", "amount"), &RichTextLabel::set_visible_characters); ClassDB::bind_method(D_METHOD("get_visible_characters"), &RichTextLabel::get_visible_characters); + ClassDB::bind_method(D_METHOD("get_visible_characters_behavior"), &RichTextLabel::get_visible_characters_behavior); + ClassDB::bind_method(D_METHOD("set_visible_characters_behavior", "behavior"), &RichTextLabel::set_visible_characters_behavior); + ClassDB::bind_method(D_METHOD("set_percent_visible", "percent_visible"), &RichTextLabel::set_percent_visible); ClassDB::bind_method(D_METHOD("get_percent_visible"), &RichTextLabel::get_percent_visible); + ClassDB::bind_method(D_METHOD("get_character_line", "character"), &RichTextLabel::get_character_line); + ClassDB::bind_method(D_METHOD("get_character_paragraph", "character"), &RichTextLabel::get_character_paragraph); ClassDB::bind_method(D_METHOD("get_total_character_count"), &RichTextLabel::get_total_character_count); ClassDB::bind_method(D_METHOD("set_use_bbcode", "enable"), &RichTextLabel::set_use_bbcode); @@ -4150,6 +4956,10 @@ void RichTextLabel::_bind_methods() { 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("get_content_width"), &RichTextLabel::get_content_width); + + ClassDB::bind_method(D_METHOD("get_line_offset", "line"), &RichTextLabel::get_line_offset); + ClassDB::bind_method(D_METHOD("get_paragraph_offset", "paragraph"), &RichTextLabel::get_paragraph_offset); ClassDB::bind_method(D_METHOD("parse_expressions_for_values", "expressions"), &RichTextLabel::parse_expressions_for_values); @@ -4157,28 +4967,39 @@ void RichTextLabel::_bind_methods() { ClassDB::bind_method(D_METHOD("get_effects"), &RichTextLabel::get_effects); ClassDB::bind_method(D_METHOD("install_effect", "effect"), &RichTextLabel::install_effect); - ADD_PROPERTY(PropertyInfo(Variant::INT, "visible_characters", PROPERTY_HINT_RANGE, "-1,128000,1"), "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"); + ClassDB::bind_method(D_METHOD("get_menu"), &RichTextLabel::get_menu); + ClassDB::bind_method(D_METHOD("is_menu_visible"), &RichTextLabel::is_menu_visible); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "meta_underlined"), "set_meta_underline", "is_meta_underlined"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_size", PROPERTY_HINT_RANGE, "0,24,1"), "set_tab_size", "get_tab_size"); - ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); + // Note: set "bbcode_enabled" first, to avoid unnecessary "text" resets. ADD_PROPERTY(PropertyInfo(Variant::BOOL, "bbcode_enabled"), "set_use_bbcode", "is_using_bbcode"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "fit_content_height"), "set_fit_content_height", "is_fit_content_height_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "threaded"), "set_threaded", "is_threaded"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "progress_bar_delay"), "set_progress_bar_delay", "get_progress_bar_delay"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_size", PROPERTY_HINT_RANGE, "0,24,1"), "set_tab_size", "get_tab_size"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "fit_content_height"), "set_fit_content_height", "is_fit_content_height_enabled"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_active"), "set_scroll_active", "is_scroll_active"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_following"), "set_scroll_follow", "is_scroll_following"); - 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::BOOL, "deselect_on_focus_loss_enabled"), "set_deselect_on_focus_loss_enabled", "is_deselect_on_focus_loss_enabled"); - 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::BOOL, "meta_underlined"), "set_meta_underline", "is_meta_underlined"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "hint_underlined"), "set_hint_underline", "is_hint_underlined"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "autowrap_mode", PROPERTY_HINT_ENUM, "Off,Arbitrary,Word,Word (Smart)"), "set_autowrap_mode", "get_autowrap_mode"); + + // Note: "visible_characters" and "percent_visible" should be set after "text" to be correctly applied. + ADD_PROPERTY(PropertyInfo(Variant::INT, "visible_characters", PROPERTY_HINT_RANGE, "-1,128000,1"), "set_visible_characters", "get_visible_characters"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "visible_characters_behavior", PROPERTY_HINT_ENUM, "Characters Before Shaping,Characters After Shaping,Glyphs (Layout Direction),Glyphs (Left-to-Right),Glyphs (Right-to-Left)"), "set_visible_characters_behavior", "get_visible_characters_behavior"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "percent_visible", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_percent_visible", "get_percent_visible"); + + 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_GROUP("Locale", ""); 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::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "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"); @@ -4188,6 +5009,13 @@ void RichTextLabel::_bind_methods() { ADD_SIGNAL(MethodInfo("meta_hover_started", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); ADD_SIGNAL(MethodInfo("meta_hover_ended", PropertyInfo(Variant::NIL, "meta", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NIL_IS_VARIANT))); + ADD_SIGNAL(MethodInfo("finished")); + + BIND_ENUM_CONSTANT(AUTOWRAP_OFF); + BIND_ENUM_CONSTANT(AUTOWRAP_ARBITRARY); + BIND_ENUM_CONSTANT(AUTOWRAP_WORD); + BIND_ENUM_CONSTANT(AUTOWRAP_WORD_SMART); + BIND_ENUM_CONSTANT(LIST_NUMBERS); BIND_ENUM_CONSTANT(LIST_LETTERS); BIND_ENUM_CONSTANT(LIST_ROMAN); @@ -4217,12 +5045,36 @@ void RichTextLabel::_bind_methods() { BIND_ENUM_CONSTANT(ITEM_BGCOLOR); BIND_ENUM_CONSTANT(ITEM_FGCOLOR); BIND_ENUM_CONSTANT(ITEM_META); + BIND_ENUM_CONSTANT(ITEM_HINT); BIND_ENUM_CONSTANT(ITEM_DROPCAP); BIND_ENUM_CONSTANT(ITEM_CUSTOMFX); + + BIND_ENUM_CONSTANT(VC_CHARS_BEFORE_SHAPING); + BIND_ENUM_CONSTANT(VC_CHARS_AFTER_SHAPING); + BIND_ENUM_CONSTANT(VC_GLYPHS_AUTO); + BIND_ENUM_CONSTANT(VC_GLYPHS_LTR); + BIND_ENUM_CONSTANT(VC_GLYPHS_RTL); +} + +RichTextLabel::VisibleCharactersBehavior RichTextLabel::get_visible_characters_behavior() const { + return visible_chars_behavior; +} + +void RichTextLabel::set_visible_characters_behavior(RichTextLabel::VisibleCharactersBehavior p_behavior) { + if (visible_chars_behavior != p_behavior) { + _stop_thread(); + + visible_chars_behavior = p_behavior; + main->first_invalid_line.store(0); //invalidate ALL + _validate_line_caches(); + update(); + } } void RichTextLabel::set_visible_characters(int p_visible) { if (visible_characters != p_visible) { + _stop_thread(); + visible_characters = p_visible; if (p_visible == -1) { percent_visible = 1; @@ -4232,8 +5084,10 @@ void RichTextLabel::set_visible_characters(int p_visible) { percent_visible = (float)p_visible / (float)total_char_count; } } - main->first_invalid_line = 0; //invalidate ALL - _validate_line_caches(main); + if (visible_chars_behavior == VC_CHARS_BEFORE_SHAPING) { + main->first_invalid_line.store(0); //invalidate ALL + _validate_line_caches(); + } update(); } } @@ -4242,6 +5096,39 @@ int RichTextLabel::get_visible_characters() const { return visible_characters; } +int RichTextLabel::get_character_line(int p_char) { + int line_count = 0; + int to_line = main->first_invalid_line.load(); + for (int i = 0; i < to_line; i++) { + MutexLock lock(main->lines[i].text_buf->get_mutex()); + if (main->lines[i].char_offset < p_char && p_char <= main->lines[i].char_offset + main->lines[i].char_count) { + for (int j = 0; j < main->lines[i].text_buf->get_line_count(); j++) { + Vector2i range = main->lines[i].text_buf->get_line_range(j); + if (main->lines[i].char_offset + range.x < p_char && p_char <= main->lines[i].char_offset + range.y) { + return line_count; + } + line_count++; + } + } else { + line_count += main->lines[i].text_buf->get_line_count(); + } + } + return -1; +} + +int RichTextLabel::get_character_paragraph(int p_char) { + int para_count = 0; + int to_line = main->first_invalid_line.load(); + for (int i = 0; i < to_line; i++) { + if (main->lines[i].char_offset < p_char && p_char <= main->lines[i].char_offset + main->lines[i].char_count) { + return para_count; + } else { + para_count++; + } + } + return -1; +} + int RichTextLabel::get_total_character_count() const { // Note: Do not use line buffer "char_count", it includes only visible characters. int tc = 0; @@ -4257,10 +5144,26 @@ int RichTextLabel::get_total_character_count() const { } it = _get_next_item(it, true); } - return tc; } +int RichTextLabel::get_total_glyph_count() const { + int tg = 0; + Item *it = main; + while (it) { + if (it->type == ITEM_FRAME) { + ItemFrame *f = static_cast<ItemFrame *>(it); + for (int i = 0; i < (int)f->lines.size(); i++) { + MutexLock lock(f->lines[i].text_buf->get_mutex()); + tg += TS->shaped_text_get_glyph_count(f->lines[i].text_buf->get_rid()); + } + } + it = _get_next_item(it, true); + } + + return tg; +} + void RichTextLabel::set_fixed_size_to_width(int p_width) { fixed_width = p_width; update_minimum_size(); @@ -4274,14 +5177,66 @@ Size2 RichTextLabel::get_minimum_size() const { size.x += fixed_width; } - if (fixed_width != -1 || fit_content_height) { - const_cast<RichTextLabel *>(this)->_validate_line_caches(main); + if (fit_content_height) { size.y += get_content_height(); } return size; } +// Context menu. +void RichTextLabel::_generate_context_menu() { + if (!menu) { + menu = memnew(PopupMenu); + add_child(menu, false, INTERNAL_MODE_FRONT); + + menu->connect("id_pressed", callable_mp(this, &RichTextLabel::_menu_option)); + } + + // Reorganize context menu. + menu->clear(); + if (selection.enabled) { + menu->add_item(RTR("Copy"), MENU_COPY, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_copy") : Key::NONE); + menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : Key::NONE); + } +} + +Key RichTextLabel::_get_menu_action_accelerator(const String &p_action) { + const List<Ref<InputEvent>> *events = InputMap::get_singleton()->action_get_events(p_action); + if (!events) { + return Key::NONE; + } + + // Use first event in the list for the accelerator. + const List<Ref<InputEvent>>::Element *first_event = events->front(); + if (!first_event) { + return Key::NONE; + } + + const Ref<InputEventKey> event = first_event->get(); + if (event.is_null()) { + return Key::NONE; + } + + // Use physical keycode if non-zero + if (event->get_physical_keycode() != Key::NONE) { + return event->get_physical_keycode_with_modifiers(); + } else { + return event->get_keycode_with_modifiers(); + } +} + +void RichTextLabel::_menu_option(int p_option) { + switch (p_option) { + case MENU_COPY: { + selection_copy(); + } break; + case MENU_SELECT_ALL: { + select_all(); + } break; + } +} + 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); @@ -4289,7 +5244,7 @@ void RichTextLabel::_draw_fbg_boxes(RID p_ci, RID p_rid, Vector2 line_off, Item // 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); + Color color; if (fbg_flag == 0) { color = _find_bgcolor(it); @@ -4415,14 +5370,15 @@ Dictionary RichTextLabel::parse_expressions_for_values(Vector<String> p_expressi return d; } -RichTextLabel::RichTextLabel() { +RichTextLabel::RichTextLabel(const String &p_text) { main = memnew(ItemFrame); main->index = 0; current = main; main->lines.resize(1); - main->lines.write[0].from = main; - main->first_invalid_line = 0; - main->first_resized_line = 0; + main->lines[0].from = main; + main->first_invalid_line.store(0); + main->first_resized_line.store(0); + main->first_invalid_font_line.store(0); current_frame = main; vscroll = memnew(VScrollBar); @@ -4436,9 +5392,14 @@ RichTextLabel::RichTextLabel() { vscroll->set_step(1); vscroll->hide(); + set_text(p_text); + updating.store(false); + stop_thread.store(false); + set_clip_contents(true); } RichTextLabel::~RichTextLabel() { + _stop_thread(); memdelete(main); } diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index 58a531e5ad..93e57058b0 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 RICH_TEXT_LABEL_H #include "rich_text_effect.h" +#include "scene/gui/popup_menu.h" #include "scene/gui/scroll_bar.h" #include "scene/resources/text_paragraph.h" @@ -39,6 +40,13 @@ class RichTextLabel : public Control { GDCLASS(RichTextLabel, Control); public: + enum AutowrapMode { + AUTOWRAP_OFF, + AUTOWRAP_ARBITRARY, + AUTOWRAP_WORD, + AUTOWRAP_WORD_SMART + }; + enum ListType { LIST_NUMBERS, LIST_LETTERS, @@ -71,10 +79,24 @@ public: ITEM_BGCOLOR, ITEM_FGCOLOR, ITEM_META, + ITEM_HINT, ITEM_DROPCAP, ITEM_CUSTOMFX }; + enum VisibleCharactersBehavior { + VC_CHARS_BEFORE_SHAPING, + VC_CHARS_AFTER_SHAPING, + VC_GLYPHS_AUTO, + VC_GLYPHS_LTR, + VC_GLYPHS_RTL, + }; + + enum MenuItems { + MENU_COPY, + MENU_SELECT_ALL, + }; + protected: void _notification(int p_what); static void _bind_methods(); @@ -95,6 +117,10 @@ private: int char_count = 0; Line() { text_buf.instantiate(); } + + _FORCE_INLINE_ float get_height(float line_separation) const { + return offset.y + text_buf->get_size().y + text_buf->get_line_count() * line_separation; + } }; struct Item { @@ -119,9 +145,10 @@ private: struct ItemFrame : public Item { bool cell = false; - Vector<Line> lines; - int first_invalid_line = 0; - int first_resized_line = 0; + LocalVector<Line> lines; + std::atomic<int> first_invalid_line; + std::atomic<int> first_invalid_font_line; + std::atomic<int> first_resized_line; ItemFrame *parent_frame = nullptr; @@ -132,7 +159,12 @@ private: Size2 max_size_over = Size2(-1, -1); Rect2 padding; - ItemFrame() { type = ITEM_FRAME; } + ItemFrame() { + type = ITEM_FRAME; + first_invalid_line.store(0); + first_invalid_font_line.store(0); + first_resized_line.store(0); + } }; struct ItemText : public Item { @@ -202,11 +234,16 @@ private: ItemMeta() { type = ITEM_META; } }; + struct ItemHint : public Item { + String description; + ItemHint() { type = ITEM_HINT; } + }; + struct ItemParagraph : public Item { HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT; String language; Control::TextDirection direction = Control::TEXT_DIRECTION_AUTO; - Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + TextServer::StructuredTextParser st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; ItemParagraph() { type = ITEM_PARAGRAPH; } }; @@ -235,8 +272,8 @@ private: int width = 0; }; - Vector<Column> columns; - Vector<float> rows; + LocalVector<Column> columns; + LocalVector<float> rows; int total_width = 0; int total_height = 0; @@ -335,8 +372,20 @@ private: Item *current = nullptr; ItemFrame *current_frame = nullptr; + Thread thread; + Mutex data_mutex; + bool threaded = false; + std::atomic<bool> stop_thread; + std::atomic<bool> updating; + std::atomic<double> loaded; + + uint64_t loading_started = 0; + int progress_delay = 1000; + VScrollBar *vscroll = nullptr; + AutowrapMode autowrap_mode = AUTOWRAP_WORD_SMART; + bool scroll_visible = false; bool scroll_follow = false; bool scroll_following = false; @@ -351,6 +400,7 @@ private: int tab_size = 4; bool underline_meta = true; + bool underline_hint = true; bool override_selected_font_color = false; HorizontalAlignment default_alignment = HORIZONTAL_ALIGNMENT_LEFT; @@ -361,14 +411,18 @@ private: Array custom_effects; void _invalidate_current_line(ItemFrame *p_frame); - void _validate_line_caches(ItemFrame *p_frame); + + static void _thread_function(void *self); + void _stop_thread(); + bool _validate_line_caches(); + void _process_line_caches(); 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); String language; TextDirection text_direction = TEXT_DIRECTION_AUTO; - Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + TextServer::StructuredTextParser st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; Array st_args; struct Selection { @@ -389,24 +443,38 @@ private: bool active = false; // anything selected? i.e. from, to, etc. valid? bool enabled = false; // allow selections? + bool drag_attempt = false; }; Selection selection; bool deselect_on_focus_loss_enabled = true; + bool context_menu_enabled = false; + bool shortcut_keys_enabled = true; + + // Context menu. + PopupMenu *menu = nullptr; + void _generate_context_menu(); + Key _get_menu_action_accelerator(const String &p_action); + void _menu_option(int p_option); + int visible_characters = -1; float percent_visible = 1.0; + VisibleCharactersBehavior visible_chars_behavior = VC_CHARS_BEFORE_SHAPING; + bool _is_click_inside_selection() const; void _find_click(ItemFrame *p_frame, const Point2i &p_click, ItemFrame **r_click_frame = nullptr, int *r_click_line = nullptr, Item **r_click_item = nullptr, int *r_click_char = nullptr, bool *r_outside = nullptr); String _get_line_text(ItemFrame *p_frame, int p_line, Selection p_sel) const; bool _search_line(ItemFrame *p_frame, int p_line, const String &p_string, int p_char_idx, bool p_reverse_search); bool _search_table(ItemTable *p_table, List<Item *>::Element *p_from, const String &p_string, bool p_reverse_search); - void _shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width, int *r_char_offset); - void _resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width); - 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, int p_shadow_outline_size, const Point2 &p_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); + float _shape_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width, float p_h, int *r_char_offset); + float _resize_line(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size, int p_width, float p_h); + + void _update_line_font(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size); + 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, int p_shadow_outline_size, const Point2 &p_shadow_ofs, int &r_processed_glyphs); + 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, bool p_table = false); String _roman(int p_num, bool p_capitalize) const; String _letters(int p_num, bool p_capitalize) const; @@ -423,23 +491,27 @@ private: int _find_margin(Item *p_item, const Ref<Font> &p_base_font, int p_base_font_size); HorizontalAlignment _find_alignment(Item *p_item); TextServer::Direction _find_direction(Item *p_item); - Control::StructuredTextParser _find_stt(Item *p_item); + TextServer::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); + bool _find_hint(Item *p_item, String *r_description); Color _find_bgcolor(Item *p_item); Color _find_fgcolor(Item *p_item); bool _find_layout_subitem(Item *from, Item *to); void _fetch_item_fx_stack(Item *p_item, Vector<ItemFX *> &r_stack); - void _update_scroll(); void _update_fx(ItemFrame *p_frame, double p_delta_time); void _scroll_changed(double); + int _find_first_line(int p_from, int p_to, int p_vofs) const; + + _FORCE_INLINE_ float _calculate_line_vertical_offset(const Line &line) const; virtual void gui_input(const Ref<InputEvent> &p_event) override; + virtual String get_tooltip(const Point2 &p_pos) const 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; @@ -479,10 +551,11 @@ public: void push_outline_color(const Color &p_color); void push_underline(); void push_strikethrough(); - void push_paragraph(HorizontalAlignment p_alignment, Control::TextDirection p_direction = Control::TEXT_DIRECTION_INHERITED, const String &p_language = "", Control::StructuredTextParser p_st_parser = STRUCTURED_TEXT_DEFAULT); + void push_paragraph(HorizontalAlignment p_alignment, Control::TextDirection p_direction = Control::TEXT_DIRECTION_INHERITED, const String &p_language = "", TextServer::StructuredTextParser p_st_parser = TextServer::STRUCTURED_TEXT_DEFAULT); void push_indent(int p_level); void push_list(int p_level, ListType p_list, bool p_capitalize); void push_meta(const Variant &p_meta); + void push_hint(const String &p_string); void push_table(int p_columns, InlineAlignment p_alignment = INLINE_ALIGNMENT_TOP); void push_fade(int p_start_index, int p_length); void push_shake(int p_strength, float p_rate); @@ -508,6 +581,9 @@ public: void set_meta_underline(bool p_underline); bool is_meta_underlined() const; + void set_hint_underline(bool p_underline); + bool is_hint_underlined() const; + void set_override_selected_font_color(bool p_override_selected_font_color); bool is_overriding_selected_font_color() const; @@ -520,6 +596,12 @@ public: void set_tab_size(int p_spaces); int get_tab_size() const; + void set_context_menu_enabled(bool p_enabled); + bool is_context_menu_enabled() const; + + void set_shortcut_keys_enabled(bool p_enabled); + bool is_shortcut_keys_enabled() const; + void set_fit_content_height(bool p_enabled); bool is_fit_content_height_enabled() const; @@ -529,24 +611,43 @@ public: int get_paragraph_count() const; int get_visible_paragraph_count() const; + float get_line_offset(int p_line); + float get_paragraph_offset(int p_paragraph); + void scroll_to_line(int p_line); int get_line_count() const; int get_visible_line_count() const; int get_content_height() const; + int get_content_width() const; - VScrollBar *get_v_scroll() { return vscroll; } + VScrollBar *get_v_scroll_bar() { return vscroll; } virtual CursorShape get_cursor_shape(const Point2 &p_pos) const override; + virtual Variant get_drag_data(const Point2 &p_point) override; void set_selection_enabled(bool p_enabled); bool is_selection_enabled() const; int get_selection_from() const; int get_selection_to() const; String get_selected_text() const; + void select_all(); void selection_copy(); void set_deselect_on_focus_loss_enabled(const bool p_enabled); bool is_deselect_on_focus_loss_enabled() const; + void deselect(); + + bool is_ready() const; + + void set_threaded(bool p_threaded); + bool is_threaded() const; + + void set_progress_bar_delay(int p_delay_ms); + int get_progress_bar_delay() const; + + // Context menu. + PopupMenu *get_menu() const; + bool is_menu_visible() const; void parse_bbcode(const String &p_bbcode); void append_text(const String &p_bbcode); @@ -563,19 +664,28 @@ public: 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_autowrap_mode(AutowrapMode p_mode); + AutowrapMode get_autowrap_mode() const; + + void set_structured_text_bidi_override(TextServer::StructuredTextParser p_parser); + TextServer::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_character_line(int p_char); + int get_character_paragraph(int p_char); int get_total_character_count() const; + int get_total_glyph_count() const; void set_percent_visible(float p_percent); float get_percent_visible() const; + VisibleCharactersBehavior get_visible_characters_behavior() const; + void set_visible_characters_behavior(VisibleCharactersBehavior p_behavior); + void set_effects(Array p_effects); Array get_effects(); @@ -584,11 +694,13 @@ public: void set_fixed_size_to_width(int p_width); virtual Size2 get_minimum_size() const override; - RichTextLabel(); + RichTextLabel(const String &p_text = String()); ~RichTextLabel(); }; +VARIANT_ENUM_CAST(RichTextLabel::AutowrapMode); VARIANT_ENUM_CAST(RichTextLabel::ListType); VARIANT_ENUM_CAST(RichTextLabel::ItemType); +VARIANT_ENUM_CAST(RichTextLabel::VisibleCharactersBehavior); #endif // RICH_TEXT_LABEL_H diff --git a/scene/gui/scroll_bar.cpp b/scene/gui/scroll_bar.cpp index 8c292e663e..e1b0e8cca8 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -218,195 +218,198 @@ void ScrollBar::gui_input(const Ref<InputEvent> &p_event) { } void ScrollBar::_notification(int p_what) { - if (p_what == NOTIFICATION_DRAW) { - RID ci = get_canvas_item(); + switch (p_what) { + case NOTIFICATION_DRAW: { + RID ci = get_canvas_item(); - Ref<Texture2D> decr, incr; + Ref<Texture2D> decr, incr; - if (decr_active) { - decr = get_theme_icon(SNAME("decrement_pressed")); - } else if (highlight == HIGHLIGHT_DECR) { - decr = get_theme_icon(SNAME("decrement_highlight")); - } else { - decr = get_theme_icon(SNAME("decrement")); - } + if (decr_active) { + decr = get_theme_icon(SNAME("decrement_pressed")); + } else if (highlight == HIGHLIGHT_DECR) { + decr = get_theme_icon(SNAME("decrement_highlight")); + } else { + decr = get_theme_icon(SNAME("decrement")); + } - if (incr_active) { - incr = get_theme_icon(SNAME("increment_pressed")); - } else if (highlight == HIGHLIGHT_INCR) { - incr = get_theme_icon(SNAME("increment_highlight")); - } else { - incr = get_theme_icon(SNAME("increment")); - } + if (incr_active) { + incr = get_theme_icon(SNAME("increment_pressed")); + } else if (highlight == HIGHLIGHT_INCR) { + incr = get_theme_icon(SNAME("increment_highlight")); + } else { + incr = get_theme_icon(SNAME("increment")); + } - Ref<StyleBox> bg = has_focus() ? get_theme_stylebox(SNAME("scroll_focus")) : get_theme_stylebox(SNAME("scroll")); + Ref<StyleBox> bg = has_focus() ? get_theme_stylebox(SNAME("scroll_focus")) : get_theme_stylebox(SNAME("scroll")); - Ref<StyleBox> grabber; - if (drag.active) { - grabber = get_theme_stylebox(SNAME("grabber_pressed")); - } else if (highlight == HIGHLIGHT_RANGE) { - grabber = get_theme_stylebox(SNAME("grabber_highlight")); - } else { - grabber = get_theme_stylebox(SNAME("grabber")); - } + Ref<StyleBox> grabber; + if (drag.active) { + grabber = get_theme_stylebox(SNAME("grabber_pressed")); + } else if (highlight == HIGHLIGHT_RANGE) { + grabber = get_theme_stylebox(SNAME("grabber_highlight")); + } else { + grabber = get_theme_stylebox(SNAME("grabber")); + } - Point2 ofs; + Point2 ofs; - decr->draw(ci, Point2()); + decr->draw(ci, Point2()); - if (orientation == HORIZONTAL) { - ofs.x += decr->get_width(); - } else { - ofs.y += decr->get_height(); - } + if (orientation == HORIZONTAL) { + ofs.x += decr->get_width(); + } else { + ofs.y += decr->get_height(); + } - Size2 area = get_size(); + Size2 area = get_size(); - if (orientation == HORIZONTAL) { - area.width -= incr->get_width() + decr->get_width(); - } else { - area.height -= incr->get_height() + decr->get_height(); - } + if (orientation == HORIZONTAL) { + area.width -= incr->get_width() + decr->get_width(); + } else { + area.height -= incr->get_height() + decr->get_height(); + } - bg->draw(ci, Rect2(ofs, area)); + bg->draw(ci, Rect2(ofs, area)); - if (orientation == HORIZONTAL) { - ofs.width += area.width; - } else { - ofs.height += area.height; - } + if (orientation == HORIZONTAL) { + ofs.width += area.width; + } else { + ofs.height += area.height; + } - incr->draw(ci, ofs); - Rect2 grabber_rect; + incr->draw(ci, ofs); + Rect2 grabber_rect; - if (orientation == HORIZONTAL) { - 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(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(SIDE_TOP); - grabber_rect.position.x = 0; - } + if (orientation == HORIZONTAL) { + 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(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(SIDE_TOP); + grabber_rect.position.x = 0; + } - grabber->draw(ci, grabber_rect); - } + grabber->draw(ci, grabber_rect); + } break; - if (p_what == NOTIFICATION_ENTER_TREE) { - if (has_node(drag_node_path)) { - Node *n = get_node(drag_node_path); - drag_node = Object::cast_to<Control>(n); - } + case NOTIFICATION_ENTER_TREE: { + if (has_node(drag_node_path)) { + Node *n = get_node(drag_node_path); + drag_node = Object::cast_to<Control>(n); + } - if (drag_node) { - drag_node->connect("gui_input", callable_mp(this, &ScrollBar::_drag_node_input)); - drag_node->connect("tree_exiting", callable_mp(this, &ScrollBar::_drag_node_exit), varray(), CONNECT_ONESHOT); - } - } - if (p_what == NOTIFICATION_EXIT_TREE) { - if (drag_node) { - drag_node->disconnect("gui_input", callable_mp(this, &ScrollBar::_drag_node_input)); - drag_node->disconnect("tree_exiting", callable_mp(this, &ScrollBar::_drag_node_exit)); - } + if (drag_node) { + drag_node->connect("gui_input", callable_mp(this, &ScrollBar::_drag_node_input)); + drag_node->connect("tree_exiting", callable_mp(this, &ScrollBar::_drag_node_exit), varray(), CONNECT_ONESHOT); + } + } break; - drag_node = nullptr; - } + case NOTIFICATION_EXIT_TREE: { + if (drag_node) { + drag_node->disconnect("gui_input", callable_mp(this, &ScrollBar::_drag_node_input)); + drag_node->disconnect("tree_exiting", callable_mp(this, &ScrollBar::_drag_node_exit)); + } - if (p_what == NOTIFICATION_INTERNAL_PHYSICS_PROCESS) { - if (scrolling) { - if (get_value() != target_scroll) { - double target = target_scroll - get_value(); - double dist = sqrt(target * target); - double vel = ((target / dist) * 500) * get_physics_process_delta_time(); + drag_node = nullptr; + } break; - if (Math::abs(vel) >= dist) { - set_value(target_scroll); + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + if (scrolling) { + if (get_value() != target_scroll) { + double target = target_scroll - get_value(); + double dist = sqrt(target * target); + double vel = ((target / dist) * 500) * get_physics_process_delta_time(); + + if (Math::abs(vel) >= dist) { + set_value(target_scroll); + scrolling = false; + set_physics_process_internal(false); + } else { + set_value(get_value() + vel); + } + } else { scrolling = false; set_physics_process_internal(false); - } else { - set_value(get_value() + vel); } - } else { - scrolling = false; - set_physics_process_internal(false); - } - } else if (drag_node_touching) { - if (drag_node_touching_deaccel) { - Vector2 pos = Vector2(orientation == HORIZONTAL ? get_value() : 0, orientation == VERTICAL ? get_value() : 0); - pos += drag_node_speed * get_physics_process_delta_time(); + } else if (drag_node_touching) { + if (drag_node_touching_deaccel) { + Vector2 pos = Vector2(orientation == HORIZONTAL ? get_value() : 0, orientation == VERTICAL ? get_value() : 0); + pos += drag_node_speed * get_physics_process_delta_time(); - bool turnoff = false; + bool turnoff = false; - if (orientation == HORIZONTAL) { - if (pos.x < 0) { - pos.x = 0; - turnoff = true; - } + if (orientation == HORIZONTAL) { + if (pos.x < 0) { + pos.x = 0; + turnoff = true; + } - if (pos.x > (get_max() - get_page())) { - pos.x = get_max() - get_page(); - turnoff = true; - } + if (pos.x > (get_max() - get_page())) { + pos.x = get_max() - get_page(); + turnoff = true; + } - set_value(pos.x); + set_value(pos.x); - float sgn_x = drag_node_speed.x < 0 ? -1 : 1; - float val_x = Math::abs(drag_node_speed.x); - val_x -= 1000 * get_physics_process_delta_time(); + float sgn_x = drag_node_speed.x < 0 ? -1 : 1; + float val_x = Math::abs(drag_node_speed.x); + val_x -= 1000 * get_physics_process_delta_time(); - if (val_x < 0) { - turnoff = true; - } + if (val_x < 0) { + turnoff = true; + } - drag_node_speed.x = sgn_x * val_x; + drag_node_speed.x = sgn_x * val_x; - } else { - if (pos.y < 0) { - pos.y = 0; - turnoff = true; - } + } else { + if (pos.y < 0) { + pos.y = 0; + turnoff = true; + } - if (pos.y > (get_max() - get_page())) { - pos.y = get_max() - get_page(); - turnoff = true; - } + if (pos.y > (get_max() - get_page())) { + pos.y = get_max() - get_page(); + turnoff = true; + } - set_value(pos.y); + set_value(pos.y); - float sgn_y = drag_node_speed.y < 0 ? -1 : 1; - float val_y = Math::abs(drag_node_speed.y); - val_y -= 1000 * get_physics_process_delta_time(); + float sgn_y = drag_node_speed.y < 0 ? -1 : 1; + float val_y = Math::abs(drag_node_speed.y); + val_y -= 1000 * get_physics_process_delta_time(); - if (val_y < 0) { - turnoff = true; + if (val_y < 0) { + turnoff = true; + } + drag_node_speed.y = sgn_y * val_y; } - drag_node_speed.y = sgn_y * val_y; - } - if (turnoff) { - set_physics_process_internal(false); - drag_node_touching = false; - drag_node_touching_deaccel = false; - } + if (turnoff) { + set_physics_process_internal(false); + drag_node_touching = false; + drag_node_touching_deaccel = false; + } - } else { - if (time_since_motion == 0 || time_since_motion > 0.1) { - Vector2 diff = drag_node_accum - last_drag_node_accum; - last_drag_node_accum = drag_node_accum; - drag_node_speed = diff / get_physics_process_delta_time(); - } + } else { + if (time_since_motion == 0 || time_since_motion > 0.1) { + Vector2 diff = drag_node_accum - last_drag_node_accum; + last_drag_node_accum = drag_node_accum; + drag_node_speed = diff / get_physics_process_delta_time(); + } - time_since_motion += get_physics_process_delta_time(); + time_since_motion += get_physics_process_delta_time(); + } } - } - } + } break; - if (p_what == NOTIFICATION_MOUSE_EXIT) { - highlight = HIGHLIGHT_NONE; - update(); + case NOTIFICATION_MOUSE_EXIT: { + highlight = HIGHLIGHT_NONE; + update(); + } break; } } @@ -423,11 +426,6 @@ double ScrollBar::get_grabber_size() const { } float page = (get_page() > 0) ? get_page() : 0; - /* - if (grabber_range < get_step()) - grabber_range=get_step(); - */ - double area_size = get_area_size(); double grabber_size = page / range * area_size; return grabber_size + get_grabber_min_size(); diff --git a/scene/gui/scroll_bar.h b/scene/gui/scroll_bar.h index 574d17ee20..651edd1a74 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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/scroll_container.cpp b/scene/gui/scroll_container.cpp index dcd2c32a3b..135bad4689 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +29,8 @@ /*************************************************************************/ #include "scroll_container.h" + +#include "core/config/project_settings.h" #include "core/os/os.h" #include "scene/main/window.h" @@ -314,97 +316,102 @@ void ScrollContainer::_update_dimensions() { } void ScrollContainer::_notification(int p_what) { - if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_THEME_CHANGED || p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { - _updating_scrollbars = true; - call_deferred(SNAME("_update_scrollbar_position")); - }; - - if (p_what == NOTIFICATION_READY) { - Viewport *viewport = get_viewport(); - ERR_FAIL_COND(!viewport); - 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(SNAME("bg")); - draw_style_box(sb, Rect2(Vector2(), get_size())); - - update_scrollbars(); - } - - if (p_what == NOTIFICATION_INTERNAL_PHYSICS_PROCESS) { - if (drag_touching) { - if (drag_touching_deaccel) { - Vector2 pos = Vector2(h_scroll->get_value(), v_scroll->get_value()); - pos += drag_speed * get_physics_process_delta_time(); - - bool turnoff_h = false; - bool turnoff_v = false; - - if (pos.x < 0) { - pos.x = 0; - turnoff_h = true; - } - if (pos.x > (h_scroll->get_max() - h_scroll->get_page())) { - pos.x = h_scroll->get_max() - h_scroll->get_page(); - turnoff_h = true; - } - - if (pos.y < 0) { - pos.y = 0; - turnoff_v = true; - } - if (pos.y > (v_scroll->get_max() - v_scroll->get_page())) { - pos.y = v_scroll->get_max() - v_scroll->get_page(); - turnoff_v = true; - } - - if (horizontal_scroll_mode != SCROLL_MODE_DISABLED) { - h_scroll->set_value(pos.x); - } - if (vertical_scroll_mode != SCROLL_MODE_DISABLED) { - v_scroll->set_value(pos.y); - } - - float sgn_x = drag_speed.x < 0 ? -1 : 1; - float val_x = Math::abs(drag_speed.x); - val_x -= 1000 * get_physics_process_delta_time(); - - if (val_x < 0) { - turnoff_h = true; - } - - float sgn_y = drag_speed.y < 0 ? -1 : 1; - float val_y = Math::abs(drag_speed.y); - val_y -= 1000 * get_physics_process_delta_time(); - - if (val_y < 0) { - turnoff_v = true; - } - - drag_speed = Vector2(sgn_x * val_x, sgn_y * val_y); + switch (p_what) { + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_THEME_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: { + _updating_scrollbars = true; + call_deferred(SNAME("_update_scrollbar_position")); + } break; + + case NOTIFICATION_READY: { + Viewport *viewport = get_viewport(); + ERR_FAIL_COND(!viewport); + viewport->connect("gui_focus_changed", callable_mp(this, &ScrollContainer::_gui_focus_changed)); + _update_dimensions(); + } break; + + case NOTIFICATION_SORT_CHILDREN: { + _update_dimensions(); + } break; + + case NOTIFICATION_DRAW: { + Ref<StyleBox> sb = get_theme_stylebox(SNAME("bg")); + draw_style_box(sb, Rect2(Vector2(), get_size())); + + update_scrollbars(); + } break; + + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + if (drag_touching) { + if (drag_touching_deaccel) { + Vector2 pos = Vector2(h_scroll->get_value(), v_scroll->get_value()); + pos += drag_speed * get_physics_process_delta_time(); + + bool turnoff_h = false; + bool turnoff_v = false; + + if (pos.x < 0) { + pos.x = 0; + turnoff_h = true; + } + if (pos.x > (h_scroll->get_max() - h_scroll->get_page())) { + pos.x = h_scroll->get_max() - h_scroll->get_page(); + turnoff_h = true; + } + + if (pos.y < 0) { + pos.y = 0; + turnoff_v = true; + } + if (pos.y > (v_scroll->get_max() - v_scroll->get_page())) { + pos.y = v_scroll->get_max() - v_scroll->get_page(); + turnoff_v = true; + } + + if (horizontal_scroll_mode != SCROLL_MODE_DISABLED) { + h_scroll->set_value(pos.x); + } + if (vertical_scroll_mode != SCROLL_MODE_DISABLED) { + v_scroll->set_value(pos.y); + } + + float sgn_x = drag_speed.x < 0 ? -1 : 1; + float val_x = Math::abs(drag_speed.x); + val_x -= 1000 * get_physics_process_delta_time(); + + if (val_x < 0) { + turnoff_h = true; + } + + float sgn_y = drag_speed.y < 0 ? -1 : 1; + float val_y = Math::abs(drag_speed.y); + val_y -= 1000 * get_physics_process_delta_time(); + + if (val_y < 0) { + turnoff_v = true; + } + + drag_speed = Vector2(sgn_x * val_x, sgn_y * val_y); + + if (turnoff_h && turnoff_v) { + _cancel_drag(); + } - if (turnoff_h && turnoff_v) { - _cancel_drag(); - } + } else { + if (time_since_motion == 0 || time_since_motion > 0.1) { + Vector2 diff = drag_accum - last_drag_accum; + last_drag_accum = drag_accum; + drag_speed = diff / get_physics_process_delta_time(); + } - } else { - if (time_since_motion == 0 || time_since_motion > 0.1) { - Vector2 diff = drag_accum - last_drag_accum; - last_drag_accum = drag_accum; - drag_speed = diff / get_physics_process_delta_time(); + time_since_motion += get_physics_process_delta_time(); } - - time_since_motion += get_physics_process_delta_time(); } - } + } break; } -}; +} void ScrollContainer::update_scrollbars() { Size2 size = get_size(); @@ -526,17 +533,17 @@ TypedArray<String> ScrollContainer::get_configuration_warnings() const { } if (found != 1) { - 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.")); + warnings.push_back(RTR("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 warnings; } -HScrollBar *ScrollContainer::get_h_scrollbar() { +HScrollBar *ScrollContainer::get_h_scroll_bar() { return h_scroll; } -VScrollBar *ScrollContainer::get_v_scrollbar() { +VScrollBar *ScrollContainer::get_v_scroll_bar() { return v_scroll; } @@ -561,8 +568,8 @@ void ScrollContainer::_bind_methods() { 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("get_h_scroll_bar"), &ScrollContainer::get_h_scroll_bar); + ClassDB::bind_method(D_METHOD("get_v_scroll_bar"), &ScrollContainer::get_v_scroll_bar); ClassDB::bind_method(D_METHOD("ensure_control_visible", "control"), &ScrollContainer::ensure_control_visible); ADD_SIGNAL(MethodInfo("scroll_started")); diff --git a/scene/gui/scroll_container.h b/scene/gui/scroll_container.h index 0cec4db57a..b9fcf64db6 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,8 +47,8 @@ public: }; private: - HScrollBar *h_scroll; - VScrollBar *v_scroll; + HScrollBar *h_scroll = nullptr; + VScrollBar *v_scroll = nullptr; Size2 child_max_size; @@ -105,8 +105,8 @@ public: bool is_following_focus() const; void set_follow_focus(bool p_follow); - HScrollBar *get_h_scrollbar(); - VScrollBar *get_v_scrollbar(); + HScrollBar *get_h_scroll_bar(); + VScrollBar *get_v_scroll_bar(); void ensure_control_visible(Control *p_control); TypedArray<String> get_configuration_warnings() const override; diff --git a/scene/gui/separator.cpp b/scene/gui/separator.cpp index 1f3cb7aa24..e3400d9c8f 100644 --- a/scene/gui/separator.cpp +++ b/scene/gui/separator.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 @@ void Separator::_notification(int p_what) { } else { style->draw(get_canvas_item(), Rect2(0, (size.y - ssize.y) / 2, size.x, ssize.y)); } - } break; } } diff --git a/scene/gui/separator.h b/scene/gui/separator.h index 77162c68fa..1621bb3351 100644 --- a/scene/gui/separator.h +++ b/scene/gui/separator.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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/slider.cpp b/scene/gui/slider.cpp index f8cabe172c..4b680f72cf 100644 --- a/scene/gui/slider.cpp +++ b/scene/gui/slider.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +29,7 @@ /*************************************************************************/ #include "slider.h" + #include "core/os/keyboard.h" Size2 Slider::get_minimum_size() const { @@ -70,8 +71,13 @@ void Slider::gui_input(const Ref<InputEvent> &p_event) { } grab.active = true; grab.uvalue = get_as_ratio(); + + emit_signal(SNAME("drag_started")); } else { grab.active = false; + + const bool value_changed = !Math::is_equal_approx((double)grab.uvalue, get_as_ratio()); + emit_signal(SNAME("drag_ended"), value_changed); } } else if (scrollable) { if (mb->is_pressed() && mb->get_button_index() == MouseButton::WHEEL_UP) { @@ -145,19 +151,23 @@ void Slider::_notification(int p_what) { update_minimum_size(); update(); } break; + case NOTIFICATION_MOUSE_ENTER: { mouse_inside = true; update(); } break; + case NOTIFICATION_MOUSE_EXIT: { mouse_inside = false; update(); } break; - case NOTIFICATION_VISIBILITY_CHANGED: // fallthrough + + case NOTIFICATION_VISIBILITY_CHANGED: case NOTIFICATION_EXIT_TREE: { mouse_inside = false; grab.active = false; } break; + case NOTIFICATION_DRAW: { RID ci = get_canvas_item(); Size2i size = get_size(); @@ -204,7 +214,6 @@ void Slider::_notification(int p_what) { } grabber->draw(ci, Point2i(ratio * areasize, size.height / 2 - grabber->get_size().height / 2)); } - } break; } } @@ -264,6 +273,9 @@ void Slider::_bind_methods() { ClassDB::bind_method(D_METHOD("set_scrollable", "scrollable"), &Slider::set_scrollable); ClassDB::bind_method(D_METHOD("is_scrollable"), &Slider::is_scrollable); + ADD_SIGNAL(MethodInfo("drag_started")); + ADD_SIGNAL(MethodInfo("drag_ended", PropertyInfo(Variant::BOOL, "value_changed"))); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editable"), "set_editable", "is_editable"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scrollable"), "set_scrollable", "is_scrollable"); ADD_PROPERTY(PropertyInfo(Variant::INT, "tick_count", PROPERTY_HINT_RANGE, "0,4096,1"), "set_ticks", "get_ticks"); diff --git a/scene/gui/slider.h b/scene/gui/slider.h index 46fa08bbf0..5fbfee2aec 100644 --- a/scene/gui/slider.h +++ b/scene/gui/slider.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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/spin_box.cpp b/scene/gui/spin_box.cpp index 8d6315d085..e50d7e765c 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,7 @@ Size2 SpinBox::get_minimum_size() const { return ms; } -void SpinBox::_value_changed(double) { +void SpinBox::_value_changed(double p_value) { String value = TS->format_number(String::num(get_value(), Math::range_step_decimals(get_step()))); if (!prefix.is_empty()) { value = prefix + " " + value; @@ -48,6 +48,7 @@ void SpinBox::_value_changed(double) { value += " " + suffix; } line_edit->set_text(value); + Range::_value_changed(p_value); } void SpinBox::_text_submitted(const String &p_string) { @@ -196,34 +197,44 @@ inline void SpinBox::_adjust_width_for_icon(const Ref<Texture2D> &icon) { } void SpinBox::_notification(int p_what) { - if (p_what == NOTIFICATION_DRAW) { - Ref<Texture2D> updown = get_theme_icon(SNAME("updown")); - - _adjust_width_for_icon(updown); - - RID ci = get_canvas_item(); - Size2i size = get_size(); - - 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(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(SNAME("update_minimum_size")); - get_line_edit()->call_deferred(SNAME("update_minimum_size")); - } else if (p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { - update(); + switch (p_what) { + case NOTIFICATION_DRAW: { + Ref<Texture2D> updown = get_theme_icon(SNAME("updown")); + + _adjust_width_for_icon(updown); + + RID ci = get_canvas_item(); + Size2i size = get_size(); + + 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)); + } + } break; + + case NOTIFICATION_ENTER_TREE: { + _adjust_width_for_icon(get_theme_icon(SNAME("updown"))); + _value_changed(0); + } break; + + case NOTIFICATION_EXIT_TREE: { + _release_mouse(); + } break; + + case NOTIFICATION_TRANSLATION_CHANGED: { + _value_changed(0); + update(); + } break; + + case NOTIFICATION_THEME_CHANGED: { + call_deferred(SNAME("update_minimum_size")); + get_line_edit()->call_deferred(SNAME("update_minimum_size")); + } break; + + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { + update(); + } break; } } diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h index 736a5d873d..d118b28334 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,16 +38,16 @@ class SpinBox : public Range { GDCLASS(SpinBox, Range); - LineEdit *line_edit; + LineEdit *line_edit = nullptr; int last_w = 0; bool update_on_text_changed = false; - Timer *range_click_timer; + Timer *range_click_timer = nullptr; void _range_click_timeout(); void _release_mouse(); void _text_submitted(const String &p_string); - virtual void _value_changed(double) override; + virtual void _value_changed(double p_value) override; void _text_changed(const String &p_string); String prefix; diff --git a/scene/gui/split_container.cpp b/scene/gui/split_container.cpp index 106bb7949f..6845d46721 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -168,15 +168,18 @@ void SplitContainer::_notification(int p_what) { 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(SNAME("autohide"))) { update(); } } break; + case NOTIFICATION_DRAW: { if (!_getch(0) || !_getch(1)) { return; @@ -200,6 +203,7 @@ void SplitContainer::_notification(int p_what) { draw_texture(tex, Point2i(middle_sep + (sep - tex->get_width()) / 2, (size.y - tex->get_height()) / 2)); } } break; + case NOTIFICATION_THEME_CHANGED: { update_minimum_size(); } break; @@ -336,6 +340,30 @@ bool SplitContainer::is_collapsed() const { return collapsed; } +Vector<int> SplitContainer::get_allowed_size_flags_horizontal() const { + Vector<int> flags; + flags.append(SIZE_FILL); + if (!vertical) { + flags.append(SIZE_EXPAND); + } + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + +Vector<int> SplitContainer::get_allowed_size_flags_vertical() const { + Vector<int> flags; + flags.append(SIZE_FILL); + if (vertical) { + flags.append(SIZE_EXPAND); + } + flags.append(SIZE_SHRINK_BEGIN); + flags.append(SIZE_SHRINK_CENTER); + flags.append(SIZE_SHRINK_END); + return flags; +} + void SplitContainer::_bind_methods() { 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); diff --git a/scene/gui/split_container.h b/scene/gui/split_container.h index 47fd30a122..a69ffe4de9 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -79,6 +79,9 @@ public: virtual Size2 get_minimum_size() const override; + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + SplitContainer(bool p_vertical = false); }; diff --git a/scene/gui/subviewport_container.cpp b/scene/gui/subviewport_container.cpp index 53ea32e1b7..68281b6a72 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +54,7 @@ Size2 SubViewportContainer::get_minimum_size() const { void SubViewportContainer::set_stretch(bool p_enable) { stretch = p_enable; + update_minimum_size(); queue_sort(); update(); } @@ -90,52 +91,81 @@ int SubViewportContainer::get_stretch_shrink() const { return shrink; } -void SubViewportContainer::_notification(int p_what) { - if (p_what == NOTIFICATION_RESIZED) { - if (!stretch) { - return; - } +Vector<int> SubViewportContainer::get_allowed_size_flags_horizontal() const { + return Vector<int>(); +} - for (int i = 0; i < get_child_count(); i++) { - SubViewport *c = Object::cast_to<SubViewport>(get_child(i)); - if (!c) { - continue; +Vector<int> SubViewportContainer::get_allowed_size_flags_vertical() const { + return Vector<int>(); +} + +void SubViewportContainer::_notification(int p_what) { + switch (p_what) { + case NOTIFICATION_RESIZED: { + if (!stretch) { + return; } - c->set_size(get_size() / shrink); - } - } + for (int i = 0; i < get_child_count(); i++) { + SubViewport *c = Object::cast_to<SubViewport>(get_child(i)); + if (!c) { + continue; + } - if (p_what == NOTIFICATION_ENTER_TREE || p_what == NOTIFICATION_VISIBILITY_CHANGED) { - for (int i = 0; i < get_child_count(); i++) { - SubViewport *c = Object::cast_to<SubViewport>(get_child(i)); - if (!c) { - continue; + c->set_size(get_size() / shrink); } - - if (is_visible_in_tree()) { - c->set_update_mode(SubViewport::UPDATE_ALWAYS); - } else { - c->set_update_mode(SubViewport::UPDATE_DISABLED); + } break; + + case NOTIFICATION_ENTER_TREE: + case NOTIFICATION_VISIBILITY_CHANGED: { + for (int i = 0; i < get_child_count(); i++) { + SubViewport *c = Object::cast_to<SubViewport>(get_child(i)); + if (!c) { + continue; + } + + if (is_visible_in_tree()) { + c->set_update_mode(SubViewport::UPDATE_ALWAYS); + } else { + c->set_update_mode(SubViewport::UPDATE_DISABLED); + } + + c->set_handle_input_locally(false); //do not handle input locally here } + } break; + + case NOTIFICATION_DRAW: { + for (int i = 0; i < get_child_count(); i++) { + SubViewport *c = Object::cast_to<SubViewport>(get_child(i)); + if (!c) { + continue; + } + + if (stretch) { + draw_texture_rect(c->get_texture(), Rect2(Vector2(), get_size())); + } else { + draw_texture_rect(c->get_texture(), Rect2(Vector2(), c->get_size())); + } + } + } break; - c->set_handle_input_locally(false); //do not handle input locally here - } - } + case NOTIFICATION_MOUSE_ENTER: { + _notify_viewports(NOTIFICATION_VP_MOUSE_ENTER); + } break; - if (p_what == NOTIFICATION_DRAW) { - for (int i = 0; i < get_child_count(); i++) { - SubViewport *c = Object::cast_to<SubViewport>(get_child(i)); - if (!c) { - continue; - } + case NOTIFICATION_MOUSE_EXIT: { + _notify_viewports(NOTIFICATION_VP_MOUSE_EXIT); + } break; + } +} - if (stretch) { - draw_texture_rect(c->get_texture(), Rect2(Vector2(), get_size())); - } else { - draw_texture_rect(c->get_texture(), Rect2(Vector2(), c->get_size())); - } +void SubViewportContainer::_notify_viewports(int p_notification) { + for (int i = 0; i < get_child_count(); i++) { + SubViewport *c = Object::cast_to<SubViewport>(get_child(i)); + if (!c) { + continue; } + c->notification(p_notification); } } @@ -146,7 +176,7 @@ void SubViewportContainer::input(const Ref<InputEvent> &p_event) { return; } - Transform2D xform = get_global_transform(); + Transform2D xform = get_global_transform_with_canvas(); if (stretch) { Transform2D scale_xf; @@ -173,7 +203,7 @@ void SubViewportContainer::unhandled_input(const Ref<InputEvent> &p_event) { return; } - Transform2D xform = get_global_transform(); + Transform2D xform = get_global_transform_with_canvas(); if (stretch) { Transform2D scale_xf; @@ -193,6 +223,23 @@ void SubViewportContainer::unhandled_input(const Ref<InputEvent> &p_event) { } } +TypedArray<String> SubViewportContainer::get_configuration_warnings() const { + TypedArray<String> warnings = Node::get_configuration_warnings(); + + bool has_viewport = false; + for (int i = 0; i < get_child_count(); i++) { + if (Object::cast_to<SubViewport>(get_child(i))) { + has_viewport = true; + break; + } + } + if (!has_viewport) { + warnings.push_back(RTR("This node doesn't have a SubViewport as child, so it can't display its intended content.\nConsider adding a SubViewport as a child to provide something displayable.")); + } + + return warnings; +} + void SubViewportContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_stretch", "enable"), &SubViewportContainer::set_stretch); ClassDB::bind_method(D_METHOD("is_stretch_enabled"), &SubViewportContainer::is_stretch_enabled); diff --git a/scene/gui/subviewport_container.h b/scene/gui/subviewport_container.h index 7853f1590e..55b7802aa4 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,6 +38,7 @@ class SubViewportContainer : public Container { bool stretch = false; int shrink = 1; + void _notify_viewports(int p_notification); protected: void _notification(int p_what); @@ -54,6 +55,11 @@ public: virtual Size2 get_minimum_size() const override; + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + + TypedArray<String> get_configuration_warnings() const override; + SubViewportContainer(); }; diff --git a/scene/gui/tab_bar.cpp b/scene/gui/tab_bar.cpp index 3d88c117e7..fec8d11bef 100644 --- a/scene/gui/tab_bar.cpp +++ b/scene/gui/tab_bar.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,54 +32,77 @@ #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" +#include "scene/main/viewport.h" Size2 TabBar::get_minimum_size() const { + Size2 ms; + + if (tabs.is_empty()) { + return ms; + } + 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<StyleBox> button_highlight = get_theme_stylebox(SNAME("button_highlight")); + Ref<Texture2D> close = get_theme_icon(SNAME("close")); + int hseparation = get_theme_constant(SNAME("h_separation")); 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, 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.is_empty()) { - ms.width += get_theme_constant(SNAME("hseparation")); - } + if (tabs[i].hidden) { + continue; } - 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); + int ofs = ms.width; + Ref<StyleBox> style; if (tabs[i].disabled) { - ms.width += tab_disabled->get_minimum_size().width; + style = tab_disabled; } else if (current == i) { - ms.width += tab_selected->get_minimum_size().width; + style = tab_selected; } else { - ms.width += tab_unselected->get_minimum_size().width; + style = tab_unselected; } + ms.width += style->get_minimum_size().width; + + Ref<Texture2D> tex = tabs[i].icon; + if (tex.is_valid()) { + ms.height = MAX(ms.height, tex->get_size().height + y_margin); + ms.width += tex->get_size().width + hseparation; + } + + if (!tabs[i].text.is_empty()) { + ms.width += tabs[i].size_text + hseparation; + } + ms.height = MAX(ms.height, tabs[i].text_buf->get_size().y + y_margin); + + bool close_visible = cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current); if (tabs[i].right_button.is_valid()) { Ref<Texture2D> rb = tabs[i].right_button; - Size2 bms = rb->get_size(); - bms.width += get_theme_constant(SNAME("hseparation")); - ms.width += bms.width; - ms.height = MAX(bms.height + tab_unselected->get_minimum_size().height, ms.height); + + if (close_visible) { + ms.width += button_highlight->get_minimum_size().width + rb->get_width(); + } else { + ms.width += button_highlight->get_margin(SIDE_LEFT) + rb->get_width() + hseparation; + } + + ms.height = MAX(ms.height, rb->get_height() + y_margin); + } + + if (close_visible) { + ms.width += button_highlight->get_margin(SIDE_LEFT) + close->get_width() + hseparation; + + ms.height = MAX(ms.height, close->get_height() + y_margin); } - if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current)) { - Ref<Texture2D> cb = get_theme_icon(SNAME("close")); - Size2 bms = cb->get_size(); - bms.width += get_theme_constant(SNAME("hseparation")); - ms.width += bms.width; - ms.height = MAX(bms.height + tab_unselected->get_minimum_size().height, ms.height); + if (ms.width - ofs > style->get_minimum_size().width) { + ms.width -= hseparation; } } @@ -136,7 +159,13 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { } } + if (get_viewport()->gui_is_dragging() && can_drop_data(pos, get_viewport()->gui_get_drag_data())) { + dragging_valid_tab = true; + update(); + } + _update_hover(); + return; } @@ -147,6 +176,7 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { if (scrolling_enabled && buttons_visible) { if (offset > 0) { offset--; + _update_cache(); update(); } } @@ -154,9 +184,9 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { if (mb->is_pressed() && mb->get_button_index() == MouseButton::WHEEL_DOWN && !mb->is_command_pressed()) { if (scrolling_enabled && buttons_visible) { - if (missing_right) { + if (missing_right && offset < tabs.size()) { offset++; - _ensure_no_over_offset(); // Avoid overreaching when scrolling fast. + _update_cache(); update(); } } @@ -164,8 +194,7 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { if (rb_pressing && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { if (rb_hover != -1) { - // Right mouse button clicked. - emit_signal(SNAME("tab_rmb_clicked"), rb_hover); + emit_signal(SNAME("tab_button_pressed"), rb_hover); } rb_pressing = false; @@ -174,7 +203,6 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { if (cb_pressing && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { if (cb_hover != -1) { - // Close button pressed. emit_signal(SNAME("tab_close_pressed"), cb_hover); } @@ -183,7 +211,6 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { } if (mb->is_pressed() && (mb->get_button_index() == MouseButton::LEFT || (select_with_rmb && mb->get_button_index() == MouseButton::RIGHT))) { - // Clicks. Point2 pos = mb->get_position(); if (buttons_visible) { @@ -194,12 +221,14 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { if (pos.x < decr->get_width()) { if (missing_right) { offset++; + _update_cache(); update(); } return; } else if (pos.x < incr->get_width() + decr->get_width()) { if (offset > 0) { offset--; + _update_cache(); update(); } return; @@ -209,12 +238,14 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { if (pos.x > limit + decr->get_width()) { if (missing_right) { offset++; + _update_cache(); update(); } return; } else if (pos.x > limit) { if (offset > 0) { offset--; + _update_cache(); update(); } return; @@ -229,6 +260,10 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { int found = -1; for (int i = offset; i <= max_drawn_tab; i++) { + if (tabs[i].hidden) { + continue; + } + if (tabs[i].rb_rect.has_point(pos)) { rb_pressing = true; update(); @@ -251,6 +286,12 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) { if (found != -1) { set_current_tab(found); + + if (mb->get_button_index() == MouseButton::RIGHT) { + // Right mouse button clicked. + emit_signal(SNAME("tab_rmb_clicked"), found); + } + emit_signal(SNAME("tab_clicked"), found); } } @@ -270,32 +311,46 @@ void TabBar::_shape(int p_tab) { 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.is_empty() ? tabs[p_tab].language : TranslationServer::get_singleton()->get_tool_locale()); + tabs.write[p_tab].text_buf->add_string(tabs[p_tab].xl_text, font, font_size, tabs[p_tab].opentype_features, !tabs[p_tab].language.is_empty() ? tabs[p_tab].language : TranslationServer::get_singleton()->get_tool_locale()); } void TabBar::_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) { _shape(i); } - _update_cache(); - update_minimum_size(); - update(); - } break; + + [[fallthrough]]; + } case NOTIFICATION_RESIZED: { + int ofs_old = offset; + int max_old = max_drawn_tab; + _update_cache(); _ensure_no_over_offset(); - ensure_tab_visible(current); + + if (scroll_to_selected && (offset != ofs_old || max_drawn_tab != max_old)) { + ensure_tab_visible(current); + } } break; + + case NOTIFICATION_DRAG_END: { + if (dragging_valid_tab) { + dragging_valid_tab = false; + update(); + } + } break; + case NOTIFICATION_DRAW: { - _update_cache(); - RID ci = get_canvas_item(); + if (tabs.is_empty()) { + return; + } Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); @@ -303,178 +358,55 @@ void TabBar::_notification(int p_what) { 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; - int mw = 0; - - for (int i = 0; i < tabs.size(); i++) { - tabs.write[i].ofs_cache = mw; - mw += get_tab_width(i); - } - - if (tab_alignment == ALIGNMENT_CENTER) { - w = (get_size().width - mw) / 2; - } else if (tab_alignment == ALIGNMENT_RIGHT) { - w = get_size().width - mw; - } - - if (w < 0) { - w = 0; - } - Ref<Texture2D> incr = get_theme_icon(SNAME("increment")); Ref<Texture2D> decr = get_theme_icon(SNAME("decrement")); - Ref<Texture2D> incr_hl = get_theme_icon(SNAME("increment_highlight")); - Ref<Texture2D> decr_hl = get_theme_icon(SNAME("decrement_highlight")); - - int limit = get_size().width; - int limit_minus_buttons = get_size().width - incr->get_width() - decr->get_width(); - - missing_right = false; - - for (int i = offset; i < tabs.size(); i++) { - tabs.write[i].ofs_cache = w; - int lsize = tabs[i].size_cache; - - Ref<StyleBox> sb; - Color col; - - if (tabs[i].disabled) { - sb = tab_disabled; - col = font_disabled_color; - } else if (i == current) { - sb = tab_selected; - col = font_selected_color; - } else { - sb = tab_unselected; - col = font_unselected_color; - } - - 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; - } else { - max_drawn_tab = i; - } - - 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(SIDE_LEFT); + bool rtl = is_layout_rtl(); + Vector2 size = get_size(); + int limit_minus_buttons = size.width - incr->get_width() - decr->get_width(); - Size2i sb_ms = sb->get_minimum_size(); - Ref<Texture2D> icon = tabs[i].icon; - if (icon.is_valid()) { - 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.is_empty()) { - w += icon->get_width() + get_theme_constant(SNAME("hseparation")); - } - } + int ofs = tabs[offset].ofs_cache; - 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); + // Draw unselected tabs in the back. + for (int i = offset; i <= max_drawn_tab; i++) { + if (tabs[i].hidden) { + continue; } - w += tabs[i].size_text; - - if (tabs[i].right_button.is_valid()) { - Ref<StyleBox> style = get_theme_stylebox(SNAME("close_bg_highlight")); - Ref<Texture2D> rb = tabs[i].right_button; + if (i != current) { + Ref<StyleBox> sb; + Color col; - w += get_theme_constant(SNAME("hseparation")); - - Rect2 rb_rect; - rb_rect.size = style->get_minimum_size() + rb->get_size(); - if (rtl) { - rb_rect.position.x = size.width - w - rb_rect.size.x; + if (tabs[i].disabled) { + sb = tab_disabled; + col = font_disabled_color; + } else if (i == current) { + sb = tab_selected; + col = font_selected_color; } 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(SNAME("button_pressed"))->draw(ci, rb_rect); - } else { - style->draw(ci, rb_rect); - } + sb = tab_unselected; + col = font_unselected_color; } - 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; + _draw_tab(sb, col, i, rtl ? size.width - ofs - tabs[i].size_cache : ofs); } - if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current)) { - Ref<StyleBox> style = get_theme_stylebox(SNAME("close_bg_highlight")); - Ref<Texture2D> cb = close; - - w += get_theme_constant(SNAME("hseparation")); - - Rect2 cb_rect; - cb_rect.size = style->get_minimum_size() + cb->get_size(); - 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(SNAME("close_bg_pressed"))->draw(ci, cb_rect); - } else { - style->draw(ci, cb_rect); - } - } + ofs += tabs[i].size_cache; + } - 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; - } + // Draw selected tab in the front, but only if it's visible. + if (current >= offset && current <= max_drawn_tab && !tabs[current].hidden) { + Ref<StyleBox> sb = tabs[current].disabled ? tab_disabled : tab_selected; + float x = rtl ? size.width - tabs[current].ofs_cache - tabs[current].size_cache : tabs[current].ofs_cache; - w += sb->get_margin(SIDE_RIGHT); + _draw_tab(sb, font_selected_color, current, x); } - if (offset > 0 || missing_right) { - int vofs = (get_size().height - incr->get_size().height) / 2; + if (buttons_visible) { + Ref<Texture2D> incr_hl = get_theme_icon(SNAME("increment_highlight")); + Ref<Texture2D> decr_hl = get_theme_icon(SNAME("decrement_highlight")); + + int vofs = (size.height - incr->get_size().height) / 2; if (rtl) { if (missing_right) { @@ -501,29 +433,180 @@ void TabBar::_notification(int p_what) { draw_texture(incr, Point2(limit_minus_buttons + decr->get_size().width, vofs), Color(1, 1, 1, 0.5)); } } + } - buttons_visible = true; - } else { - buttons_visible = false; + if (dragging_valid_tab) { + int x; + + int tab_hover = get_hovered_tab(); + if (tab_hover != -1) { + Rect2 tab_rect = get_tab_rect(tab_hover); + + x = tab_rect.position.x; + if (get_local_mouse_position().x > x + tab_rect.size.width / 2) { + x += tab_rect.size.width; + } + } else { + if (rtl ^ (get_local_mouse_position().x < get_tab_rect(0).position.x)) { + x = get_tab_rect(0).position.x; + if (rtl) { + x += get_tab_rect(0).size.width; + } + } else { + Rect2 tab_rect = get_tab_rect(get_tab_count() - 1); + + x = tab_rect.position.x; + if (!rtl) { + x += tab_rect.size.width; + } + } + } + + Ref<Texture2D> drop_mark = get_theme_icon(SNAME("drop_mark")); + Color drop_mark_color = get_theme_color(SNAME("drop_mark_color")); + + drop_mark->draw(get_canvas_item(), Point2(x - drop_mark->get_width() / 2, (size.height - drop_mark->get_height()) / 2), drop_mark_color); } } break; } } +void TabBar::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x) { + RID ci = get_canvas_item(); + bool rtl = is_layout_rtl(); + + Color font_outline_color = get_theme_color(SNAME("font_outline_color")); + int outline_size = get_theme_constant(SNAME("outline_size")); + int hseparation = get_theme_constant(SNAME("h_separation")); + + Rect2 sb_rect = Rect2(p_x, 0, tabs[p_index].size_cache, get_size().height); + p_tab_style->draw(ci, sb_rect); + + p_x += rtl ? tabs[p_index].size_cache - p_tab_style->get_margin(SIDE_LEFT) : p_tab_style->get_margin(SIDE_LEFT); + + Size2i sb_ms = p_tab_style->get_minimum_size(); + + // Draw the icon. + Ref<Texture2D> icon = tabs[p_index].icon; + if (icon.is_valid()) { + icon->draw(ci, Point2i(rtl ? p_x - icon->get_width() : p_x, p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - icon->get_height()) / 2)); + + p_x = rtl ? p_x - icon->get_width() - hseparation : p_x + icon->get_width() + hseparation; + } + + // Draw the text. + if (!tabs[p_index].text.is_empty()) { + Point2i text_pos = Point2i(rtl ? p_x - tabs[p_index].size_text : p_x, + p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - tabs[p_index].text_buf->get_size().y) / 2); + + if (outline_size > 0 && font_outline_color.a > 0) { + tabs[p_index].text_buf->draw_outline(ci, text_pos, outline_size, font_outline_color); + } + tabs[p_index].text_buf->draw(ci, text_pos, p_font_color); + + p_x = rtl ? p_x - tabs[p_index].size_text - hseparation : p_x + tabs[p_index].size_text + hseparation; + } + + // Draw and calculate rect of the right button. + if (tabs[p_index].right_button.is_valid()) { + Ref<StyleBox> style = get_theme_stylebox(SNAME("button_highlight")); + Ref<Texture2D> rb = tabs[p_index].right_button; + + Rect2 rb_rect; + rb_rect.size = style->get_minimum_size() + rb->get_size(); + rb_rect.position.x = rtl ? p_x - rb_rect.size.width : p_x; + rb_rect.position.y = p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - (rb_rect.size.y)) / 2; + + tabs.write[p_index].rb_rect = rb_rect; + + if (rb_hover == p_index) { + if (rb_pressing) { + get_theme_stylebox(SNAME("button_pressed"))->draw(ci, rb_rect); + } else { + style->draw(ci, rb_rect); + } + } + + rb->draw(ci, Point2i(rb_rect.position.x + style->get_margin(SIDE_LEFT), rb_rect.position.y + style->get_margin(SIDE_TOP))); + + p_x = rtl ? rb_rect.position.x : rb_rect.position.x + rb_rect.size.width; + } + + // Draw and calculate rect of the close button. + if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && p_index == current)) { + Ref<StyleBox> style = get_theme_stylebox(SNAME("button_highlight")); + Ref<Texture2D> cb = get_theme_icon(SNAME("close")); + + Rect2 cb_rect; + cb_rect.size = style->get_minimum_size() + cb->get_size(); + cb_rect.position.x = rtl ? p_x - cb_rect.size.width : p_x; + cb_rect.position.y = p_tab_style->get_margin(SIDE_TOP) + ((sb_rect.size.y - sb_ms.y) - (cb_rect.size.y)) / 2; + + tabs.write[p_index].cb_rect = cb_rect; + + if (!tabs[p_index].disabled && cb_hover == p_index) { + if (cb_pressing) { + get_theme_stylebox(SNAME("button_pressed"))->draw(ci, cb_rect); + } else { + style->draw(ci, cb_rect); + } + } + + cb->draw(ci, Point2i(cb_rect.position.x + style->get_margin(SIDE_LEFT), cb_rect.position.y + style->get_margin(SIDE_TOP))); + } +} + +void TabBar::set_tab_count(int p_count) { + if (p_count == tabs.size()) { + return; + } + + ERR_FAIL_COND(p_count < 0); + tabs.resize(p_count); + + if (p_count == 0) { + offset = 0; + max_drawn_tab = 0; + current = 0; + previous = 0; + } else { + offset = MIN(offset, p_count - 1); + max_drawn_tab = MIN(max_drawn_tab, p_count - 1); + current = MIN(current, p_count - 1); + + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } + } + + update(); + update_minimum_size(); + notify_property_list_changed(); +} + int TabBar::get_tab_count() const { return tabs.size(); } void TabBar::set_current_tab(int p_current) { - if (current == p_current) { - return; - } ERR_FAIL_INDEX(p_current, get_tab_count()); previous = current; current = p_current; + if (current == previous) { + emit_signal(SNAME("tab_selected"), current); + return; + } + + emit_signal(SNAME("tab_selected"), current); + _update_cache(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); emit_signal(SNAME("tab_changed"), p_current); @@ -552,7 +635,13 @@ bool TabBar::get_offset_buttons_visible() const { void TabBar::set_tab_title(int p_tab, const String &p_title) { ERR_FAIL_INDEX(p_tab, tabs.size()); tabs.write[p_tab].text = p_title; + _shape(p_tab); + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); update_minimum_size(); } @@ -565,6 +654,7 @@ String TabBar::get_tab_title(int p_tab) const { void TabBar::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); @@ -580,22 +670,38 @@ Control::TextDirection TabBar::get_tab_text_direction(int p_tab) const { void TabBar::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_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); + update_minimum_size(); } void TabBar::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_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); + update_minimum_size(); } } int TabBar::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; @@ -605,10 +711,17 @@ int TabBar::get_tab_opentype_feature(int p_tab, const String &p_name) const { void TabBar::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_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); + update_minimum_size(); } } @@ -620,6 +733,12 @@ String TabBar::get_tab_language(int p_tab) const { void TabBar::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; + + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); update_minimum_size(); } @@ -632,23 +751,53 @@ Ref<Texture2D> TabBar::get_tab_icon(int p_tab) const { void TabBar::set_tab_disabled(int p_tab, bool p_disabled) { ERR_FAIL_INDEX(p_tab, tabs.size()); tabs.write[p_tab].disabled = p_disabled; + + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); + update_minimum_size(); } -bool TabBar::get_tab_disabled(int p_tab) const { +bool TabBar::is_tab_disabled(int p_tab) const { ERR_FAIL_INDEX_V(p_tab, tabs.size(), false); return tabs[p_tab].disabled; } -void TabBar::set_tab_right_button(int p_tab, const Ref<Texture2D> &p_right_button) { +void TabBar::set_tab_hidden(int p_tab, bool p_hidden) { + ERR_FAIL_INDEX(p_tab, tabs.size()); + tabs.write[p_tab].hidden = p_hidden; + + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } + update(); + update_minimum_size(); +} + +bool TabBar::is_tab_hidden(int p_tab) const { + ERR_FAIL_INDEX_V(p_tab, tabs.size(), false); + return tabs[p_tab].hidden; +} + +void TabBar::set_tab_button_icon(int p_tab, const Ref<Texture2D> &p_icon) { ERR_FAIL_INDEX(p_tab, tabs.size()); - tabs.write[p_tab].right_button = p_right_button; + tabs.write[p_tab].right_button = p_icon; + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); update_minimum_size(); } -Ref<Texture2D> TabBar::get_tab_right_button(int p_tab) const { +Ref<Texture2D> TabBar::get_tab_button_icon(int p_tab) const { ERR_FAIL_INDEX_V(p_tab, tabs.size(), Ref<Texture2D>()); return tabs[p_tab].right_button; } @@ -658,99 +807,147 @@ void TabBar::_update_hover() { return; } + ERR_FAIL_COND(tabs.is_empty()); + const Point2 &pos = get_local_mouse_position(); - // test hovering to display right or close button. + // Test hovering to display right or close button. int hover_now = -1; int hover_buttons = -1; - for (int i = offset; i < tabs.size(); i++) { + for (int i = offset; i <= max_drawn_tab; i++) { + if (tabs[i].hidden) { + continue; + } + Rect2 rect = get_tab_rect(i); if (rect.has_point(pos)) { hover_now = i; } + if (tabs[i].rb_rect.has_point(pos)) { rb_hover = i; cb_hover = -1; hover_buttons = i; - break; } else if (!tabs[i].disabled && tabs[i].cb_rect.has_point(pos)) { cb_hover = i; rb_hover = -1; hover_buttons = i; + } + + if (hover_buttons != -1) { + update(); break; } } + if (hover != hover_now) { hover = hover_now; - emit_signal(SNAME("tab_hovered"), hover); + + if (hover != -1) { + emit_signal(SNAME("tab_hovered"), hover); + } } if (hover_buttons == -1) { // No hover. + int rb_hover_old = rb_hover; + int cb_hover_old = cb_hover; + rb_hover = hover_buttons; cb_hover = hover_buttons; + + if (rb_hover != rb_hover_old || cb_hover != cb_hover_old) { + update(); + } } } void TabBar::_update_cache() { + if (tabs.is_empty()) { + buttons_visible = false; + return; + } + 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 limit = get_size().width; + int limit_minus_buttons = limit - incr->get_width() - decr->get_width(); int w = 0; - int mw = 0; - int size_fixed = 0; - int count_resize = 0; + + max_drawn_tab = tabs.size() - 1; + 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(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; - } else { - count_resize++; + tabs.write[i].size_text = Math::ceil(tabs[i].text_buf->get_size().x); + tabs.write[i].size_cache = get_tab_width(i); + + if (max_width > 0 && tabs[i].size_cache > max_width) { + int size_textless = tabs[i].size_cache - tabs[i].size_text; + int mw = MAX(size_textless, max_width); + + tabs.write[i].size_text = MAX(mw - size_textless, 1); + tabs.write[i].text_buf->set_width(tabs[i].size_text); + tabs.write[i].size_cache = size_textless + tabs[i].size_text; } - } - int m_width = min_width; - if (count_resize > 0) { - 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_selected; - } else { - sb = tab_unselected; + + if (i < offset || i > max_drawn_tab) { + tabs.write[i].ofs_cache = 0; + continue; } - int lsize = tabs[i].size_cache; - int slen = tabs[i].size_text; - if (min_width > 0 && mw > limit_minus_buttons && i != current) { - if (lsize > m_width) { - 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(SNAME("hseparation")); - } - if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && i == current)) { - Ref<Texture2D> cb = get_theme_icon(SNAME("close")); - slen -= cb->get_width(); - slen -= get_theme_constant(SNAME("hseparation")); + + tabs.write[i].ofs_cache = w; + + if (tabs[i].hidden) { + continue; + } + + w += tabs[i].size_cache; + + // Check if all tabs would fit inside the area. + if (clip_tabs && i > offset && (w > limit || (offset > 0 && w > limit_minus_buttons))) { + tabs.write[i].ofs_cache = 0; + + w -= tabs[i].size_cache; + max_drawn_tab = i - 1; + + while (w > limit_minus_buttons && max_drawn_tab > offset) { + tabs.write[max_drawn_tab].ofs_cache = 0; + + if (!tabs[max_drawn_tab].hidden) { + w -= tabs[max_drawn_tab].size_cache; } - slen = MAX(slen, 1); - lsize = m_width; + + max_drawn_tab--; } } + } + + missing_right = max_drawn_tab < tabs.size() - 1; + buttons_visible = offset > 0 || missing_right; + + if (tab_alignment == ALIGNMENT_LEFT) { + _update_hover(); + return; + } + + if (tab_alignment == ALIGNMENT_CENTER) { + w = ((buttons_visible ? limit_minus_buttons : limit) - w) / 2; + } else if (tab_alignment == ALIGNMENT_RIGHT) { + w = (buttons_visible ? limit_minus_buttons : limit) - w; + } + + for (int i = offset; i <= max_drawn_tab; i++) { 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; + + if (!tabs[i].hidden) { + w += tabs[i].size_cache; + } } + + _update_hover(); } void TabBar::_on_mouse_exited() { @@ -758,65 +955,87 @@ void TabBar::_on_mouse_exited() { cb_hover = -1; hover = -1; highlight_arrow = -1; + dragging_valid_tab = false; + update(); } void TabBar::add_tab(const String &p_str, const Ref<Texture2D> &p_icon) { Tab t; t.text = 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; - t.size_cache = 0; - tabs.push_back(t); + + _shape(tabs.size() - 1); _update_cache(); - call_deferred(SNAME("_update_hover")); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); update_minimum_size(); + + if (tabs.size() == 1 && is_inside_tree()) { + emit_signal(SNAME("tab_changed"), 0); + } } void TabBar::clear_tabs() { + if (tabs.is_empty()) { + return; + } + tabs.clear(); + offset = 0; + max_drawn_tab = 0; current = 0; previous = 0; - call_deferred(SNAME("_update_hover")); + update(); + update_minimum_size(); + notify_property_list_changed(); } void TabBar::remove_tab(int p_idx) { ERR_FAIL_INDEX(p_idx, tabs.size()); tabs.remove_at(p_idx); - if (current >= p_idx) { + + bool is_tab_changing = current == p_idx && !tabs.is_empty(); + + if (current >= p_idx && current > 0) { current--; } - _update_cache(); - call_deferred(SNAME("_update_hover")); - update(); - update_minimum_size(); - if (current < 0) { - current = 0; + if (tabs.is_empty()) { + offset = 0; + max_drawn_tab = 0; previous = 0; - } - if (current >= tabs.size()) { - current = tabs.size() - 1; + } else { + offset = MIN(offset, tabs.size() - 1); + max_drawn_tab = MIN(max_drawn_tab, tabs.size() - 1); + + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } } - _ensure_no_over_offset(); + update(); + update_minimum_size(); + notify_property_list_changed(); + + if (is_tab_changing && is_inside_tree()) { + emit_signal(SNAME("tab_changed"), current); + } } Variant TabBar::get_drag_data(const Point2 &p_point) { if (!drag_to_rearrange_enabled) { - return Variant(); + return Control::get_drag_data(p_point); // Allow stuff like TabContainer to override it. } int tab_over = get_tab_idx_at_point(p_point); - if (tab_over < 0) { return Variant(); } @@ -826,27 +1045,26 @@ Variant TabBar::get_drag_data(const Point2 &p_point) { if (!tabs[tab_over].icon.is_null()) { TextureRect *tf = memnew(TextureRect); tf->set_texture(tabs[tab_over].icon); + tf->set_stretch_mode(TextureRect::STRETCH_KEEP_CENTERED); drag_preview->add_child(tf); } + Label *label = memnew(Label(tabs[tab_over].xl_text)); drag_preview->add_child(label); - if (!tabs[tab_over].right_button.is_null()) { - TextureRect *tf = memnew(TextureRect); - tf->set_texture(tabs[tab_over].right_button); - drag_preview->add_child(tf); - } + set_drag_preview(drag_preview); Dictionary drag_data; drag_data["type"] = "tab_element"; drag_data["tab_element"] = tab_over; drag_data["from_path"] = get_path(); + return drag_data; } bool TabBar::can_drop_data(const Point2 &p_point, const Variant &p_data) const { if (!drag_to_rearrange_enabled) { - return false; + return Control::can_drop_data(p_point, p_data); // Allow stuff like TabContainer to override it. } Dictionary d = p_data; @@ -868,16 +1086,16 @@ bool TabBar::can_drop_data(const Point2 &p_point, const Variant &p_data) const { } } } + return false; } void TabBar::drop_data(const Point2 &p_point, const Variant &p_data) { if (!drag_to_rearrange_enabled) { + Control::drop_data(p_point, p_data); // Allow stuff like TabContainer to override it. return; } - int hover_now = get_tab_idx_at_point(p_point); - Dictionary d = p_data; if (!d.has("type")) { return; @@ -885,36 +1103,84 @@ void TabBar::drop_data(const Point2 &p_point, const Variant &p_data) { if (String(d["type"]) == "tab_element") { int tab_from_id = d["tab_element"]; + int hover_now = get_tab_idx_at_point(p_point); NodePath from_path = d["from_path"]; NodePath to_path = get_path(); + if (from_path == to_path) { - if (hover_now < 0) { - hover_now = get_tab_count() - 1; + if (tab_from_id == hover_now) { + return; } + + // Drop the new tab to the left or right depending on where the target tab is being hovered. + if (hover_now != -1) { + Rect2 tab_rect = get_tab_rect(hover_now); + if (is_layout_rtl() ^ (p_point.x <= tab_rect.position.x + tab_rect.size.width / 2)) { + if (hover_now > tab_from_id) { + hover_now -= 1; + } + } else if (tab_from_id > hover_now) { + hover_now += 1; + } + } else { + hover_now = is_layout_rtl() ^ (p_point.x < get_tab_rect(0).position.x) ? 0 : get_tab_count() - 1; + } + move_tab(tab_from_id, hover_now); - emit_signal(SNAME("active_tab_rearranged"), hover_now); - set_current_tab(hover_now); + if (!is_tab_disabled(hover_now)) { + emit_signal(SNAME("active_tab_rearranged"), hover_now); + set_current_tab(hover_now); + } } else if (get_tabs_rearrange_group() != -1) { // Drag and drop between Tabs. + Node *from_node = get_node(from_path); TabBar *from_tabs = Object::cast_to<TabBar>(from_node); + if (from_tabs && from_tabs->get_tabs_rearrange_group() == get_tabs_rearrange_group()) { if (tab_from_id >= from_tabs->get_tab_count()) { return; } - Tab moving_tab = from_tabs->tabs[tab_from_id]; - if (hover_now < 0) { - hover_now = get_tab_count(); + + // Drop the new tab to the left or right depending on where the target tab is being hovered. + if (hover_now != -1) { + Rect2 tab_rect = get_tab_rect(hover_now); + if (is_layout_rtl() ^ (p_point.x > tab_rect.position.x + tab_rect.size.width / 2)) { + hover_now += 1; + } + } else { + hover_now = is_layout_rtl() ^ (p_point.x < get_tab_rect(0).position.x) ? 0 : get_tab_count(); } - tabs.insert(hover_now, moving_tab); + + Tab moving_tab = from_tabs->tabs[tab_from_id]; from_tabs->remove_tab(tab_from_id); - set_current_tab(hover_now); - emit_signal(SNAME("tab_changed"), hover_now); - _update_cache(); + tabs.insert(hover_now, moving_tab); + + if (tabs.size() > 1) { + if (current >= hover_now) { + current++; + } + if (previous >= hover_now) { + previous++; + } + } + + if (!is_tab_disabled(hover_now)) { + set_current_tab(hover_now); + } else { + _update_cache(); + update(); + } + + update_minimum_size(); + + if (tabs.size() == 1) { + emit_signal(SNAME("tab_selected"), 0); + emit_signal(SNAME("tab_changed"), 0); + } } } } - update(); } int TabBar::get_tab_idx_at_point(const Point2 &p_point) const { @@ -932,6 +1198,8 @@ int TabBar::get_tab_idx_at_point(const Point2 &p_point) const { void TabBar::set_tab_alignment(AlignmentMode p_alignment) { ERR_FAIL_INDEX(p_alignment, ALIGNMENT_MAX); tab_alignment = p_alignment; + + _update_cache(); update(); } @@ -944,6 +1212,16 @@ void TabBar::set_clip_tabs(bool p_clip_tabs) { return; } clip_tabs = p_clip_tabs; + + if (!clip_tabs) { + offset = 0; + max_drawn_tab = 0; + } + + _update_cache(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); update_minimum_size(); } @@ -952,20 +1230,41 @@ bool TabBar::get_clip_tabs() const { return clip_tabs; } -void TabBar::move_tab(int from, int to) { - if (from == to) { +void TabBar::move_tab(int p_from, int p_to) { + if (p_from == p_to) { return; } - ERR_FAIL_INDEX(from, tabs.size()); - ERR_FAIL_INDEX(to, tabs.size()); + ERR_FAIL_INDEX(p_from, tabs.size()); + ERR_FAIL_INDEX(p_to, tabs.size()); + + Tab tab_from = tabs[p_from]; + tabs.remove_at(p_from); + tabs.insert(p_to, tab_from); + + if (current == p_from) { + current = p_to; + } else if (current > p_from && current <= p_to) { + current--; + } else if (current < p_from && current >= p_to) { + current++; + } - Tab tab_from = tabs[from]; - tabs.remove_at(from); - tabs.insert(to, tab_from); + if (previous == p_from) { + previous = p_to; + } else if (previous > p_from && previous >= p_to) { + previous--; + } else if (previous < p_from && previous <= p_to) { + previous++; + } _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); + notify_property_list_changed(); } int TabBar::get_tab_width(int p_idx) const { @@ -974,101 +1273,134 @@ int TabBar::get_tab_width(int p_idx) const { 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 hseparation = get_theme_constant(SNAME("h_separation")); - int x = 0; + Ref<StyleBox> style; + + if (tabs[p_idx].disabled) { + style = tab_disabled; + } else if (current == p_idx) { + style = tab_selected; + } else { + style = tab_unselected; + } + int x = style->get_minimum_size().width; Ref<Texture2D> tex = tabs[p_idx].icon; if (tex.is_valid()) { - x += tex->get_width(); - if (!tabs[p_idx].text.is_empty()) { - x += get_theme_constant(SNAME("hseparation")); - } + x += tex->get_width() + hseparation; } - 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_selected->get_minimum_size().width; - } else { - x += tab_unselected->get_minimum_size().width; + if (!tabs[p_idx].text.is_empty()) { + x += tabs[p_idx].size_text + hseparation; } + bool close_visible = cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && p_idx == current); + if (tabs[p_idx].right_button.is_valid()) { + Ref<StyleBox> btn_style = get_theme_stylebox(SNAME("button_highlight")); Ref<Texture2D> rb = tabs[p_idx].right_button; - x += rb->get_width(); - x += get_theme_constant(SNAME("hseparation")); + + if (close_visible) { + x += btn_style->get_minimum_size().width + rb->get_width(); + } else { + x += btn_style->get_margin(SIDE_LEFT) + rb->get_width() + hseparation; + } } - if (cb_displaypolicy == CLOSE_BUTTON_SHOW_ALWAYS || (cb_displaypolicy == CLOSE_BUTTON_SHOW_ACTIVE_ONLY && p_idx == current)) { + if (close_visible) { + Ref<StyleBox> btn_style = get_theme_stylebox(SNAME("button_highlight")); Ref<Texture2D> cb = get_theme_icon(SNAME("close")); - x += cb->get_width(); - x += get_theme_constant(SNAME("hseparation")); + x += btn_style->get_margin(SIDE_LEFT) + cb->get_width() + hseparation; + } + + if (x > style->get_minimum_size().width) { + x -= hseparation; } return x; } void TabBar::_ensure_no_over_offset() { - if (!is_inside_tree()) { + if (!is_inside_tree() || !buttons_visible) { return; } 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(); - while (offset > 0) { - int total_w = 0; - for (int i = offset - 1; i < tabs.size(); i++) { - total_w += tabs[i].size_cache; + int prev_offset = offset; + + int total_w = tabs[max_drawn_tab].ofs_cache + tabs[max_drawn_tab].size_cache - tabs[offset].ofs_cache; + for (int i = offset; i > 0; i--) { + if (tabs[i - 1].hidden) { + continue; } - if ((buttons_visible && total_w < limit_minus_buttons) || total_w < limit) { // For the last tab, we accept if the tab covers the buttons. + total_w += tabs[i - 1].size_cache; + + if (total_w < limit_minus_buttons) { offset--; - update(); } else { break; } } -} -void TabBar::ensure_tab_visible(int p_idx) { - if (!is_inside_tree()) { - return; + if (prev_offset != offset) { + _update_cache(); + update(); } +} - if (tabs.size() == 0) { +void TabBar::ensure_tab_visible(int p_idx) { + if (!is_inside_tree() || !buttons_visible) { return; } ERR_FAIL_INDEX(p_idx, tabs.size()); - if (p_idx == offset) { + if (tabs[p_idx].hidden || (p_idx >= offset && p_idx <= max_drawn_tab)) { return; } + if (p_idx < offset) { offset = p_idx; + _update_cache(); update(); + return; } - int prev_offset = offset; 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++) { - int total_w = tabs[i].ofs_cache + tabs[i].size_cache; - if (total_w > limit || (buttons_visible && total_w > limit_minus_buttons)) { + int total_w = tabs[max_drawn_tab].ofs_cache - tabs[offset].ofs_cache; + for (int i = max_drawn_tab; i <= p_idx; i++) { + if (tabs[i].hidden) { + continue; + } + + total_w += tabs[i].size_cache; + } + + int prev_offset = offset; + + for (int i = offset; i < p_idx; i++) { + if (tabs[i].hidden) { + continue; + } + + if (total_w > limit_minus_buttons) { + total_w -= tabs[i].size_cache; offset++; + } else { + break; } } if (prev_offset != offset) { + _update_cache(); update(); } } @@ -1085,15 +1417,35 @@ Rect2 TabBar::get_tab_rect(int p_tab) const { void TabBar::set_tab_close_display_policy(CloseButtonDisplayPolicy p_policy) { ERR_FAIL_INDEX(p_policy, CLOSE_BUTTON_MAX); cb_displaypolicy = p_policy; + + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } update(); + update_minimum_size(); } TabBar::CloseButtonDisplayPolicy TabBar::get_tab_close_display_policy() const { return cb_displaypolicy; } -void TabBar::set_min_width(int p_width) { - min_width = p_width; +void TabBar::set_max_tab_width(int p_width) { + ERR_FAIL_COND(p_width < 0); + max_width = p_width; + + _update_cache(); + _ensure_no_over_offset(); + if (scroll_to_selected) { + ensure_tab_visible(current); + } + update(); + update_minimum_size(); +} + +int TabBar::get_max_tab_width() const { + return max_width; } void TabBar::set_scrolling_enabled(bool p_enabled) { @@ -1120,6 +1472,17 @@ int TabBar::get_tabs_rearrange_group() const { return tabs_rearrange_group; } +void TabBar::set_scroll_to_selected(bool p_enabled) { + scroll_to_selected = p_enabled; + if (p_enabled) { + ensure_tab_visible(current); + } +} + +bool TabBar::get_scroll_to_selected() const { + return scroll_to_selected; +} + void TabBar::set_select_with_rmb(bool p_enabled) { select_with_rmb = p_enabled; } @@ -1128,8 +1491,60 @@ bool TabBar::get_select_with_rmb() const { return select_with_rmb; } +bool TabBar::_set(const StringName &p_name, const Variant &p_value) { + Vector<String> components = String(p_name).split("/", true, 2); + if (components.size() >= 2 && components[0].begins_with("tab_") && components[0].trim_prefix("tab_").is_valid_int()) { + int tab_index = components[0].trim_prefix("tab_").to_int(); + String property = components[1]; + if (property == "title") { + set_tab_title(tab_index, p_value); + return true; + } else if (property == "icon") { + set_tab_icon(tab_index, p_value); + return true; + } else if (components[1] == "disabled") { + set_tab_disabled(tab_index, p_value); + return true; + } + } + return false; +} + +bool TabBar::_get(const StringName &p_name, Variant &r_ret) const { + Vector<String> components = String(p_name).split("/", true, 2); + if (components.size() >= 2 && components[0].begins_with("tab_") && components[0].trim_prefix("tab_").is_valid_int()) { + int tab_index = components[0].trim_prefix("tab_").to_int(); + String property = components[1]; + if (property == "title") { + r_ret = get_tab_title(tab_index); + return true; + } else if (property == "icon") { + r_ret = get_tab_icon(tab_index); + return true; + } else if (components[1] == "disabled") { + r_ret = is_tab_disabled(tab_index); + return true; + } + } + return false; +} + +void TabBar::_get_property_list(List<PropertyInfo> *p_list) const { + for (int i = 0; i < tabs.size(); i++) { + p_list->push_back(PropertyInfo(Variant::STRING, vformat("tab_%d/title", i))); + + PropertyInfo pi = PropertyInfo(Variant::OBJECT, vformat("tab_%d/icon", i), PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"); + pi.usage &= ~(get_tab_icon(i).is_null() ? PROPERTY_USAGE_STORAGE : 0); + p_list->push_back(pi); + + pi = PropertyInfo(Variant::BOOL, vformat("tab_%d/disabled", i)); + pi.usage &= ~(!is_tab_disabled(i) ? PROPERTY_USAGE_STORAGE : 0); + p_list->push_back(pi); + } +} + void TabBar::_bind_methods() { - ClassDB::bind_method(D_METHOD("_update_hover"), &TabBar::_update_hover); + ClassDB::bind_method(D_METHOD("set_tab_count", "count"), &TabBar::set_tab_count); ClassDB::bind_method(D_METHOD("get_tab_count"), &TabBar::get_tab_count); ClassDB::bind_method(D_METHOD("set_current_tab", "tab_idx"), &TabBar::set_current_tab); ClassDB::bind_method(D_METHOD("get_current_tab"), &TabBar::get_current_tab); @@ -1145,10 +1560,15 @@ void TabBar::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tab_language", "tab_idx"), &TabBar::get_tab_language); ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &TabBar::set_tab_icon); ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &TabBar::get_tab_icon); + ClassDB::bind_method(D_METHOD("set_tab_button_icon", "tab_idx", "icon"), &TabBar::set_tab_button_icon); + ClassDB::bind_method(D_METHOD("get_tab_button_icon", "tab_idx"), &TabBar::get_tab_button_icon); ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &TabBar::set_tab_disabled); - ClassDB::bind_method(D_METHOD("get_tab_disabled", "tab_idx"), &TabBar::get_tab_disabled); + ClassDB::bind_method(D_METHOD("is_tab_disabled", "tab_idx"), &TabBar::is_tab_disabled); + ClassDB::bind_method(D_METHOD("set_tab_hidden", "tab_idx", "hidden"), &TabBar::set_tab_hidden); + ClassDB::bind_method(D_METHOD("is_tab_hidden", "tab_idx"), &TabBar::is_tab_hidden); ClassDB::bind_method(D_METHOD("remove_tab", "tab_idx"), &TabBar::remove_tab); ClassDB::bind_method(D_METHOD("add_tab", "title", "icon"), &TabBar::add_tab, DEFVAL(""), DEFVAL(Ref<Texture2D>())); + ClassDB::bind_method(D_METHOD("get_tab_idx_at_point", "point"), &TabBar::get_tab_idx_at_point); ClassDB::bind_method(D_METHOD("set_tab_alignment", "alignment"), &TabBar::set_tab_alignment); ClassDB::bind_method(D_METHOD("get_tab_alignment"), &TabBar::get_tab_alignment); ClassDB::bind_method(D_METHOD("set_clip_tabs", "clip_tabs"), &TabBar::set_clip_tabs); @@ -1160,29 +1580,40 @@ void TabBar::_bind_methods() { ClassDB::bind_method(D_METHOD("move_tab", "from", "to"), &TabBar::move_tab); ClassDB::bind_method(D_METHOD("set_tab_close_display_policy", "policy"), &TabBar::set_tab_close_display_policy); ClassDB::bind_method(D_METHOD("get_tab_close_display_policy"), &TabBar::get_tab_close_display_policy); + ClassDB::bind_method(D_METHOD("set_max_tab_width", "width"), &TabBar::set_max_tab_width); + ClassDB::bind_method(D_METHOD("get_max_tab_width"), &TabBar::get_max_tab_width); ClassDB::bind_method(D_METHOD("set_scrolling_enabled", "enabled"), &TabBar::set_scrolling_enabled); ClassDB::bind_method(D_METHOD("get_scrolling_enabled"), &TabBar::get_scrolling_enabled); ClassDB::bind_method(D_METHOD("set_drag_to_rearrange_enabled", "enabled"), &TabBar::set_drag_to_rearrange_enabled); ClassDB::bind_method(D_METHOD("get_drag_to_rearrange_enabled"), &TabBar::get_drag_to_rearrange_enabled); ClassDB::bind_method(D_METHOD("set_tabs_rearrange_group", "group_id"), &TabBar::set_tabs_rearrange_group); ClassDB::bind_method(D_METHOD("get_tabs_rearrange_group"), &TabBar::get_tabs_rearrange_group); - + ClassDB::bind_method(D_METHOD("set_scroll_to_selected", "enabled"), &TabBar::set_scroll_to_selected); + ClassDB::bind_method(D_METHOD("get_scroll_to_selected"), &TabBar::get_scroll_to_selected); ClassDB::bind_method(D_METHOD("set_select_with_rmb", "enabled"), &TabBar::set_select_with_rmb); ClassDB::bind_method(D_METHOD("get_select_with_rmb"), &TabBar::get_select_with_rmb); + ADD_SIGNAL(MethodInfo("tab_selected", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab"))); + ADD_SIGNAL(MethodInfo("tab_clicked", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("tab_rmb_clicked", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("tab_close_pressed", PropertyInfo(Variant::INT, "tab"))); + ADD_SIGNAL(MethodInfo("tab_button_pressed", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("tab_hovered", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("active_tab_rearranged", 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_alignment", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_tab_alignment", "get_tab_alignment"); 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::INT, "max_tab_width", PROPERTY_HINT_RANGE, "0,99999,1"), "set_max_tab_width", "get_max_tab_width"); 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"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "tabs_rearrange_group"), "set_tabs_rearrange_group", "get_tabs_rearrange_group"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_to_selected"), "set_scroll_to_selected", "get_scroll_to_selected"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "select_with_rmb"), "set_select_with_rmb", "get_select_with_rmb"); + + ADD_ARRAY_COUNT("Tabs", "tab_count", "set_tab_count", "get_tab_count", "tab_"); BIND_ENUM_CONSTANT(ALIGNMENT_LEFT); BIND_ENUM_CONSTANT(ALIGNMENT_CENTER); @@ -1196,5 +1627,6 @@ void TabBar::_bind_methods() { } TabBar::TabBar() { + set_size(Size2(get_size().width, get_minimum_size().height)); connect("mouse_exited", callable_mp(this, &TabBar::_on_mouse_exited)); } diff --git a/scene/gui/tab_bar.h b/scene/gui/tab_bar.h index 2d211937fc..548a2e62af 100644 --- a/scene/gui/tab_bar.h +++ b/scene/gui/tab_bar.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -63,16 +63,19 @@ private: Ref<TextLine> text_buf; Ref<Texture2D> icon; - int ofs_cache = 0; bool disabled = false; + bool hidden = false; + int ofs_cache = 0; 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; + + Tab() { + text_buf.instantiate(); + } }; int offset = 0; @@ -83,7 +86,7 @@ private: Vector<Tab> tabs; int current = 0; int previous = 0; - AlignmentMode tab_alignment = ALIGNMENT_CENTER; + AlignmentMode tab_alignment = ALIGNMENT_LEFT; bool clip_tabs = true; int rb_hover = -1; bool rb_pressing = false; @@ -95,9 +98,11 @@ private: CloseButtonDisplayPolicy cb_displaypolicy = CLOSE_BUTTON_SHOW_NEVER; int hover = -1; // Hovered tab. - int min_width = 0; + int max_width = 0; bool scrolling_enabled = true; bool drag_to_rearrange_enabled = false; + bool dragging_valid_tab = false; + bool scroll_to_selected = true; int tabs_rearrange_group = -1; int get_tab_width(int p_idx) const; @@ -109,16 +114,19 @@ private: void _on_mouse_exited(); void _shape(int p_tab); + void _draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x); protected: 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 _notification(int p_what); static void _bind_methods(); Variant get_drag_data(const Point2 &p_point) override; bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override; void drop_data(const Point2 &p_point, const Variant &p_data) override; - int get_tab_idx_at_point(const Point2 &p_point) const; public: void add_tab(const String &p_str = "", const Ref<Texture2D> &p_icon = Ref<Texture2D>()); @@ -140,10 +148,15 @@ public: Ref<Texture2D> get_tab_icon(int p_tab) const; void set_tab_disabled(int p_tab, bool p_disabled); - bool get_tab_disabled(int p_tab) const; + bool is_tab_disabled(int p_tab) const; + + void set_tab_hidden(int p_tab, bool p_hidden); + bool is_tab_hidden(int p_tab) const; - void set_tab_right_button(int p_tab, const Ref<Texture2D> &p_right_button); - Ref<Texture2D> get_tab_right_button(int p_tab) const; + void set_tab_button_icon(int p_tab, const Ref<Texture2D> &p_icon); + Ref<Texture2D> get_tab_button_icon(int p_tab) const; + + int get_tab_idx_at_point(const Point2 &p_point) const; void set_tab_alignment(AlignmentMode p_alignment); AlignmentMode get_tab_alignment() const; @@ -151,12 +164,14 @@ public: void set_clip_tabs(bool p_clip_tabs); bool get_clip_tabs() const; - void move_tab(int from, int to); + void move_tab(int p_from, int p_to); void set_tab_close_display_policy(CloseButtonDisplayPolicy p_policy); CloseButtonDisplayPolicy get_tab_close_display_policy() const; + void set_tab_count(int p_count); int get_tab_count() const; + void set_current_tab(int p_current); int get_current_tab() const; int get_previous_tab() const; @@ -177,11 +192,16 @@ public: void set_tabs_rearrange_group(int p_group_id); int get_tabs_rearrange_group() const; + void set_scroll_to_selected(bool p_enabled); + bool get_scroll_to_selected() const; + void set_select_with_rmb(bool p_enabled); bool get_select_with_rmb() const; void ensure_tab_visible(int p_idx); - void set_min_width(int p_width); + + void set_max_tab_width(int p_width); + int get_max_tab_width() const; Rect2 get_tab_rect(int p_tab) const; Size2 get_minimum_size() const override; diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index fd5df5c93b..8299d73b68 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,45 +30,17 @@ #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" int TabContainer::_get_top_margin() const { - if (!tabs_visible) { - return 0; - } - - // Respect the minimum tab height. - Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); - Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); - Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); - - int tab_height = MAX(MAX(tab_unselected->get_minimum_size().height, tab_selected->get_minimum_size().height), tab_disabled->get_minimum_size().height); - - // Font height or higher icon wins. - 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; - } - - Ref<Texture2D> tex = c->get_meta("_tab_icon"); - if (!tex.is_valid()) { - continue; - } - content_height = MAX(content_height, tex->get_size().height); + int height = 0; + if (tabs_visible && get_tab_count() > 0) { + height = tab_bar->get_minimum_size().height; } - return tab_height + content_height; + return height; } void TabContainer::gui_input(const Ref<InputEvent> &p_event) { @@ -114,77 +86,6 @@ void TabContainer::gui_input(const Ref<InputEvent> &p_event) { return; } } - - // Do not activate tabs when tabs is empty. - if (get_tab_count() == 0) { - return; - } - - Vector<Control *> tabs = _get_tabs(); - - // Handle navigation buttons. - if (buttons_visible_cache) { - int popup_ofs = 0; - if (popup) { - popup_ofs = menu->get_width(); - } - - 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; - } - } 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; - } - } - } - - // 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)) { - continue; - } - int tab_width = _get_tab_width(i); - if (pos.x < tab_width) { - if (!get_tab_disabled(i)) { - set_current_tab(i); - } - break; - } - pos.x -= tab_width; - } } Ref<InputEventMouseMotion> mm = p_event; @@ -195,9 +96,8 @@ void TabContainer::gui_input(const Ref<InputEvent> &p_event) { // Mouse must be on tabs in the tab header area. if (pos.y > _get_top_margin()) { - if (menu_hovered || highlight_arrow > -1) { + if (menu_hovered) { menu_hovered = false; - highlight_arrow = -1; update(); } return; @@ -209,7 +109,6 @@ void TabContainer::gui_input(const Ref<InputEvent> &p_event) { if (pos.x <= menu->get_width()) { if (!menu_hovered) { menu_hovered = true; - highlight_arrow = -1; update(); return; } @@ -221,7 +120,6 @@ void TabContainer::gui_input(const Ref<InputEvent> &p_event) { if (pos.x >= size.width - menu->get_width()) { if (!menu_hovered) { menu_hovered = true; - highlight_arrow = -1; update(); return; } @@ -235,101 +133,26 @@ void TabContainer::gui_input(const Ref<InputEvent> &p_event) { return; } } - - // Do not activate tabs when tabs is empty. - if ((get_tab_count() == 0 || !buttons_visible_cache) && menu_hovered) { - highlight_arrow = -1; - update(); - return; - } - - int popup_ofs = 0; - if (popup) { - popup_ofs = menu->get_width(); - } - - 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() - 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(); - } - } } } void TabContainer::_notification(int p_what) { switch (p_what) { - case NOTIFICATION_RESIZED: { - Vector<Control *> tabs = _get_tabs(); - 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. - Popup *popup = get_popup(); - if (popup) { - header_width -= menu->get_width(); - } - if (buttons_visible_cache) { - header_width -= increment->get_width() + decrement->get_width(); + case NOTIFICATION_ENTER_TREE: { + // If some nodes happen to be renamed outside the tree, the tab names need to be updated manually. + if (get_tab_count() > 0) { + _refresh_tab_names(); } - if (popup || buttons_visible_cache) { - header_width += side_margin; - } - - // Find the width of all tabs after first_tab_cache. - int all_tabs_width = 0; - for (int i = first_tab_cache; i < tabs.size(); i++) { - int tab_width = _get_tab_width(i); - all_tabs_width += tab_width; - } - - // Check if tabs before first_tab_cache would fit into the header area. - for (int i = first_tab_cache - 1; i >= 0; i--) { - int tab_width = _get_tab_width(i); - - if (all_tabs_width + tab_width > header_width) { - break; - } + } break; - all_tabs_width += tab_width; - first_tab_cache--; - } + case NOTIFICATION_READY: + case NOTIFICATION_RESIZED: { + _update_margins(); } break; + 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(SNAME("panel")); @@ -338,494 +161,173 @@ void TabContainer::_notification(int p_what) { return; } - Vector<Control *> tabs = _get_tabs(); - 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; - int header_width = size.width - side_margin * 2; int header_height = _get_top_margin(); - Popup *popup = get_popup(); - if (popup) { - header_width -= menu->get_width(); - } - - // Check if all tabs would fit into the header area. - int all_tabs_width = 0; - for (int i = 0; i < tabs.size(); i++) { - if (get_tab_hidden(i)) { - continue; - } - int tab_width = _get_tab_width(i); - all_tabs_width += tab_width; - - if (all_tabs_width > header_width) { - // Not all tabs are visible at the same time - reserve space for navigation buttons. - buttons_visible_cache = true; - header_width -= decrement->get_width() + increment->get_width(); - break; - } else { - buttons_visible_cache = false; - } - } - // With buttons, a right side margin does not need to be respected. - if (popup || buttons_visible_cache) { - header_width += side_margin; - } - - if (!buttons_visible_cache) { - first_tab_cache = 0; - } - - // Go through the visible tabs to find the width they occupy. - all_tabs_width = 0; - 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); - if (all_tabs_width + tab_width > header_width && tab_widths.size() > 0) { - break; - } - all_tabs_width += tab_width; - tab_widths.push_back(tab_width); - } - - // Find the offset at which to draw tabs, according to the alignment. - switch (alignment) { - case ALIGNMENT_LEFT: - tabs_ofs_cache = header_x; - break; - case ALIGNMENT_CENTER: - tabs_ofs_cache = header_x + (header_width / 2) - (all_tabs_width / 2); - break; - case ALIGNMENT_RIGHT: - tabs_ofs_cache = header_x + header_width - all_tabs_width; - 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++) { - index = i + first_tab_cache; - if (get_tab_hidden(index)) { - continue; - } - - int tab_width = tab_widths[i]; - if (index == current) { - x_current = x; - } else if (get_tab_disabled(index)) { - if (rtl) { - _draw_tab(tab_disabled, font_disabled_color, index, size.width - (tabs_ofs_cache + x) - tab_width); - } else { - _draw_tab(tab_disabled, font_disabled_color, index, tabs_ofs_cache + x); - } - } else { - if (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 = index; - } - - 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. Only draw selected tab when it's in visible range. - if (tabs.size() > 0 && current - first_tab_cache < tab_widths.size() && current >= first_tab_cache) { - Ref<StyleBox> current_style_box = get_tab_disabled(current) ? tab_disabled : tab_selected; - if (rtl) { - _draw_tab(current_style_box, font_selected_color, current, size.width - (tabs_ofs_cache + x_current) - tab_widths[current]); - } else { - _draw_tab(current_style_box, font_selected_color, current, tabs_ofs_cache + x_current); - } - } + panel->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height)); // Draw the popup menu. - if (rtl) { - x = 0; - } else { - x = get_size().width; - } - if (popup) { - 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(); - } - } + if (get_popup()) { + Ref<Texture2D> menu = get_theme_icon(SNAME("menu")); + Ref<Texture2D> menu_hl = get_theme_icon(SNAME("menu_highlight")); - // Draw the navigation buttons. - if (buttons_visible_cache) { - 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(); + int x = is_layout_rtl() ? 0 : get_size().width - menu->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(); + if (menu_hovered) { + menu_hl->draw(get_canvas_item(), Point2(x, (header_height - menu_hl->get_height()) / 2)); } else { - 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)); - } + menu->draw(get_canvas_item(), Point2(x, (header_height - menu->get_height()) / 2)); } } } break; + case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_THEME_CHANGED: { - Vector<Control *> tabs = _get_tabs(); - for (int i = 0; i < tabs.size(); i++) { - text_buf.write[i]->clear(); - } - _theme_changing = true; + 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) { - Control *control = get_tab_control(p_index); - RID canvas = get_canvas_item(); - 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(); - - // Draw the tab background. - Rect2 tab_rect(p_x, 0, tab_width, header_height); - p_tab_style->draw(canvas, tab_rect); - - // Draw the tab contents. - String text = control->has_meta("_tab_name") ? String(atr(String(control->get_meta("_tab_name")))) : String(atr(control->get_name())); - - int x_content = tab_rect.position.x + p_tab_style->get_margin(SIDE_LEFT); - int top_margin = p_tab_style->get_margin(SIDE_TOP); - int y_center = top_margin + (tab_rect.size.y - p_tab_style->get_minimum_size().y) / 2; - - // Draw the tab icon. - if (control->has_meta("_tab_icon")) { - Ref<Texture2D> icon = control->get_meta("_tab_icon"); - if (icon.is_valid()) { - int y = y_center - (icon->get_height() / 2); - icon->draw(canvas, Point2i(x_content, y)); - if (!text.is_empty()) { - x_content += icon->get_width() + icon_text_distance; - } - } - } - - // Draw the tab text. - 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) { + if (!theme_changing) { return; } - _refresh_texts(); - - update_minimum_size(); + tab_bar->add_theme_style_override(SNAME("tab_unselected"), get_theme_stylebox(SNAME("tab_unselected"))); + tab_bar->add_theme_style_override(SNAME("tab_selected"), get_theme_stylebox(SNAME("tab_selected"))); + tab_bar->add_theme_style_override(SNAME("tab_disabled"), get_theme_stylebox(SNAME("tab_disabled"))); + tab_bar->add_theme_icon_override(SNAME("increment"), get_theme_icon(SNAME("increment"))); + tab_bar->add_theme_icon_override(SNAME("increment_highlight"), get_theme_icon(SNAME("increment_highlight"))); + tab_bar->add_theme_icon_override(SNAME("decrement"), get_theme_icon(SNAME("decrement"))); + tab_bar->add_theme_icon_override(SNAME("decrement_highlight"), get_theme_icon(SNAME("decrement_highlight"))); + tab_bar->add_theme_icon_override(SNAME("drop_mark"), get_theme_icon(SNAME("drop_mark"))); + tab_bar->add_theme_color_override(SNAME("drop_mark_color"), get_theme_color(SNAME("drop_mark_color"))); + tab_bar->add_theme_color_override(SNAME("font_selected_color"), get_theme_color(SNAME("font_selected_color"))); + tab_bar->add_theme_color_override(SNAME("font_unselected_color"), get_theme_color(SNAME("font_unselected_color"))); + tab_bar->add_theme_color_override(SNAME("font_disabled_color"), get_theme_color(SNAME("font_disabled_color"))); + tab_bar->add_theme_color_override(SNAME("font_outline_color"), get_theme_color(SNAME("font_outline_color"))); + tab_bar->add_theme_font_override(SNAME("font"), get_theme_font(SNAME("font"))); + tab_bar->add_theme_font_size_override(SNAME("font_size"), get_theme_font_size(SNAME("font_size"))); + tab_bar->add_theme_constant_override(SNAME("h_separation"), get_theme_constant(SNAME("icon_separation"))); + tab_bar->add_theme_constant_override(SNAME("outline_size"), get_theme_constant(SNAME("outline_size"))); + + _update_margins(); if (get_tab_count() > 0) { _repaint(); - update(); + } else { + update_minimum_size(); } - _theme_changing = false; + update(); + + theme_changing = false; } void TabContainer::_repaint() { 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]; + Vector<Control *> controls = _get_tab_controls(); + int current = get_current_tab(); + + for (int i = 0; i < controls.size(); i++) { + Control *c = controls[i]; + if (i == current) { c->show(); c->set_anchors_and_offsets_preset(Control::PRESET_WIDE); + if (tabs_visible) { c->set_offset(SIDE_TOP, _get_top_margin()); } + 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(); } } -} - -void TabContainer::_on_mouse_exited() { - if (menu_hovered || highlight_arrow > -1) { - menu_hovered = false; - highlight_arrow = -1; - update(); - } -} - -int TabContainer::_get_tab_width(int p_index) const { - ERR_FAIL_INDEX_V(p_index, get_tab_count(), 0); - Control *control = get_tab_control(p_index); - if (!control || get_tab_hidden(p_index)) { - return 0; - } - - // Get the width of the text displayed on the tab. - Ref<Font> font = get_theme_font(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")) { - Ref<Texture2D> icon = control->get_meta("_tab_icon"); - if (icon.is_valid()) { - width += icon->get_width(); - if (!text.is_empty()) { - width += get_theme_constant(SNAME("icon_separation")); - } - } - } - - // Respect a minimum size. - Ref<StyleBox> tab_unselected = get_theme_stylebox(SNAME("tab_unselected")); - Ref<StyleBox> tab_selected = get_theme_stylebox(SNAME("tab_selected")); - Ref<StyleBox> tab_disabled = get_theme_stylebox(SNAME("tab_disabled")); - if (get_tab_disabled(p_index)) { - width += tab_disabled->get_minimum_size().width; - } else if (p_index == current) { - width += tab_selected->get_minimum_size().width; - } else { - width += tab_unselected->get_minimum_size().width; - } - return width; + update_minimum_size(); } -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_set_as_top_level()) { - continue; - } - - controls.push_back(control); - } - return controls; -} +void TabContainer::_update_margins() { + int menu_width = get_theme_icon(SNAME("menu"))->get_width(); + int side_margin = get_theme_constant(SNAME("side_margin")); -void TabContainer::_child_renamed_callback() { - _refresh_texts(); - update(); -} + // Directly check for validity, to avoid errors when quitting. + bool has_popup = popup_obj_id.is_valid(); -void TabContainer::add_child_notify(Node *p_child) { - Container::add_child_notify(p_child); + if (get_tab_count() == 0) { + tab_bar->set_offset(SIDE_LEFT, 0); + tab_bar->set_offset(SIDE_RIGHT, has_popup ? -menu_width : 0); - Control *c = Object::cast_to<Control>(p_child); - if (!c || c->is_set_as_top_level()) { return; } - Vector<Control *> tabs = _get_tabs(); - _refresh_texts(); - - bool first = false; - - if (tabs.size() != 1) { - c->hide(); - } else { - c->show(); - //call_deferred(SNAME("set_current_tab"),0); - first = true; - current = 0; - previous = 0; - } - c->set_anchors_and_offsets_preset(Control::PRESET_WIDE); - if (tabs_visible) { - c->set_offset(SIDE_TOP, _get_top_margin()); - } - 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(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; - } + switch (get_tab_alignment()) { + case TabBar::ALIGNMENT_LEFT: { + tab_bar->set_offset(SIDE_LEFT, side_margin); + tab_bar->set_offset(SIDE_RIGHT, has_popup ? -menu_width : 0); + } break; - _update_current_tab(); - update(); -} + case TabBar::ALIGNMENT_CENTER: { + tab_bar->set_offset(SIDE_LEFT, 0); + tab_bar->set_offset(SIDE_RIGHT, has_popup ? -menu_width : 0); + } break; -int TabContainer::get_tab_count() const { - return _get_tabs().size(); -} + case TabBar::ALIGNMENT_RIGHT: { + tab_bar->set_offset(SIDE_LEFT, 0); -void TabContainer::set_current_tab(int p_current) { - ERR_FAIL_INDEX(p_current, get_tab_count()); + if (has_popup) { + tab_bar->set_offset(SIDE_RIGHT, -menu_width); + return; + } - int pending_previous = current; - current = p_current; + int first_tab_pos = tab_bar->get_tab_rect(0).position.x; + Rect2 last_tab_rect = tab_bar->get_tab_rect(get_tab_count() - 1); + int total_tabs_width = last_tab_rect.position.x - first_tab_pos + last_tab_rect.size.width; - _repaint(); + // Calculate if all the tabs would still fit if the margin was present. + if (get_clip_tabs() && (tab_bar->get_offset_buttons_visible() || (get_tab_count() > 1 && (total_tabs_width + side_margin) > get_size().width))) { + tab_bar->set_offset(SIDE_RIGHT, has_popup ? -menu_width : 0); + } else { + tab_bar->set_offset(SIDE_RIGHT, -side_margin); + } + } break; - if (pending_previous == current) { - emit_signal(SNAME("tab_selected"), current); - } else { - previous = pending_previous; - emit_signal(SNAME("tab_selected"), current); - emit_signal(SNAME("tab_changed"), current); + case TabBar::ALIGNMENT_MAX: + break; // Can't happen, but silences warning. } - - update(); -} - -int TabContainer::get_current_tab() const { - return current; } -int TabContainer::get_previous_tab() const { - return previous; -} - -Control *TabContainer::get_tab_control(int p_idx) const { - Vector<Control *> tabs = _get_tabs(); - if (p_idx >= 0 && p_idx < tabs.size()) { - return tabs[p_idx]; - } else { - return nullptr; +void TabContainer::_on_mouse_exited() { + if (menu_hovered) { + menu_hovered = false; + update(); } } -Control *TabContainer::get_current_tab_control() const { - return get_tab_control(current); -} - -void TabContainer::remove_child_notify(Node *p_child) { - Container::remove_child_notify(p_child); +Vector<Control *> TabContainer::_get_tab_controls() 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_set_as_top_level() || control == tab_bar) { + continue; + } - Control *c = Object::cast_to<Control>(p_child); - if (!c || c->is_set_as_top_level()) { - return; + controls.push_back(control); } - // 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)); - - update(); -} - -void TabContainer::_update_current_tab() { - _refresh_texts(); - - int tc = get_tab_count(); - if (current >= tc) { - current = tc - 1; - } - if (current < 0) { - current = 0; - } else { - set_current_tab(current); - } + return controls; } -Variant TabContainer::get_drag_data(const Point2 &p_point) { +Variant TabContainer::_get_drag_data_fw(const Point2 &p_point, Control *p_from_control) { if (!drag_to_rearrange_enabled) { return Variant(); } int tab_over = get_tab_idx_at_point(p_point); - if (tab_over < 0) { return Variant(); } @@ -838,18 +340,20 @@ Variant TabContainer::get_drag_data(const Point2 &p_point) { tf->set_texture(icon); drag_preview->add_child(tf); } + Label *label = memnew(Label(get_tab_title(tab_over))); - drag_preview->add_child(label); set_drag_preview(drag_preview); + drag_preview->add_child(label); Dictionary drag_data; drag_data["type"] = "tabc_element"; drag_data["tabc_element"] = tab_over; drag_data["from_path"] = get_path(); + return drag_data; } -bool TabContainer::can_drop_data(const Point2 &p_point, const Variant &p_data) const { +bool TabContainer::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from_control) const { if (!drag_to_rearrange_enabled) { return false; } @@ -865,7 +369,7 @@ bool TabContainer::can_drop_data(const Point2 &p_point, const Variant &p_data) c if (from_path == to_path) { return true; } else if (get_tabs_rearrange_group() != -1) { - // drag and drop between other TabContainers + // Drag and drop between other TabContainers. Node *from_node = get_node(from_path); TabContainer *from_tabc = Object::cast_to<TabContainer>(from_node); if (from_tabc && from_tabc->get_tabs_rearrange_group() == get_tabs_rearrange_group()) { @@ -873,16 +377,15 @@ bool TabContainer::can_drop_data(const Point2 &p_point, const Variant &p_data) c } } } + return false; } -void TabContainer::drop_data(const Point2 &p_point, const Variant &p_data) { +void TabContainer::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from_control) { if (!drag_to_rearrange_enabled) { return; } - int hover_now = get_tab_idx_at_point(p_point); - Dictionary d = p_data; if (!d.has("type")) { return; @@ -890,91 +393,240 @@ void TabContainer::drop_data(const Point2 &p_point, const Variant &p_data) { if (String(d["type"]) == "tabc_element") { int tab_from_id = d["tabc_element"]; + int hover_now = get_tab_idx_at_point(p_point); NodePath from_path = d["from_path"]; NodePath to_path = get_path(); + if (from_path == to_path) { - if (hover_now < 0) { - hover_now = get_tab_count() - 1; + if (tab_from_id == hover_now) { + return; } - move_child(get_tab_control(tab_from_id), get_tab_control(hover_now)->get_index()); - set_current_tab(hover_now); + + // Drop the new tab to the left or right depending on where the target tab is being hovered. + if (hover_now != -1) { + Rect2 tab_rect = tab_bar->get_tab_rect(hover_now); + if (is_layout_rtl() ^ (p_point.x <= tab_rect.position.x + tab_rect.size.width / 2)) { + if (hover_now > tab_from_id) { + hover_now -= 1; + } + } else if (tab_from_id > hover_now) { + hover_now += 1; + } + } else { + hover_now = is_layout_rtl() ^ (p_point.x < tab_bar->get_tab_rect(0).position.x) ? 0 : get_tab_count() - 1; + } + + move_child(get_tab_control(tab_from_id), get_tab_control(hover_now)->get_index(false)); + if (!is_tab_disabled(hover_now)) { + set_current_tab(hover_now); + } + } else if (get_tabs_rearrange_group() != -1) { - // drag and drop between TabContainers + // Drag and drop between TabContainers. + Node *from_node = get_node(from_path); TabContainer *from_tabc = Object::cast_to<TabContainer>(from_node); + if (from_tabc && from_tabc->get_tabs_rearrange_group() == get_tabs_rearrange_group()) { + // Get the tab properties before they get erased by the child removal. + String tab_title = from_tabc->get_tab_title(tab_from_id); + bool tab_disabled = from_tabc->is_tab_disabled(tab_from_id); + + // Drop the new tab to the left or right depending on where the target tab is being hovered. + if (hover_now != -1) { + Rect2 tab_rect = tab_bar->get_tab_rect(hover_now); + if (is_layout_rtl() ^ (p_point.x > tab_rect.position.x + tab_rect.size.width / 2)) { + hover_now += 1; + } + } else { + hover_now = is_layout_rtl() ^ (p_point.x < tab_bar->get_tab_rect(0).position.x) ? 0 : get_tab_count(); + } + Control *moving_tabc = from_tabc->get_tab_control(tab_from_id); from_tabc->remove_child(moving_tabc); - add_child(moving_tabc, false, INTERNAL_MODE_FRONT); - if (hover_now < 0) { - hover_now = get_tab_count() - 1; + add_child(moving_tabc, true); + + set_tab_title(get_tab_count() - 1, tab_title); + set_tab_disabled(get_tab_count() - 1, tab_disabled); + + move_child(moving_tabc, get_tab_control(hover_now)->get_index(false)); + if (!is_tab_disabled(hover_now)) { + set_current_tab(hover_now); } - move_child(moving_tabc, get_tab_control(hover_now)->get_index()); - set_current_tab(hover_now); - emit_signal(SNAME("tab_changed"), hover_now); } } } - update(); } -int TabContainer::get_tab_idx_at_point(const Point2 &p_point) const { - if (get_tab_count() == 0) { - return -1; +void TabContainer::_on_tab_changed(int p_tab) { + call_deferred(SNAME("_repaint")); + + emit_signal(SNAME("tab_changed"), p_tab); +} + +void TabContainer::_on_tab_selected(int p_tab) { + if (p_tab != get_previous_tab()) { + call_deferred(SNAME("_repaint")); + } + + emit_signal(SNAME("tab_selected"), p_tab); +} + +void TabContainer::_on_tab_button_pressed(int p_tab) { + emit_signal(SNAME("tab_button_pressed"), p_tab); +} + +void TabContainer::_refresh_tab_names() { + Vector<Control *> controls = _get_tab_controls(); + for (int i = 0; i < controls.size(); i++) { + if (!controls[i]->has_meta("_tab_name") && String(controls[i]->get_name()) != get_tab_title(i)) { + tab_bar->set_tab_title(i, controls[i]->get_name()); + } } +} - // must be on tabs in the tab header area. - if (p_point.y > _get_top_margin()) { - return -1; +void TabContainer::add_child_notify(Node *p_child) { + if (p_child == tab_bar) { + return; } - Size2 size = get_size(); - int button_ofs = 0; - int px = p_point.x; + Container::add_child_notify(p_child); - if (is_layout_rtl()) { - px = size.width - px; + Control *c = Object::cast_to<Control>(p_child); + if (!c || c->is_set_as_top_level()) { + return; } + c->hide(); + + tab_bar->add_tab(p_child->get_name()); - if (px < tabs_ofs_cache) { - return -1; + _update_margins(); + if (get_tab_count() == 1) { + update(); } - Popup *popup = get_popup(); - if (popup) { - Ref<Texture2D> menu = get_theme_icon(SNAME("menu")); - button_ofs += menu->get_width(); + p_child->connect("renamed", callable_mp(this, &TabContainer::_refresh_tab_names)); + + // TabBar won't emit the "tab_changed" signal when not inside the tree. + if (!is_inside_tree()) { + call_deferred("_repaint"); + } +} + +void TabContainer::move_child_notify(Node *p_child) { + if (p_child == tab_bar) { + return; + } + + Container::move_child_notify(p_child); + + Control *c = Object::cast_to<Control>(p_child); + if (c && !c->is_set_as_top_level()) { + int old_idx = -1; + String tab_name = String(c->get_meta("_tab_name", c->get_name())); + + // Find the previous tab index of the control. + for (int i = 0; i < get_tab_count(); i++) { + if (get_tab_title(i) == tab_name) { + old_idx = i; + break; + } + } + + tab_bar->move_tab(old_idx, get_tab_idx_from_control(c)); } - if (buttons_visible_cache) { - Ref<Texture2D> increment = get_theme_icon(SNAME("increment")); - Ref<Texture2D> decrement = get_theme_icon(SNAME("decrement")); - button_ofs += increment->get_width() + decrement->get_width(); +} + +void TabContainer::remove_child_notify(Node *p_child) { + if (p_child == tab_bar) { + return; } - if (px > size.width - button_ofs) { - return -1; + + Container::remove_child_notify(p_child); + + Control *c = Object::cast_to<Control>(p_child); + if (!c || c->is_set_as_top_level()) { + return; } - // get the tab at the point - Vector<Control *> tabs = _get_tabs(); - px -= tabs_ofs_cache; - for (int i = first_tab_cache; i <= last_tab_cache; i++) { - int tab_width = _get_tab_width(i); - if (px < tab_width) { + tab_bar->remove_tab(get_tab_idx_from_control(c)); + + _update_margins(); + if (get_tab_count() == 0) { + update(); + } + + p_child->remove_meta("_tab_name"); + p_child->disconnect("renamed", callable_mp(this, &TabContainer::_refresh_tab_names)); + + // TabBar won't emit the "tab_changed" signal when not inside the tree. + if (!is_inside_tree()) { + call_deferred("_repaint"); + } +} + +int TabContainer::get_tab_count() const { + return tab_bar->get_tab_count(); +} + +void TabContainer::set_current_tab(int p_current) { + tab_bar->set_current_tab(p_current); +} + +int TabContainer::get_current_tab() const { + return tab_bar->get_current_tab(); +} + +int TabContainer::get_previous_tab() const { + return tab_bar->get_previous_tab(); +} + +Control *TabContainer::get_tab_control(int p_idx) const { + Vector<Control *> controls = _get_tab_controls(); + if (p_idx >= 0 && p_idx < controls.size()) { + return controls[p_idx]; + } else { + return nullptr; + } +} + +Control *TabContainer::get_current_tab_control() const { + return get_tab_control(tab_bar->get_current_tab()); +} + +int TabContainer::get_tab_idx_at_point(const Point2 &p_point) const { + return tab_bar->get_tab_idx_at_point(p_point); +} + +int TabContainer::get_tab_idx_from_control(Control *p_child) const { + ERR_FAIL_NULL_V(p_child, -1); + ERR_FAIL_COND_V(p_child->get_parent() != this, -1); + + Vector<Control *> controls = _get_tab_controls(); + for (int i = 0; i < controls.size(); i++) { + if (controls[i] == p_child) { return i; } - px -= tab_width; } + return -1; } -void TabContainer::set_tab_alignment(AlignmentMode p_alignment) { - ERR_FAIL_INDEX(p_alignment, 3); - alignment = p_alignment; - update(); +void TabContainer::set_tab_alignment(TabBar::AlignmentMode p_alignment) { + tab_bar->set_tab_alignment(p_alignment); + _update_margins(); +} + +TabBar::AlignmentMode TabContainer::get_tab_alignment() const { + return tab_bar->get_tab_alignment(); } -TabContainer::AlignmentMode TabContainer::get_tab_alignment() const { - return alignment; +void TabContainer::set_clip_tabs(bool p_clip_tabs) { + tab_bar->set_clip_tabs(p_clip_tabs); +} + +bool TabContainer::get_clip_tabs() const { + return tab_bar->get_clip_tabs(); } void TabContainer::set_tabs_visible(bool p_visible) { @@ -983,11 +635,12 @@ void TabContainer::set_tabs_visible(bool p_visible) { } tabs_visible = p_visible; + tab_bar->set_visible(tabs_visible); - Vector<Control *> tabs = _get_tabs(); - for (int i = 0; i < tabs.size(); i++) { - Control *c = tabs[i]; - if (p_visible) { + Vector<Control *> controls = _get_tab_controls(); + for (int i = 0; i < controls.size(); i++) { + Control *c = controls[i]; + if (tabs_visible) { c->set_offset(SIDE_TOP, _get_top_margin()); } else { c->set_offset(SIDE_TOP, 0); @@ -1009,7 +662,8 @@ void TabContainer::set_all_tabs_in_front(bool p_in_front) { all_tabs_in_front = p_in_front; - update(); + remove_child(tab_bar); + add_child(tab_bar, false, all_tabs_in_front ? INTERNAL_MODE_FRONT : INTERNAL_MODE_BACK); } bool TabContainer::is_all_tabs_in_front() const { @@ -1019,95 +673,87 @@ bool TabContainer::is_all_tabs_in_front() const { void TabContainer::set_tab_title(int p_tab, const String &p_title) { 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_control(p_tab); - ERR_FAIL_COND_V(!child, ""); - if (child->has_meta("_tab_name")) { - return child->get_meta("_tab_name"); + tab_bar->set_tab_title(p_tab, p_title); + + if (p_title == child->get_name()) { + child->remove_meta("_tab_name"); } else { - return child->get_name(); + child->set_meta("_tab_name", p_title); + } + + _update_margins(); + if (!get_clip_tabs()) { + update_minimum_size(); } } +String TabContainer::get_tab_title(int p_tab) const { + return tab_bar->get_tab_title(p_tab); +} + void TabContainer::set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon) { - Control *child = get_tab_control(p_tab); - ERR_FAIL_COND(!child); - child->set_meta("_tab_icon", p_icon); - update(); + tab_bar->set_tab_icon(p_tab, p_icon); + + _update_margins(); + _repaint(); } Ref<Texture2D> TabContainer::get_tab_icon(int p_tab) const { - 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"); - } else { - return Ref<Texture2D>(); - } + return tab_bar->get_tab_icon(p_tab); } void TabContainer::set_tab_disabled(int p_tab, bool p_disabled) { - Control *child = get_tab_control(p_tab); - ERR_FAIL_COND(!child); - child->set_meta("_tab_disabled", p_disabled); - update(); -} + tab_bar->set_tab_disabled(p_tab, p_disabled); -bool TabContainer::get_tab_disabled(int p_tab) const { - 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"); - } else { - return false; + _update_margins(); + if (!get_clip_tabs()) { + update_minimum_size(); } } +bool TabContainer::is_tab_disabled(int p_tab) const { + return tab_bar->is_tab_disabled(p_tab); +} + void TabContainer::set_tab_hidden(int p_tab, bool p_hidden) { Control *child = get_tab_control(p_tab); ERR_FAIL_COND(!child); - child->set_meta("_tab_hidden", p_hidden); - update(); - for (int i = 0; i < get_tab_count(); i++) { - int try_tab = (p_tab + 1 + i) % get_tab_count(); - if (get_tab_disabled(try_tab) || get_tab_hidden(try_tab)) { - continue; - } - set_current_tab(try_tab); - return; + tab_bar->set_tab_hidden(p_tab, p_hidden); + child->hide(); + + _update_margins(); + if (!get_clip_tabs()) { + update_minimum_size(); } +} - //assumed no other tab can be switched to, just hide - child->hide(); +bool TabContainer::is_tab_hidden(int p_tab) const { + return tab_bar->is_tab_hidden(p_tab); } -bool TabContainer::get_tab_hidden(int p_tab) const { - 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"); - } else { - return false; - } +void TabContainer::set_tab_button_icon(int p_tab, const Ref<Texture2D> &p_icon) { + tab_bar->set_tab_button_icon(p_tab, p_icon); + + _update_margins(); + _repaint(); +} + +Ref<Texture2D> TabContainer::get_tab_button_icon(int p_tab) const { + return tab_bar->get_tab_button_icon(p_tab); } void TabContainer::get_translatable_strings(List<String> *p_strings) const { - Vector<Control *> tabs = _get_tabs(); - for (int i = 0; i < tabs.size(); i++) { - Control *c = tabs[i]; + Vector<Control *> controls = _get_tab_controls(); + for (int i = 0; i < controls.size(); i++) { + Control *c = controls[i]; if (!c->has_meta("_tab_name")) { continue; } String name = c->get_meta("_tab_name"); - if (!name.is_empty()) { p_strings->push_back(name); } @@ -1117,9 +763,26 @@ void TabContainer::get_translatable_strings(List<String> *p_strings) const { Size2 TabContainer::get_minimum_size() const { Size2 ms; - Vector<Control *> tabs = _get_tabs(); - for (int i = 0; i < tabs.size(); i++) { - Control *c = tabs[i]; + if (tabs_visible) { + ms = tab_bar->get_minimum_size(); + + if (!get_clip_tabs()) { + if (get_popup()) { + ms.x += get_theme_icon(SNAME("menu"))->get_width(); + } + + int side_margin = get_theme_constant(SNAME("side_margin")); + if (side_margin > 0 && get_tab_alignment() != TabBar::ALIGNMENT_CENTER && + (get_tab_alignment() != TabBar::ALIGNMENT_RIGHT || !get_popup())) { + ms.x += side_margin; + } + } + } + + Vector<Control *> controls = _get_tab_controls(); + int max_control_height = 0; + for (int i = 0; i < controls.size(); i++) { + Control *c = controls[i]; if (!c->is_visible_in_tree() && !use_hidden_tabs_for_min_size) { continue; @@ -1127,29 +790,30 @@ Size2 TabContainer::get_minimum_size() const { Size2 cms = c->get_combined_minimum_size(); ms.x = MAX(ms.x, cms.x); - ms.y = MAX(ms.y, cms.y); + max_control_height = MAX(max_control_height, cms.y); } + ms.y += max_control_height; - 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 (tabs_visible) { - ms.y += MAX(MAX(tab_unselected->get_minimum_size().y, tab_selected->get_minimum_size().y), tab_disabled->get_minimum_size().y); - ms.y += _get_top_margin(); - } - - Ref<StyleBox> sb = get_theme_stylebox(SNAME("panel")); - ms += sb->get_minimum_size(); + Size2 panel_ms = get_theme_stylebox(SNAME("panel"))->get_minimum_size(); + ms.x = MAX(ms.x, panel_ms.x); + ms.y += panel_ms.y; return ms; } void TabContainer::set_popup(Node *p_popup) { - ERR_FAIL_NULL(p_popup); + bool had_popup = get_popup(); + Popup *popup = Object::cast_to<Popup>(p_popup); popup_obj_id = popup ? popup->get_instance_id() : ObjectID(); - update(); + + if (had_popup != bool(popup)) { + update(); + _update_margins(); + if (!get_clip_tabs()) { + update_minimum_size(); + } + } } Popup *TabContainer::get_popup() const { @@ -1164,6 +828,7 @@ Popup *TabContainer::get_popup() const { popup_obj_id = ObjectID(); } } + return nullptr; } @@ -1176,21 +841,30 @@ bool TabContainer::get_drag_to_rearrange_enabled() const { } void TabContainer::set_tabs_rearrange_group(int p_group_id) { - tabs_rearrange_group = p_group_id; + tab_bar->set_tabs_rearrange_group(p_group_id); } int TabContainer::get_tabs_rearrange_group() const { - return tabs_rearrange_group; + return tab_bar->get_tabs_rearrange_group(); } void TabContainer::set_use_hidden_tabs_for_min_size(bool p_use_hidden_tabs) { use_hidden_tabs_for_min_size = p_use_hidden_tabs; + update_minimum_size(); } bool TabContainer::get_use_hidden_tabs_for_min_size() const { return use_hidden_tabs_for_min_size; } +Vector<int> TabContainer::get_allowed_size_flags_horizontal() const { + return Vector<int>(); +} + +Vector<int> TabContainer::get_allowed_size_flags_vertical() const { + return Vector<int>(); +} + void TabContainer::_bind_methods() { 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); @@ -1200,6 +874,8 @@ void TabContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_tab_control", "tab_idx"), &TabContainer::get_tab_control); ClassDB::bind_method(D_METHOD("set_tab_alignment", "alignment"), &TabContainer::set_tab_alignment); ClassDB::bind_method(D_METHOD("get_tab_alignment"), &TabContainer::get_tab_alignment); + ClassDB::bind_method(D_METHOD("set_clip_tabs", "clip_tabs"), &TabContainer::set_clip_tabs); + ClassDB::bind_method(D_METHOD("get_clip_tabs"), &TabContainer::get_clip_tabs); 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); @@ -1209,39 +885,51 @@ void TabContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &TabContainer::set_tab_icon); ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &TabContainer::get_tab_icon); ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &TabContainer::set_tab_disabled); - ClassDB::bind_method(D_METHOD("get_tab_disabled", "tab_idx"), &TabContainer::get_tab_disabled); + ClassDB::bind_method(D_METHOD("is_tab_disabled", "tab_idx"), &TabContainer::is_tab_disabled); ClassDB::bind_method(D_METHOD("set_tab_hidden", "tab_idx", "hidden"), &TabContainer::set_tab_hidden); - ClassDB::bind_method(D_METHOD("get_tab_hidden", "tab_idx"), &TabContainer::get_tab_hidden); + ClassDB::bind_method(D_METHOD("is_tab_hidden", "tab_idx"), &TabContainer::is_tab_hidden); + ClassDB::bind_method(D_METHOD("set_tab_button_icon", "tab_idx", "icon"), &TabContainer::set_tab_button_icon); + ClassDB::bind_method(D_METHOD("get_tab_button_icon", "tab_idx"), &TabContainer::get_tab_button_icon); ClassDB::bind_method(D_METHOD("get_tab_idx_at_point", "point"), &TabContainer::get_tab_idx_at_point); + ClassDB::bind_method(D_METHOD("get_tab_idx_from_control", "control"), &TabContainer::get_tab_idx_from_control); ClassDB::bind_method(D_METHOD("set_popup", "popup"), &TabContainer::set_popup); ClassDB::bind_method(D_METHOD("get_popup"), &TabContainer::get_popup); ClassDB::bind_method(D_METHOD("set_drag_to_rearrange_enabled", "enabled"), &TabContainer::set_drag_to_rearrange_enabled); ClassDB::bind_method(D_METHOD("get_drag_to_rearrange_enabled"), &TabContainer::get_drag_to_rearrange_enabled); ClassDB::bind_method(D_METHOD("set_tabs_rearrange_group", "group_id"), &TabContainer::set_tabs_rearrange_group); ClassDB::bind_method(D_METHOD("get_tabs_rearrange_group"), &TabContainer::get_tabs_rearrange_group); - ClassDB::bind_method(D_METHOD("set_use_hidden_tabs_for_min_size", "enabled"), &TabContainer::set_use_hidden_tabs_for_min_size); ClassDB::bind_method(D_METHOD("get_use_hidden_tabs_for_min_size"), &TabContainer::get_use_hidden_tabs_for_min_size); + ClassDB::bind_method(D_METHOD("_repaint"), &TabContainer::_repaint); ClassDB::bind_method(D_METHOD("_on_theme_changed"), &TabContainer::_on_theme_changed); - ClassDB::bind_method(D_METHOD("_update_current_tab"), &TabContainer::_update_current_tab); + ClassDB::bind_method(D_METHOD("_get_drag_data_fw"), &TabContainer::_get_drag_data_fw); + ClassDB::bind_method(D_METHOD("_can_drop_data_fw"), &TabContainer::_can_drop_data_fw); + ClassDB::bind_method(D_METHOD("_drop_data_fw"), &TabContainer::_drop_data_fw); ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("tab_selected", PropertyInfo(Variant::INT, "tab"))); + ADD_SIGNAL(MethodInfo("tab_button_pressed", PropertyInfo(Variant::INT, "tab"))); ADD_SIGNAL(MethodInfo("pre_popup_pressed")); ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_alignment", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_tab_alignment", "get_tab_alignment"); 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, "clip_tabs"), "set_clip_tabs", "get_clip_tabs"); 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::INT, "tabs_rearrange_group"), "set_tabs_rearrange_group", "get_tabs_rearrange_group"); 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"); - - BIND_ENUM_CONSTANT(ALIGNMENT_LEFT); - BIND_ENUM_CONSTANT(ALIGNMENT_CENTER); - BIND_ENUM_CONSTANT(ALIGNMENT_RIGHT); } TabContainer::TabContainer() { + tab_bar = memnew(TabBar); + tab_bar->set_drag_forwarding(this); + add_child(tab_bar, false, INTERNAL_MODE_FRONT); + tab_bar->set_anchors_and_offsets_preset(Control::PRESET_TOP_WIDE); + tab_bar->connect("tab_changed", callable_mp(this, &TabContainer::_on_tab_changed)); + tab_bar->connect("tab_selected", callable_mp(this, &TabContainer::_on_tab_selected)); + tab_bar->connect("tab_button_pressed", callable_mp(this, &TabContainer::_on_tab_button_pressed)); + 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 6dfeb2b625..9adaa0d844 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,65 +33,52 @@ #include "scene/gui/container.h" #include "scene/gui/popup.h" -#include "scene/resources/text_line.h" +#include "scene/gui/tab_bar.h" class TabContainer : public Container { GDCLASS(TabContainer, Container); -public: - enum AlignmentMode { - ALIGNMENT_LEFT, - ALIGNMENT_CENTER, - ALIGNMENT_RIGHT, - }; - -private: - int first_tab_cache = 0; - int tabs_ofs_cache = 0; - int last_tab_cache = 0; - int current = 0; - int previous = 0; + TabBar *tab_bar = nullptr; bool tabs_visible = true; bool all_tabs_in_front = false; - bool buttons_visible_cache = false; bool menu_hovered = false; - int highlight_arrow = -1; - AlignmentMode alignment = ALIGNMENT_CENTER; - int _get_top_margin() const; mutable ObjectID popup_obj_id; bool drag_to_rearrange_enabled = false; bool use_hidden_tabs_for_min_size = false; - int tabs_rearrange_group = -1; + bool theme_changing = false; - Vector<Ref<TextLine>> text_buf; - Vector<Control *> _get_tabs() const; - int _get_tab_width(int p_index) const; - bool _theme_changing = false; + int _get_top_margin() const; + Vector<Control *> _get_tab_controls() const; void _on_theme_changed(); void _repaint(); + void _refresh_tab_names(); + void _update_margins(); 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(); + void _on_tab_changed(int p_tab); + void _on_tab_selected(int p_tab); + void _on_tab_button_pressed(int p_tab); + + Variant _get_drag_data_fw(const Point2 &p_point, Control *p_from_control); + bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from_control) const; + void _drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from_control); protected: - void _child_renamed_callback(); 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; + static void _bind_methods(); - Variant get_drag_data(const Point2 &p_point) override; - bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override; - void drop_data(const Point2 &p_point, const Variant &p_data) override; +public: int get_tab_idx_at_point(const Point2 &p_point) const; + int get_tab_idx_from_control(Control *p_child) const; - static void _bind_methods(); + void set_tab_alignment(TabBar::AlignmentMode p_alignment); + TabBar::AlignmentMode get_tab_alignment() const; -public: - void set_tab_alignment(AlignmentMode p_alignment); - AlignmentMode get_tab_alignment() const; + void set_clip_tabs(bool p_clip_tabs); + bool get_clip_tabs() const; void set_tabs_visible(bool p_visible); bool are_tabs_visible() const; @@ -106,10 +93,13 @@ public: Ref<Texture2D> get_tab_icon(int p_tab) const; void set_tab_disabled(int p_tab, bool p_disabled); - bool get_tab_disabled(int p_tab) const; + bool is_tab_disabled(int p_tab) const; void set_tab_hidden(int p_tab, bool p_hidden); - bool get_tab_hidden(int p_tab) const; + bool is_tab_hidden(int p_tab) const; + + void set_tab_button_icon(int p_tab, const Ref<Texture2D> &p_icon); + Ref<Texture2D> get_tab_button_icon(int p_tab) const; int get_tab_count() const; void set_current_tab(int p_current); @@ -133,9 +123,10 @@ public: void set_use_hidden_tabs_for_min_size(bool p_use_hidden_tabs); bool get_use_hidden_tabs_for_min_size() const; + virtual Vector<int> get_allowed_size_flags_horizontal() const override; + virtual Vector<int> get_allowed_size_flags_vertical() const override; + TabContainer(); }; -VARIANT_ENUM_CAST(TabContainer::AlignmentMode); - #endif // TAB_CONTAINER_H diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 63e3740a97..8e948203f1 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,18 +43,6 @@ #include "scene/main/window.h" -static bool _is_text_char(char32_t c) { - return !is_symbol(c); -} - -static bool _is_whitespace(char32_t c) { - return c == '\t' || c == ' '; -} - -static bool _is_char(char32_t c) { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'; -} - /////////////////////////////////////////////////////////////////////////////// /// TEXT /// /////////////////////////////////////////////////////////////////////////////// @@ -128,6 +116,10 @@ void TextEdit::Text::set_width(float p_width) { width = p_width; } +float TextEdit::Text::get_width() const { + return width; +} + int TextEdit::Text::get_line_wrap_amount(int p_line) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); @@ -183,29 +175,44 @@ void TextEdit::Text::_calculate_max_line_width() { max_width = width; } -void TextEdit::Text::invalidate_cache(int p_line, int p_column, const String &p_ime_text, const Array &p_bidi_override) { +void TextEdit::Text::invalidate_cache(int p_line, int p_column, bool p_text_changed, const String &p_ime_text, const Array &p_bidi_override) { ERR_FAIL_INDEX(p_line, text.size()); if (font.is_null() || font_size <= 0) { return; // Not in tree? } - text.write[p_line].data_buf->clear(); + if (p_text_changed) { + 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_text_changed) { + 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 (p_text_changed) { + 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); } } + if (!p_text_changed) { + RID r = text.write[p_line].data_buf->get_rid(); + int spans = TS->shaped_get_span_count(r); + for (int i = 0; i < spans; i++) { + TS->shaped_set_span_update_font(r, i, font->get_rids(), font_size, opentype_features); + } + } + // Apply tab align. if (tab_size > 0) { Vector<float> tabs; @@ -262,6 +269,24 @@ void TextEdit::Text::invalidate_all_lines() { } } +void TextEdit::Text::invalidate_font() { + if (!is_dirty) { + return; + } + + max_width = -1; + line_height = -1; + + if (!font.is_null() && font_size > 0) { + font_height = font->get_height(font_size); + } + + for (int i = 0; i < text.size(); i++) { + invalidate_cache(i, -1, false); + } + is_dirty = false; +} + void TextEdit::Text::invalidate_all() { if (!is_dirty) { return; @@ -275,7 +300,7 @@ void TextEdit::Text::invalidate_all() { } for (int i = 0; i < text.size(); i++) { - invalidate_cache(i); + invalidate_cache(i, -1, true); } is_dirty = false; } @@ -290,7 +315,7 @@ void TextEdit::Text::clear() { line.gutters.resize(gutter_count); line.data = ""; text.insert(0, line); - invalidate_cache(0); + invalidate_cache(0, -1, true); } int TextEdit::Text::get_max_width() const { @@ -302,7 +327,7 @@ void TextEdit::Text::set(int p_line, const String &p_text, const Array &p_bidi_o text.write[p_line].data = p_text; text.write[p_line].bidi_override = p_bidi_override; - invalidate_cache(p_line); + invalidate_cache(p_line, -1, true); } void TextEdit::Text::insert(int p_at, const Vector<String> &p_text, const Vector<Array> &p_bidi_override) { @@ -327,7 +352,7 @@ void TextEdit::Text::insert(int p_at, const Vector<String> &p_text, const Vector line.data = p_text[i]; line.bidi_override = p_bidi_override[i]; text.write[p_at + i] = line; - invalidate_cache(p_at + i); + invalidate_cache(p_at + i, -1, true); } } @@ -407,32 +432,38 @@ void TextEdit::_notification(int p_what) { } _update_wrap_at_column(true); } break; + case NOTIFICATION_RESIZED: { _update_scrollbars(); _update_wrap_at_column(); } break; + case NOTIFICATION_VISIBILITY_CHANGED: { if (is_visible()) { call_deferred(SNAME("_update_scrollbars")); call_deferred(SNAME("_update_wrap_at_column")); } } break; + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: case NOTIFICATION_TRANSLATION_CHANGED: case NOTIFICATION_THEME_CHANGED: { _update_caches(); _update_wrap_at_column(true); } break; + case NOTIFICATION_WM_WINDOW_FOCUS_IN: { window_has_focus = true; draw_caret = true; update(); } break; + case NOTIFICATION_WM_WINDOW_FOCUS_OUT: { window_has_focus = false; draw_caret = false; update(); } break; + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { if (scrolling && get_v_scroll() != target_v_scroll) { double target_y = target_v_scroll - get_v_scroll(); @@ -440,6 +471,11 @@ void TextEdit::_notification(int p_what) { // To ensure minimap is responsive override the speed setting. double vel = ((target_y / dist) * ((minimap_clicked) ? 3000 : v_scroll_speed)) * get_physics_process_delta_time(); + // Prevent too small velocity to block scrolling + if (Math::abs(vel) < v_scroll->get_step()) { + vel = v_scroll->get_step() * SIGN(vel); + } + if (Math::abs(vel) >= dist) { set_v_scroll(target_v_scroll); scrolling = false; @@ -454,6 +490,7 @@ void TextEdit::_notification(int p_what) { set_physics_process_internal(false); } } break; + case NOTIFICATION_DRAW: { if (first_draw) { // Size may not be the final one, so attempts to ensure caret was visible may have failed. @@ -646,6 +683,8 @@ void TextEdit::_notification(int p_what) { } } + bool draw_placeholder = text.size() == 1 && text[0].length() == 0; + // Get the highlighted words. String highlighted_text = get_selected_text(); @@ -656,7 +695,7 @@ void TextEdit::_notification(int p_what) { 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); + draw_amount += draw_placeholder ? placeholder_wraped_rows.size() - 1 : get_line_wrap_count(first_visible_line + 1); // Draw minimap. if (draw_minimap) { @@ -781,8 +820,8 @@ 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 + minimap_width); - bool is_whitespace = _is_whitespace(str[j]); - if (!is_whitespace) { + bool whitespace = is_whitespace(str[j]); + if (!whitespace) { characters++; if (j < str.length() - 1 && color == previous_color && !out_of_bounds) { @@ -804,7 +843,7 @@ void TextEdit::_notification(int p_what) { if (characters > 0) { previous_color.a *= 0.6; // 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 chars = MAX(0, (j - (characters - 1)) - (whitespace ? 1 : 0)) + 1; int char_x_ofs = indent_px + ((xmargin_end + minimap_char_size.x) + (minimap_char_size.x * chars)) + tabs; 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); @@ -841,7 +880,7 @@ void TextEdit::_notification(int p_what) { // Draw main text. caret.visible = false; line_drawing_cache.clear(); - int row_height = get_line_height(); + int row_height = draw_placeholder ? placeholder_line_height + line_spacing : get_line_height(); int line = first_visible_line; for (int i = 0; i < draw_amount; i++) { line++; @@ -867,11 +906,14 @@ void TextEdit::_notification(int p_what) { // Ensure we at least use the font color. Color current_color = !editable ? font_readonly_color : font_color; + if (draw_placeholder) { + current_color = font_placeholder_color; + } - const Ref<TextParagraph> ldata = text.get_line_data(line); + const Ref<TextParagraph> ldata = draw_placeholder ? placeholder_data_buf : text.get_line_data(line); - Vector<String> wrap_rows = get_line_wrapped_text(line); - int line_wrap_amount = get_line_wrap_count(line); + Vector<String> wrap_rows = draw_placeholder ? placeholder_wraped_rows : get_line_wrapped_text(line); + int line_wrap_amount = draw_placeholder ? placeholder_wraped_rows.size() - 1 : get_line_wrap_count(line); for (int line_wrap_index = 0; line_wrap_index <= line_wrap_amount; line_wrap_index++) { if (line_wrap_index != 0) { @@ -931,7 +973,7 @@ void TextEdit::_notification(int p_what) { // 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 = font->get_char_size(' ', 0, font_size).width; + float 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 { @@ -1008,15 +1050,17 @@ void TextEdit::_notification(int p_what) { icon->draw_rect(ci, gutter_rect, false, get_line_gutter_item_color(line, g)); } break; 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, 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)); + if (gutter.custom_draw_callback.is_valid()) { + 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; } + + Variant args[3] = { line, g, Rect2(gutter_rect) }; + const Variant *argp[] = { &args[0], &args[1], &args[2] }; + Callable::CallError ce; + Variant ret; + gutter.custom_draw_callback.call(argp, 3, ret, ce); } } break; } @@ -1100,7 +1144,7 @@ void TextEdit::_notification(int p_what) { } if (!clipped && lookup_symbol_word.length() != 0) { // Highlight word - if (_is_char(lookup_symbol_word[0]) || lookup_symbol_word[0] == '.') { + if (is_ascii_char(lookup_symbol_word[0]) || 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); @@ -1135,7 +1179,7 @@ void TextEdit::_notification(int p_what) { int first_visible_char = TS->shaped_text_get_range(rid).y; int last_visible_char = TS->shaped_text_get_range(rid).x; - int char_ofs = 0; + float 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++) { @@ -1170,7 +1214,7 @@ void TextEdit::_notification(int p_what) { } } - int char_pos = char_ofs + char_margin + ofs_x; + float 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) || @@ -1244,7 +1288,8 @@ void TextEdit::_notification(int p_what) { } // Carets. - int caret_width = Math::round(1 * get_theme_default_base_scale()); + // Prevent carets from disappearing at theme scales below 1.0 (if the caret width is 1). + const int caret_width = get_theme_constant(SNAME("caret_width")) * MAX(1, get_theme_default_base_scale()); if (!clipped && caret.line == line && line_wrap_index == caret_wrap_index) { caret.draw_pos.y = ofs_y + ldata->get_line_descent(line_wrap_index); @@ -1383,7 +1428,9 @@ void TextEdit::_notification(int p_what) { } } - line_drawing_cache[line] = cache_entry; + if (!draw_placeholder) { + line_drawing_cache[line] = cache_entry; + } } if (has_focus()) { @@ -1393,6 +1440,7 @@ void TextEdit::_notification(int p_what) { } } } break; + case NOTIFICATION_FOCUS_ENTER: { if (caret_blink_enabled) { caret_blink_timer->start(); @@ -1424,6 +1472,7 @@ void TextEdit::_notification(int p_what) { DisplayServer::get_singleton()->virtual_keyboard_show(get_text(), get_global_rect(), true, -1, caret_start, caret_end); } } break; + case NOTIFICATION_FOCUS_EXIT: { if (caret_blink_enabled) { caret_blink_timer->stop(); @@ -1433,9 +1482,11 @@ void TextEdit::_notification(int p_what) { 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 (!ime_text.is_empty()) { + ime_text = ""; + ime_selection = Point2(); + text.invalidate_cache(caret.line, caret.column, true, ime_text); + } if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_VIRTUAL_KEYBOARD) && virtual_keyboard_enabled) { DisplayServer::get_singleton()->virtual_keyboard_hide(); @@ -1445,6 +1496,7 @@ void TextEdit::_notification(int p_what) { deselect(); } } break; + case MainLoop::NOTIFICATION_OS_IME_UPDATE: { if (has_focus()) { ime_text = DisplayServer::get_singleton()->ime_get_text(); @@ -1457,11 +1509,12 @@ void TextEdit::_notification(int p_what) { t = ime_text; } - text.invalidate_cache(caret.line, caret.column, t, structured_text_parser(st_parser, st_args, t)); + text.invalidate_cache(caret.line, caret.column, true, t, structured_text_parser(st_parser, st_args, t)); update(); } } break; - case Control::NOTIFICATION_DRAG_BEGIN: { + + case NOTIFICATION_DRAG_BEGIN: { selection.selecting_mode = SelectionMode::SELECTION_MODE_NONE; drag_action = true; dragging_minimap = false; @@ -1469,7 +1522,8 @@ void TextEdit::_notification(int p_what) { can_drag_minimap = false; click_select_held->stop(); } break; - case Control::NOTIFICATION_DRAG_END: { + + case NOTIFICATION_DRAG_END: { if (is_drag_successful()) { if (selection.drag_attempt) { selection.drag_attempt = false; @@ -1488,6 +1542,62 @@ void TextEdit::_notification(int p_what) { } } +void TextEdit::unhandled_key_input(const Ref<InputEvent> &p_event) { + Ref<InputEventKey> k = p_event; + + if (k.is_valid()) { + if (!k->is_pressed()) { + return; + } + // Handle Unicode (with modifiers active, process after shortcuts). + if (has_focus() && editable && (k->get_unicode() >= 32)) { + handle_unicode_input(k->get_unicode()); + accept_event(); + } + } +} + +bool TextEdit::alt_input(const Ref<InputEvent> &p_gui_input) { + Ref<InputEventKey> k = p_gui_input; + if (k.is_valid()) { + if (!k->is_pressed()) { + if (alt_start && k->get_keycode() == Key::ALT) { + alt_start = false; + if ((alt_code > 0x31 && alt_code < 0xd800) || (alt_code > 0xdfff && alt_code <= 0x10ffff)) { + handle_unicode_input(alt_code); + } + return true; + } + return false; + } + + if (k->is_alt_pressed()) { + if (!alt_start) { + if (k->get_keycode() == Key::KP_ADD) { + alt_start = true; + alt_code = 0; + return true; + } + } else { + if (k->get_keycode() >= Key::KEY_0 && k->get_keycode() <= Key::KEY_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::KEY_0); + } + if (k->get_keycode() >= Key::KP_0 && k->get_keycode() <= Key::KP_9) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::KP_0); + } + if (k->get_keycode() >= Key::A && k->get_keycode() <= Key::F) { + alt_code = alt_code << 4; + alt_code += (uint32_t)(k->get_keycode() - Key::A) + 10; + } + return true; + } + } + } + return false; +} + void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { ERR_FAIL_COND(p_gui_input.is_null()); @@ -1571,7 +1681,7 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { set_caret_column(col); selection.drag_attempt = false; - if (mb->is_shift_pressed() && (caret.column != prev_col || caret.line != prev_line)) { + if (selecting_enabled && 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; @@ -1639,7 +1749,6 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { last_dblclk = OS::get_singleton()->get_ticks_msec(); last_dblclk_pos = mb->get_position(); } - update(); } @@ -1647,7 +1756,7 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { paste_primary_clipboard(); } - if (mb->get_button_index() == MouseButton::RIGHT && context_menu_enabled) { + if (mb->get_button_index() == MouseButton::RIGHT && (context_menu_enabled || is_move_caret_on_right_click_enabled())) { _reset_caret_blink_timer(); Point2i pos = get_line_column_at_pos(mpos); @@ -1672,11 +1781,13 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { } } - _generate_context_menu(); - menu->set_position(get_screen_position() + mpos); - menu->reset_size(); - menu->popup(); - grab_focus(); + if (context_menu_enabled) { + _generate_context_menu(); + menu->set_position(get_screen_position() + mpos); + menu->reset_size(); + menu->popup(); + grab_focus(); + } } } else { if (mb->get_button_index() == MouseButton::LEFT) { @@ -1795,6 +1906,10 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { Ref<InputEventKey> k = p_gui_input; if (k.is_valid()) { + if (alt_input(p_gui_input)) { + accept_event(); + return; + } if (!k->is_pressed()) { return; } @@ -1875,44 +1990,45 @@ void TextEdit::gui_input(const Ref<InputEvent> &p_gui_input) { return; } - // SELECT ALL, SELECT WORD UNDER CARET, CUT, COPY, PASTE. - - 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 (is_shortcut_keys_enabled()) { + // SELECT ALL, SELECT WORD UNDER CARET, CUT, COPY, PASTE. + 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; + } - // UNDO/REDO. - if (k->is_action("ui_undo", true)) { - undo(); - accept_event(); - return; - } - if (k->is_action("ui_redo", true)) { - redo(); - 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; + } } // MISC. @@ -2045,6 +2161,7 @@ void TextEdit::_new_line(bool p_split_current_line, bool p_above) { bool first_line = false; if (!p_split_current_line) { + deselect(); if (p_above) { if (caret.line > 0) { set_caret_line(caret.line - 1, false); @@ -2083,16 +2200,21 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { if (p_move_by_word) { int cc = caret.column; - + // 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 (cc == 0 && caret.line > 0) { set_caret_line(caret.line - 1); set_caret_column(text[caret.line].length()); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); - for (int i = words.size() - 2; i >= 0; i = i - 2) { - if (words[i] < cc) { - cc = words[i]; - break; + if (words.is_empty() || cc <= words[0]) { + // This solves the scenario where there are no words but glyfs that can be ignored. + cc = 0; + } else { + for (int i = words.size() - 2; i >= 0; i = i - 2) { + if (words[i] < cc) { + cc = words[i]; + break; + } } } set_caret_column(cc); @@ -2134,16 +2256,21 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { if (p_move_by_word) { int cc = caret.column; - + // If the caret is at the end of the line, and not on the last line, move it down to the beginning of the next line. if (cc == text[caret.line].length() && caret.line < text.size() - 1) { set_caret_line(caret.line + 1); set_caret_column(0); } else { PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); - for (int i = 1; i < words.size(); i = i + 2) { - if (words[i] > cc) { - cc = words[i]; - break; + if (words.is_empty() || cc >= words[words.size() - 1]) { + // This solves the scenario where there are no words but glyfs that can be ignored. + cc = text[caret.line].length(); + } else { + for (int i = 1; i < words.size(); i = i + 2) { + if (words[i] > cc) { + cc = words[i]; + break; + } } } set_caret_column(cc); @@ -2233,15 +2360,7 @@ void TextEdit::_move_caret_to_line_start(bool p_select) { } 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++; - } - + int current_line_whitespace_len = get_first_non_whitespace_column(caret.line); if (get_caret_column() == current_line_whitespace_len) { set_caret_column(0); } else { @@ -2314,37 +2433,47 @@ void TextEdit::_move_caret_page_down(bool p_select) { } void TextEdit::_do_backspace(bool p_word, bool p_all_to_left) { - if (!editable) { + if (!editable || (caret.column == 0 && caret.line == 0 && !has_selection())) { return; } - if (has_selection() || (!p_all_to_left && !p_word)) { + if (has_selection() || (!p_all_to_left && !p_word) || caret.column == 0) { backspace(); return; } if (p_all_to_left) { int caret_current_column = caret.column; - caret.column = 0; + set_caret_column(0); _remove_text(caret.line, 0, caret.line, caret_current_column); return; } if (p_word) { - int line = caret.line; int column = caret.column; - - PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(line)->get_rid()); - for (int i = words.size() - 2; i >= 0; i = i - 2) { - if (words[i] < column) { - column = words[i]; - break; + // Check for the case "<word><space><caret>" and ignore the space. + // No need to check for column being 0 since it is checked above. + if (is_whitespace(text[caret.line][caret.column - 1])) { + column -= 1; + } + // Get a list with the indices of the word bounds of the given text line. + const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); + if (words.is_empty() || column <= words[0]) { + // If "words" is empty, meaning no words are left, we can remove everything until the beginning of the line. + column = 0; + } else { + // Otherwise search for the first word break that is smaller than the index from which we're currently deleting. + for (int i = words.size() - 2; i >= 0; i = i - 2) { + if (words[i] < column) { + column = words[i]; + break; + } } } - _remove_text(line, column, caret.line, caret.column); + _remove_text(caret.line, column, caret.line, caret.column); - set_caret_line(line, false); + set_caret_line(caret.line, false); set_caret_column(column); return; } @@ -2369,6 +2498,10 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { int next_column; if (p_all_to_right) { + if (caret.column == curline_len) { + return; + } + // Delete everything to right of caret next_column = curline_len; next_line = caret.line; @@ -2430,6 +2563,47 @@ void TextEdit::_move_caret_document_end(bool p_select) { } } +void TextEdit::_update_placeholder() { + if (font.is_null() || font_size <= 0) { + return; // Not in tree? + } + + // Placeholder is generally smaller then text documents, and updates less so this should be fast enough for now. + placeholder_data_buf->clear(); + placeholder_data_buf->set_width(text.get_width()); + placeholder_data_buf->set_direction((TextServer::Direction)text_direction); + placeholder_data_buf->set_preserve_control(draw_control_chars); + placeholder_data_buf->add_string(placeholder_text, font, font_size, opentype_features, language); + + placeholder_bidi_override = structured_text_parser(st_parser, st_args, placeholder_text); + if (placeholder_bidi_override.is_empty()) { + TS->shaped_text_set_bidi_override(placeholder_data_buf->get_rid(), placeholder_bidi_override); + } + + if (get_tab_size() > 0) { + Vector<float> tabs; + tabs.push_back(font->get_char_size(' ', 0, font_size).width * get_tab_size()); + placeholder_data_buf->tab_align(tabs); + } + + // Update height. + const int wrap_amount = placeholder_data_buf->get_line_count() - 1; + placeholder_line_height = font->get_height(font_size); + for (int i = 0; i <= wrap_amount; i++) { + placeholder_line_height = MAX(placeholder_line_height, placeholder_data_buf->get_line_size(i).y); + } + + // Update width. + placeholder_max_width = placeholder_data_buf->get_size().x; + + // Update wrapped rows. + placeholder_wraped_rows.clear(); + for (int i = 0; i <= wrap_amount; i++) { + Vector2i line_range = placeholder_data_buf->get_line_range(i); + placeholder_wraped_rows.push_back(placeholder_text.substr(line_range.x, line_range.y - line_range.x)); + } +} + void TextEdit::_update_caches() { /* Internal API for CodeEdit. */ brace_mismatch_color = get_theme_color(SNAME("brace_mismatch_color"), SNAME("CodeEdit")); @@ -2460,6 +2634,7 @@ void TextEdit::_update_caches() { 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")); + font_placeholder_color = get_theme_color(SNAME("font_placeholder_color")); outline_size = get_theme_constant(SNAME("outline_size")); outline_color = get_theme_color(SNAME("font_outline_color")); @@ -2482,7 +2657,8 @@ void TextEdit::_update_caches() { text.set_draw_control_chars(draw_control_chars); text.set_font(font); text.set_font_size(font_size); - text.invalidate_all(); + text.invalidate_font(); + _update_placeholder(); /* Syntax highlighting. */ if (syntax_highlighter.is_valid()) { @@ -2598,8 +2774,7 @@ Control::CursorShape TextEdit::get_cursor_shape(const Point2 &p_pos) const { } String TextEdit::get_tooltip(const Point2 &p_pos) const { - Object *tooltip_obj = ObjectDB::get_instance(tooltip_obj_id); - if (!tooltip_obj) { + if (!tooltip_callback.is_valid()) { return Control::get_tooltip(p_pos); } Point2i pos = get_line_column_at_pos(p_pos); @@ -2612,19 +2787,20 @@ String TextEdit::get_tooltip(const Point2 &p_pos) const { } int beg, end; if (select_word(s, col, beg, end)) { - String tt = tooltip_obj->call(tooltip_func, s.substr(beg, end - beg), tooltip_ud); - - return tt; + Variant args[1] = { s.substr(beg, end - beg) }; + const Variant *argp[] = { &args[0] }; + Callable::CallError ce; + Variant ret; + tooltip_callback.call(argp, 1, ret, ce); + ERR_FAIL_COND_V_MSG(ce.error != Callable::CallError::CALL_OK, "", "Failed to call custom tooltip."); + return ret; } return Control::get_tooltip(p_pos); } -void TextEdit::set_tooltip_request_func(Object *p_obj, const StringName &p_function, const Variant &p_udata) { - ERR_FAIL_NULL(p_obj); - tooltip_obj_id = p_obj->get_instance_id(); - tooltip_func = p_function; - tooltip_ud = p_udata; +void TextEdit::set_tooltip_request_func(const Callable &p_tooltip_callback) { + tooltip_callback = p_tooltip_callback; } /* Text */ @@ -2661,7 +2837,8 @@ void TextEdit::set_text_direction(Control::TextDirection p_text_direction) { dir = (TextServer::Direction)text_direction; } text.set_direction_and_language(dir, (!language.is_empty()) ? language : TranslationServer::get_singleton()->get_tool_locale()); - text.invalidate_all(); + text.invalidate_font(); + _update_placeholder(); if (menu_dir) { menu_dir->set_item_checked(menu_dir->get_item_index(MENU_DIR_INHERITED), text_direction == TEXT_DIRECTION_INHERITED); @@ -2682,7 +2859,8 @@ void TextEdit::set_opentype_feature(const String &p_name, int p_value) { 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(); + text.invalidate_font(); + _update_placeholder(); update(); } } @@ -2698,7 +2876,8 @@ int TextEdit::get_opentype_feature(const String &p_name) const { void TextEdit::clear_opentype_features() { opentype_features.clear(); text.set_font_features(opentype_features); - text.invalidate_all(); + text.invalidate_font(); + _update_placeholder(); update(); } @@ -2713,6 +2892,7 @@ void TextEdit::set_language(const String &p_language) { } text.set_direction_and_language(dir, (!language.is_empty()) ? language : TranslationServer::get_singleton()->get_tool_locale()); text.invalidate_all(); + _update_placeholder(); update(); } } @@ -2721,7 +2901,7 @@ String TextEdit::get_language() const { return language; } -void TextEdit::set_structured_text_bidi_override(Control::StructuredTextParser p_parser) { +void TextEdit::set_structured_text_bidi_override(TextServer::StructuredTextParser p_parser) { if (st_parser != p_parser) { st_parser = p_parser; for (int i = 0; i < text.size(); i++) { @@ -2731,7 +2911,7 @@ void TextEdit::set_structured_text_bidi_override(Control::StructuredTextParser p } } -Control::StructuredTextParser TextEdit::get_structured_text_bidi_override() const { +TextServer::StructuredTextParser TextEdit::get_structured_text_bidi_override() const { return st_parser; } @@ -2754,6 +2934,7 @@ void TextEdit::set_tab_size(const int p_size) { } text.set_tab_size(p_size); text.invalidate_all_lines(); + _update_placeholder(); update(); } @@ -2812,15 +2993,31 @@ void TextEdit::clear() { } void TextEdit::_clear() { + if (editable && undo_enabled) { + _move_caret_document_start(false); + 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(""); + text.clear(); + + end_complex_operation(); + return; + } + // Cannot merge with above, as we are not part of the tree on creation. + int old_text_size = text.size(); + clear_undo_history(); text.clear(); - caret.column = 0; - caret.line = 0; + set_caret_line(0, false); + set_caret_column(0); caret.x_ofs = 0; caret.line_ofs = 0; caret.wrap_ofs = 0; caret.last_fit_x = 0; selection.active = false; + + emit_signal(SNAME("lines_edited_from"), old_text_size, 0); } void TextEdit::set_text(const String &p_text) { @@ -2865,18 +3062,30 @@ int TextEdit::get_line_count() const { return text.size(); } +void TextEdit::set_placeholder(const String &p_text) { + placeholder_text = p_text; + _update_placeholder(); + update(); +} + +String TextEdit::get_placeholder() const { + return placeholder_text; +} + void TextEdit::set_line(int p_line, const String &p_new_text) { if (p_line < 0 || p_line >= text.size()) { return; } + begin_complex_operation(); _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 (caret.line == p_line && caret.column > p_new_text.length()) { + set_caret_column(p_new_text.length(), false); } if (has_selection() && p_line == selection.to_line && selection.to_column > text[p_line].length()) { selection.to_column = text[p_line].length(); } + end_complex_operation(); } String TextEdit::get_line(int p_line) const { @@ -2919,7 +3128,7 @@ int TextEdit::get_first_non_whitespace_column(int p_line) const { ERR_FAIL_INDEX_V(p_line, text.size(), 0); int col = 0; - while (col < text[p_line].length() && _is_whitespace(text[p_line][col])) { + while (col < text[p_line].length() && is_whitespace(text[p_line][col])) { col++; } return col; @@ -2931,8 +3140,10 @@ void TextEdit::swap_lines(int p_from_line, int p_to_line) { String tmp = get_line(p_from_line); String tmp2 = get_line(p_to_line); + begin_complex_operation(); set_line(p_to_line, tmp); set_line(p_from_line, tmp2); + end_complex_operation(); } void TextEdit::insert_line_at(int p_at, const String &p_text) { @@ -2941,7 +3152,7 @@ void TextEdit::insert_line_at(int p_at, const String &p_text) { _insert_text(p_at, 0, p_text + "\n"); if (caret.line >= p_at) { // offset caret when located after inserted line - ++caret.line; + set_caret_line(caret.line + 1, false); } if (has_selection()) { if (selection.from_line >= p_at) { @@ -2953,6 +3164,7 @@ void TextEdit::insert_line_at(int p_at, const String &p_text) { ++selection.to_line; } } + update(); } void TextEdit::insert_text_at_caret(const String &p_text) { @@ -3333,9 +3545,6 @@ void TextEdit::undo() { 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); - } current_op.version = op.prev_version; if (undo_stack_pos->get().chain_backward) { @@ -3351,6 +3560,10 @@ void TextEdit::undo() { } } + 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); + } + _update_scrollbars(); if (undo_stack_pos->get().type == TextOperation::TYPE_REMOVE) { set_caret_line(undo_stack_pos->get().to_line, false); @@ -3504,9 +3717,9 @@ Point2i TextEdit::search(const String &p_key, uint32_t p_search_flags, int p_fro if (pos != -1 && (p_search_flags & SEARCH_WHOLE_WORDS)) { // Validate for whole words. - if (pos > 0 && _is_text_char(text_line[pos - 1])) { + if (pos > 0 && !is_symbol(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()])) { + } else if (pos + p_key.length() < text_line.length() && !is_symbol(text_line[pos + p_key.length()])) { is_match = false; } } @@ -3647,7 +3860,7 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ Point2i TextEdit::get_pos_at_line_column(int p_line, int p_column) const { Rect2i rect = get_rect_at_line_column(p_line, p_column); - return rect.position + Vector2i(0, get_line_height()); + return rect.position.x == -1 ? rect.position : rect.position + Vector2i(0, get_line_height()); } Rect2i TextEdit::get_rect_at_line_column(int p_line, int p_column) const { @@ -3846,6 +4059,7 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ } } } + bool caret_moved = caret.line != p_line; caret.line = p_line; int n_col = _get_char_pos_for_line(caret.last_fit_x, p_line, p_wrap_index); @@ -3859,15 +4073,16 @@ void TextEdit::set_caret_line(int p_line, bool p_adjust_viewport, bool p_can_be_ n_col -= 1; } } + caret_moved = (caret_moved || caret.column != n_col); caret.column = n_col; - if (p_adjust_viewport) { + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(); } setting_caret_line = false; - if (!caret_pos_dirty) { + if (caret_moved && !caret_pos_dirty) { if (is_inside_tree()) { MessageQueue::get_singleton()->push_call(this, "_emit_caret_changed"); } @@ -3883,19 +4098,20 @@ void TextEdit::set_caret_column(int p_col, bool p_adjust_viewport) { if (p_col < 0) { p_col = 0; } + if (p_col > get_line(caret.line).length()) { + p_col = get_line(caret.line).length(); + } + bool caret_moved = caret.column != p_col; caret.column = p_col; - if (caret.column > get_line(caret.line).length()) { - caret.column = get_line(caret.line).length(); - } caret.last_fit_x = _get_column_x_offset_for_line(caret.column, caret.line); - if (p_adjust_viewport) { + if (is_inside_tree() && p_adjust_viewport) { adjust_viewport_to_caret(); } - if (!caret_pos_dirty) { + if (caret_moved && !caret_pos_dirty) { if (is_inside_tree()) { MessageQueue::get_singleton()->push_call(this, "_emit_caret_changed"); } @@ -4016,13 +4232,18 @@ void TextEdit::select_word_under_caret() { int end = 0; const PackedInt32Array words = TS->shaped_text_get_word_breaks(text.get_line_data(caret.line)->get_rid()); for (int i = 0; i < words.size(); i = i + 2) { - if ((words[i] < caret.column && words[i + 1] > caret.column) || (i == words.size() - 2 && caret.column == words[i + 1])) { + if ((words[i] <= caret.column && words[i + 1] >= caret.column) || (i == words.size() - 2 && caret.column == words[i + 1])) { begin = words[i]; end = words[i + 1]; break; } } + // No word found. + if (begin == 0 && end == 0) { + return; + } + select(caret.line, begin, caret.line, end); /* Move the caret to the end of the word for easier editing. */ set_caret_column(end, false); @@ -4098,10 +4319,12 @@ String TextEdit::get_selected_text() const { } int TextEdit::get_selection_line() const { + ERR_FAIL_COND_V(!selection.active, -1); return selection.selecting_line; } int TextEdit::get_selection_column() const { + ERR_FAIL_COND_V(!selection.active, -1); return selection.selecting_column; } @@ -4259,6 +4482,8 @@ int TextEdit::get_h_scroll() const { } void TextEdit::set_v_scroll_speed(float p_speed) { + // Prevent setting a vertical scroll speed value under 1.0 + ERR_FAIL_COND(p_speed < 1.0); v_scroll_speed = p_speed; } @@ -4301,9 +4526,13 @@ void TextEdit::set_line_as_center_visible(int p_line, int p_wrap_index) { ERR_FAIL_COND(p_wrap_index > get_line_wrap_count(p_line)); int visible_rows = get_visible_line_count(); - Point2i next_line = get_next_visible_line_index_offset_from(p_line, p_wrap_index, -visible_rows / 2); + Point2i next_line = get_next_visible_line_index_offset_from(p_line, p_wrap_index, (-visible_rows / 2) - 1); int first_line = p_line - next_line.x + 1; + if (first_line < 0) { + set_v_scroll(0); + return; + } set_v_scroll(get_scroll_pos_for_line(first_line, next_line.y)); } @@ -4315,6 +4544,12 @@ void TextEdit::set_line_as_last_visible(int p_line, int p_wrap_index) { 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; + // Adding _get_visible_lines_offset is not 100% correct as we end up showing almost p_line + 1, however, it provides a + // better user experience. Therefore we need to special case < visible line count, else showing line 0 is impossible. + if (get_visible_line_count_in_range(0, p_line) < get_visible_line_count() + 1) { + set_v_scroll(0); + return; + } set_v_scroll(get_scroll_pos_for_line(first_line, next_line.y) + _get_visible_lines_offset()); } @@ -4338,7 +4573,11 @@ int TextEdit::get_visible_line_count() const { int TextEdit::get_visible_line_count_in_range(int p_from_line, int p_to_line) const { ERR_FAIL_INDEX_V(p_from_line, text.size(), 0); ERR_FAIL_INDEX_V(p_to_line, text.size(), 0); - ERR_FAIL_COND_V(p_from_line > p_to_line, 0); + + // So we can handle inputs in whatever order + if (p_from_line > p_to_line) { + SWAP(p_from_line, p_to_line); + } /* Returns the total number of (lines + wrapped - hidden). */ if (!_is_hiding_enabled() && get_line_wrapping_mode() == LineWrappingMode::LINE_WRAPPING_NONE) { @@ -4517,9 +4756,7 @@ void TextEdit::add_gutter(int p_at) { gutters.insert(p_at, GutterInfo()); } - for (int i = 0; i < text.size() + 1; i++) { - text.add_gutter(p_at); - } + text.add_gutter(p_at); emit_signal(SNAME("gutter_added")); update(); } @@ -4529,9 +4766,7 @@ void TextEdit::remove_gutter(int p_gutter) { gutters.remove_at(p_gutter); - for (int i = 0; i < text.size() + 1; i++) { - text.remove_gutter(p_gutter); - } + text.remove_gutter(p_gutter); emit_signal(SNAME("gutter_removed")); update(); } @@ -4647,12 +4882,10 @@ void TextEdit::merge_gutters(int p_from_line, int p_to_line) { update(); } -void TextEdit::set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback) { +void TextEdit::set_gutter_custom_draw(int p_gutter, const Callable &p_draw_callback) { ERR_FAIL_INDEX(p_gutter, gutters.size()); - ERR_FAIL_NULL(p_object); - gutters.write[p_gutter].custom_draw_obj = p_object->get_instance_id(); - gutters.write[p_gutter].custom_draw_callback = p_callback; + gutters.write[p_gutter].custom_draw_callback = p_draw_callback; update(); } @@ -4771,7 +5004,8 @@ void TextEdit::set_draw_control_chars(bool p_enabled) { menu->set_item_checked(menu->get_item_index(MENU_DISPLAY_UCC), draw_control_chars); } text.set_draw_control_chars(draw_control_chars); - text.invalidate_all(); + text.invalidate_font(); + _update_placeholder(); update(); } } @@ -4851,6 +5085,9 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_text"), &TextEdit::get_text); ClassDB::bind_method(D_METHOD("get_line_count"), &TextEdit::get_line_count); + ClassDB::bind_method(D_METHOD("set_placeholder", "text"), &TextEdit::set_placeholder); + ClassDB::bind_method(D_METHOD("get_placeholder"), &TextEdit::get_placeholder); + ClassDB::bind_method(D_METHOD("set_line", "line", "new_text"), &TextEdit::set_line); ClassDB::bind_method(D_METHOD("get_line", "line"), &TextEdit::get_line); @@ -4942,7 +5179,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("search", "text", "flags", "from_line", "from_colum"), &TextEdit::search); /* Tooltip */ - ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "object", "callback", "data"), &TextEdit::set_tooltip_request_func); + ClassDB::bind_method(D_METHOD("set_tooltip_request_func", "callback"), &TextEdit::set_tooltip_request_func); /* Mouse */ ClassDB::bind_method(D_METHOD("get_local_mouse_pos"), &TextEdit::get_local_mouse_pos); @@ -5048,7 +5285,7 @@ void TextEdit::_bind_methods() { ClassDB::bind_method(D_METHOD("get_line_wrapped_text", "line"), &TextEdit::get_line_wrapped_text); /* Viewport. */ - // Scolling. + // Scrolling. 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); @@ -5114,7 +5351,7 @@ void TextEdit::_bind_methods() { 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("set_gutter_custom_draw", "column", "draw_callback"), &TextEdit::set_gutter_custom_draw); ClassDB::bind_method(D_METHOD("get_total_gutter_width"), &TextEdit::get_total_gutter_width); // Line gutters. @@ -5159,8 +5396,9 @@ void TextEdit::_bind_methods() { /* Inspector */ ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT), "set_text", "get_text"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "placeholder_text", PROPERTY_HINT_MULTILINE_TEXT), "set_placeholder", "get_placeholder"); 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::STRING, "language", PROPERTY_HINT_LOCALE_ID, ""), "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"); @@ -5228,19 +5466,21 @@ bool TextEdit::_set(const StringName &p_name, const Variant &p_value) { if (str.begins_with("opentype_features/")) { String name = str.get_slicec('/', 1); int32_t tag = TS->name_to_tag(name); - double value = p_value; + int value = p_value; if (value == -1) { if (opentype_features.has(tag)) { opentype_features.erase(tag); text.set_font_features(opentype_features); - text.invalidate_all(); + text.invalidate_font(); + _update_placeholder(); update(); } } else { - if ((double)opentype_features[tag] != value) { + if (!opentype_features.has(tag) || (int)opentype_features[tag] != value) { opentype_features[tag] = value; text.set_font_features(opentype_features); - text.invalidate_all(); + text.invalidate_font(); + _update_placeholder(); update(); } } @@ -5270,7 +5510,7 @@ bool TextEdit::_get(const StringName &p_name, Variant &r_ret) const { 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::INT, "opentype_features/" + name)); } p_list->push_back(PropertyInfo(Variant::NIL, "opentype_features/_new", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_EDITOR)); } @@ -5410,7 +5650,7 @@ void TextEdit::_cut_internal() { set_caret_line(get_caret_line() + 1); } - // Correct the visualy perceived caret column taking care of identation level of the lines. + // Correct the visually perceived caret column taking care of indentation level of the lines. int diff_indent = indent_level - get_indent_level(get_caret_line()); cc += diff_indent; if (diff_indent != 0) { @@ -5528,8 +5768,10 @@ void TextEdit::_generate_context_menu() { if (editable) { menu->add_item(RTR("Paste"), MENU_PASTE, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_paste") : Key::NONE); } - menu->add_separator(); - if (is_selecting_enabled()) { + if (selecting_enabled || editable) { + menu->add_separator(); + } + if (selecting_enabled) { menu->add_item(RTR("Select All"), MENU_SELECT_ALL, is_shortcut_keys_enabled() ? _get_menu_action_accelerator("ui_text_select_all") : Key::NONE); } if (editable) { @@ -5656,9 +5898,9 @@ int TextEdit::_get_column_pos_of_word(const String &p_key, const String &p_searc if (col != -1 && p_search_flags & SEARCH_WHOLE_WORDS) { p_from_column = col; - if (col > 0 && _is_text_char(p_search[col - 1])) { + if (col > 0 && !is_symbol(p_search[col - 1])) { col = -1; - } else if ((col + p_key.length()) < p_search.length() && _is_text_char(p_search[col + p_key.length()])) { + } else if ((col + p_key.length()) < p_search.length() && !is_symbol(p_search[col + p_key.length()])) { col = -1; } } @@ -5713,7 +5955,7 @@ int TextEdit::_get_column_x_offset_for_line(int p_char, int p_line) const { 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)) { + if ((p_char >= rows2[i].x) && (p_char <= rows2[i].y)) { row = i; break; } @@ -5797,8 +6039,8 @@ void TextEdit::_update_selection_mode_word() { 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); + set_caret_line(line, false); + set_caret_column(end); } else { if ((col <= selection.selected_word_origin && line == selection.selecting_line) || line < selection.selecting_line) { selection.selecting_column = selection.selected_word_end; @@ -5854,6 +6096,10 @@ void TextEdit::_update_selection_mode_line() { } void TextEdit::_pre_shift_selection() { + if (!selecting_enabled) { + return; + } + if (!selection.active || selection.selecting_mode == SelectionMode::SELECTION_MODE_NONE) { selection.selecting_line = caret.line; selection.selecting_column = caret.column; @@ -5864,6 +6110,10 @@ void TextEdit::_pre_shift_selection() { } void TextEdit::_post_shift_selection() { + if (!selecting_enabled) { + return; + } + if (selection.active && selection.selecting_mode == SelectionMode::SELECTION_MODE_SHIFT) { select(selection.selecting_line, selection.selecting_column, caret.line, caret.column); update(); @@ -5892,6 +6142,7 @@ void TextEdit::_update_wrap_at_column(bool p_force) { text.set_width(-1); } text.invalidate_all_lines(); + _update_placeholder(); } _update_caret_wrap_offset(); @@ -5919,14 +6170,16 @@ void TextEdit::_update_scrollbars() { h_scroll->set_begin(Point2(0, size.height - hmin.height)); h_scroll->set_end(Point2(size.width - vmin.width, size.height)); + bool draw_placeholder = text.size() == 1 && text[0].length() == 0; + int visible_rows = get_visible_line_count(); - int total_rows = get_total_visible_line_count(); + int total_rows = draw_placeholder ? placeholder_wraped_rows.size() - 1 : get_total_visible_line_count(); if (scroll_past_end_of_file_enabled) { total_rows += visible_rows - 1; } int visible_width = size.width - style_normal->get_minimum_size().width; - int total_width = text.get_max_width() + vmin.x + gutters_width + gutter_padding; + int total_width = (draw_placeholder ? placeholder_max_width : text.get_max_width()) + vmin.x + gutters_width + gutter_padding; if (draw_minimap) { total_width += minimap_width; @@ -5949,6 +6202,7 @@ void TextEdit::_update_scrollbars() { caret.line_ofs = 0; caret.wrap_ofs = 0; v_scroll->set_value(0); + v_scroll->set_max(0); v_scroll->hide(); } @@ -5966,6 +6220,7 @@ void TextEdit::_update_scrollbars() { } else { caret.x_ofs = 0; h_scroll->set_value(0); + h_scroll->set_max(0); h_scroll->hide(); } @@ -5996,20 +6251,22 @@ void TextEdit::_scroll_moved(double p_to_val) { } if (v_scroll->is_visible_in_tree()) { // Set line ofs and wrap ofs. + bool draw_placeholder = text.size() == 1 && text[0].length() == 0; + 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); + sc += draw_placeholder ? placeholder_wraped_rows.size() - 1 : 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 line_wrap_amount = draw_placeholder ? placeholder_wraped_rows.size() - 1 : get_line_wrap_count(n_line); int wi = line_wrap_amount - (sc - v_scroll_i - 1); wi = CLAMP(wi, 0, line_wrap_amount); @@ -6436,11 +6693,14 @@ void TextEdit::_base_remove_text(int p_from_line, int p_from_column, int p_to_li emit_signal(SNAME("lines_edited_from"), p_to_line, p_from_line); } -TextEdit::TextEdit() { +TextEdit::TextEdit(const String &p_placeholder) { + placeholder_data_buf.instantiate(); + clear(); set_focus_mode(FOCUS_ALL); _update_caches(); set_default_cursor_shape(CURSOR_IBEAM); + set_process_unhandled_key_input(true); text.set_tab_size(text.get_tab_size()); @@ -6476,5 +6736,7 @@ TextEdit::TextEdit() { undo_stack_max_size = GLOBAL_GET("gui/common/text_edit_undo_stack_max_size"); + set_placeholder(p_placeholder); + set_editable(true); } diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index df3edca943..993203bee6 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -70,7 +70,7 @@ public: GUTTER_TYPE_CUSTOM }; - /* Contex Menu. */ + /* Context Menu. */ enum MenuItems { MENU_CUT, MENU_COPY, @@ -120,8 +120,7 @@ private: bool clickable = false; bool overwritable = false; - ObjectID custom_draw_obj = ObjectID(); - StringName custom_draw_callback; + Callable custom_draw_callback; }; class Text { @@ -190,6 +189,7 @@ private: int get_max_width() const; void set_width(float p_width); + float get_width() const; int get_line_wrap_amount(int p_line) const; Vector<Vector2i> get_line_wrap_ranges(int p_line) const; @@ -210,7 +210,8 @@ private: int size() const { return text.size(); } void clear(); - void invalidate_cache(int p_line, int p_column = -1, const String &p_ime_text = String(), const Array &p_bidi_override = Array()); + void invalidate_cache(int p_line, int p_column = -1, bool p_text_changed = false, const String &p_ime_text = String(), const Array &p_bidi_override = Array()); + void invalidate_font(); void invalidate_all(); void invalidate_all_lines(); @@ -246,10 +247,24 @@ private: bool setting_text = false; + bool alt_start = false; + uint32_t alt_code = 0; + // Text properties. String ime_text = ""; Point2 ime_selection; + // Placeholder + String placeholder_text = ""; + Array placeholder_bidi_override; + Ref<TextParagraph> placeholder_data_buf; + int placeholder_line_height = -1; + int placeholder_max_width = -1; + + Vector<String> placeholder_wraped_rows; + + void _update_placeholder(); + /* Initialise to opposite first, so we get past the early-out in set_editable. */ bool editable = false; @@ -259,7 +274,7 @@ private: Dictionary opentype_features; String language = ""; - Control::StructuredTextParser st_parser = STRUCTURED_TEXT_DEFAULT; + TextServer::StructuredTextParser st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; Array st_args; void _clear(); @@ -313,7 +328,7 @@ private: List<TextOperation> undo_stack; List<TextOperation>::Element *undo_stack_pos = nullptr; - Timer *idle_detect; + Timer *idle_detect = nullptr; uint32_t version = 0; uint32_t saved_version = 0; @@ -332,9 +347,7 @@ private: int _get_column_pos_of_word(const String &p_key, const String &p_search, uint32_t p_search_flags, int p_from_column) const; /* Tooltip. */ - ObjectID tooltip_obj_id; - StringName tooltip_func; - Variant tooltip_ud; + Callable tooltip_callback; /* Mouse */ struct LineDrawingCache { @@ -343,7 +356,7 @@ private: Vector<int> last_visible_chars; }; - Map<int, LineDrawingCache> line_drawing_cache; + HashMap<int, LineDrawingCache> line_drawing_cache; int _get_char_pos_for_line(int p_px, int p_line, int p_wrap_index = 0) const; @@ -370,11 +383,11 @@ private: bool draw_caret = true; bool caret_blink_enabled = false; - Timer *caret_blink_timer; + Timer *caret_blink_timer = nullptr; bool move_caret_on_right_click = true; - bool caret_mid_grapheme_enabled = false; + bool caret_mid_grapheme_enabled = true; bool drag_action = false; bool drag_caret_force_displayed = false; @@ -416,7 +429,7 @@ private: bool dragging_selection = false; - Timer *click_select_held; + Timer *click_select_held = nullptr; uint64_t last_dblclk = 0; Vector2 last_dblclk_pos; void _click_selection_held(); @@ -439,8 +452,8 @@ private: void _update_caret_wrap_offset(); /* Viewport. */ - HScrollBar *h_scroll; - VScrollBar *v_scroll; + HScrollBar *h_scroll = nullptr; + VScrollBar *v_scroll = nullptr; bool scroll_past_end_of_file_enabled = false; @@ -498,7 +511,6 @@ private: /* Syntax highlighting. */ Ref<SyntaxHighlighter> syntax_highlighter; - Map<int, Dictionary> syntax_highlighting_cache; Dictionary _get_line_syntax_highlighting(int p_line); @@ -514,6 +526,7 @@ private: int font_size = 16; Color font_color = Color(1, 1, 1); Color font_readonly_color = Color(1, 1, 1); + Color font_placeholder_color = Color(1, 1, 1, 0.6); int outline_size = 0; Color outline_color = Color(1, 1, 1); @@ -612,7 +625,9 @@ protected: public: /* General overrides. */ + virtual void unhandled_key_input(const Ref<InputEvent> &p_event) override; virtual void gui_input(const Ref<InputEvent> &p_gui_input) override; + bool alt_input(const Ref<InputEvent> &p_gui_input); 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; @@ -620,7 +635,7 @@ public: virtual bool can_drop_data(const Point2 &p_point, const Variant &p_data) const override; virtual void drop_data(const Point2 &p_point, const Variant &p_data) 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_tooltip_request_func(const Callable &p_tooltip_callback); /* Text */ // Text properties. @@ -639,8 +654,8 @@ public: 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(TextServer::StructuredTextParser p_parser); + TextServer::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; @@ -670,6 +685,9 @@ public: String get_text() const; int get_line_count() const; + void set_placeholder(const String &p_text); + String get_placeholder() const; + void set_line(int p_line, const String &p_new_text); String get_line(int p_line) const; @@ -884,7 +902,7 @@ public: void merge_gutters(int p_from_line, int p_to_line); - void set_gutter_custom_draw(int p_gutter, Object *p_object, const StringName &p_callback); + void set_gutter_custom_draw(int p_gutter, const Callable &p_draw_callback); // Line gutters. void set_line_gutter_metadata(int p_line, int p_gutter, const Variant &p_metadata); @@ -926,7 +944,7 @@ public: void set_draw_spaces(bool p_enabled); bool is_drawing_spaces() const; - TextEdit(); + TextEdit(const String &p_placeholder = String()); }; VARIANT_ENUM_CAST(TextEdit::CaretType); diff --git a/scene/gui/texture_button.cpp b/scene/gui/texture_button.cpp index 3f0d907a7e..26acfaaa70 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,7 +37,7 @@ Size2 TextureButton::get_minimum_size() const { Size2 rscale = Control::get_minimum_size(); - if (!expand) { + if (!ignore_texture_size) { if (normal.is_null()) { if (pressed.is_null()) { if (hover.is_null()) { @@ -170,55 +170,60 @@ void TextureButton::_notification(int p_what) { Point2 ofs; Size2 size; + bool draw_focus = (has_focus() && focused.is_valid()); + + // If no other texture is valid, try using focused texture. + bool draw_focus_only = draw_focus && !texdraw.is_valid(); + if (draw_focus_only) { + texdraw = focused; + } if (texdraw.is_valid()) { size = texdraw->get_size(); _texture_region = Rect2(Point2(), texdraw->get_size()); _tile = false; - if (expand) { - switch (stretch_mode) { - case STRETCH_KEEP: - size = texdraw->get_size(); - break; - case STRETCH_SCALE: - size = get_size(); - break; - case STRETCH_TILE: - size = get_size(); - _tile = true; - break; - case STRETCH_KEEP_CENTERED: - ofs = (get_size() - texdraw->get_size()) / 2; - size = texdraw->get_size(); - break; - case STRETCH_KEEP_ASPECT_CENTERED: - case STRETCH_KEEP_ASPECT: { - Size2 _size = get_size(); - float tex_width = texdraw->get_width() * _size.height / texdraw->get_height(); - float tex_height = _size.height; - - if (tex_width > _size.width) { - tex_width = _size.width; - tex_height = texdraw->get_height() * tex_width / texdraw->get_width(); - } + switch (stretch_mode) { + case STRETCH_KEEP: + size = texdraw->get_size(); + break; + case STRETCH_SCALE: + size = get_size(); + break; + case STRETCH_TILE: + size = get_size(); + _tile = true; + break; + case STRETCH_KEEP_CENTERED: + ofs = (get_size() - texdraw->get_size()) / 2; + size = texdraw->get_size(); + break; + case STRETCH_KEEP_ASPECT_CENTERED: + case STRETCH_KEEP_ASPECT: { + Size2 _size = get_size(); + float tex_width = texdraw->get_width() * _size.height / texdraw->get_height(); + float tex_height = _size.height; + + if (tex_width > _size.width) { + tex_width = _size.width; + tex_height = texdraw->get_height() * tex_width / texdraw->get_width(); + } - if (stretch_mode == STRETCH_KEEP_ASPECT_CENTERED) { - ofs.x = (_size.width - tex_width) / 2; - ofs.y = (_size.height - tex_height) / 2; - } - size.width = tex_width; - size.height = tex_height; - } break; - case STRETCH_KEEP_ASPECT_COVERED: { - size = get_size(); - Size2 tex_size = texdraw->get_size(); - Size2 scale_size(size.width / tex_size.width, size.height / tex_size.height); - float scale = scale_size.width > scale_size.height ? scale_size.width : scale_size.height; - Size2 scaled_tex_size = tex_size * scale; - Point2 ofs2 = ((scaled_tex_size - size) / scale).abs() / 2.0f; - _texture_region = Rect2(ofs2, size / scale); - } break; - } + if (stretch_mode == STRETCH_KEEP_ASPECT_CENTERED) { + ofs.x = (_size.width - tex_width) / 2; + ofs.y = (_size.height - tex_height) / 2; + } + size.width = tex_width; + size.height = tex_height; + } break; + case STRETCH_KEEP_ASPECT_COVERED: { + size = get_size(); + Size2 tex_size = texdraw->get_size(); + Size2 scale_size(size.width / tex_size.width, size.height / tex_size.height); + float scale = scale_size.width > scale_size.height ? scale_size.width : scale_size.height; + Size2 scaled_tex_size = tex_size * scale; + Point2 ofs2 = ((scaled_tex_size - size) / scale).abs() / 2.0f; + _texture_region = Rect2(ofs2, size / scale); + } break; } _position_rect = Rect2(ofs, size); @@ -226,7 +231,9 @@ void TextureButton::_notification(int p_what) { size.width *= hflip ? -1.0f : 1.0f; size.height *= vflip ? -1.0f : 1.0f; - if (_tile) { + if (draw_focus_only) { + // Do nothing, we only needed to calculate the rectangle. + } else if (_tile) { draw_texture_rect(texdraw, Rect2(ofs, size), _tile); } else { draw_texture_rect_region(texdraw, Rect2(ofs, size), _texture_region); @@ -235,7 +242,7 @@ void TextureButton::_notification(int p_what) { _position_rect = Rect2(); } - if (has_focus() && focused.is_valid()) { + if (draw_focus) { draw_texture_rect(focused, Rect2(ofs, size), false); }; } break; @@ -249,7 +256,7 @@ 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", "expand"), &TextureButton::set_expand); + ClassDB::bind_method(D_METHOD("set_ignore_texture_size", "ignore"), &TextureButton::set_ignore_texture_size); 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); @@ -262,7 +269,7 @@ void TextureButton::_bind_methods() { ClassDB::bind_method(D_METHOD("get_disabled_texture"), &TextureButton::get_disabled_texture); ClassDB::bind_method(D_METHOD("get_focused_texture"), &TextureButton::get_focused_texture); ClassDB::bind_method(D_METHOD("get_click_mask"), &TextureButton::get_click_mask); - ClassDB::bind_method(D_METHOD("get_expand"), &TextureButton::get_expand); + ClassDB::bind_method(D_METHOD("get_ignore_texture_size"), &TextureButton::get_ignore_texture_size); ClassDB::bind_method(D_METHOD("get_stretch_mode"), &TextureButton::get_stretch_mode); ADD_GROUP("Textures", "texture_"); @@ -272,7 +279,7 @@ void TextureButton::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_disabled", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_disabled_texture", "get_disabled_texture"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_focused", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_focused_texture", "get_focused_texture"); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture_click_mask", PROPERTY_HINT_RESOURCE_TYPE, "BitMap"), "set_click_mask", "get_click_mask"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_expand", "get_expand"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "ignore_texture_size", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_ignore_texture_size", "get_ignore_texture_size"); ADD_PROPERTY(PropertyInfo(Variant::INT, "stretch_mode", PROPERTY_HINT_ENUM, "Scale,Tile,Keep,Keep Centered,Keep Aspect,Keep Aspect Centered,Keep Aspect Covered"), "set_stretch_mode", "get_stretch_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_h", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_flip_h", "is_flipped_h"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_v", PROPERTY_HINT_RESOURCE_TYPE, "bool"), "set_flip_v", "is_flipped_v"); @@ -343,12 +350,12 @@ void TextureButton::set_focused_texture(const Ref<Texture2D> &p_focused) { focused = p_focused; }; -bool TextureButton::get_expand() const { - return expand; +bool TextureButton::get_ignore_texture_size() const { + return ignore_texture_size; } -void TextureButton::set_expand(bool p_expand) { - expand = p_expand; +void TextureButton::set_ignore_texture_size(bool p_ignore) { + ignore_texture_size = p_ignore; update_minimum_size(); update(); } diff --git a/scene/gui/texture_button.h b/scene/gui/texture_button.h index 8361f3c341..5762949acd 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,8 +54,8 @@ private: Ref<Texture2D> disabled; Ref<Texture2D> focused; Ref<BitMap> click_mask; - bool expand = false; - StretchMode stretch_mode = STRETCH_SCALE; + bool ignore_texture_size = false; + StretchMode stretch_mode = STRETCH_KEEP; Rect2 _texture_region; Rect2 _position_rect; @@ -85,8 +85,8 @@ public: Ref<Texture2D> get_focused_texture() const; Ref<BitMap> get_click_mask() const; - bool get_expand() const; - void set_expand(bool p_expand); + bool get_ignore_texture_size() const; + void set_ignore_texture_size(bool p_ignore); void set_stretch_mode(StretchMode p_stretch_mode); StretchMode get_stretch_mode() const; diff --git a/scene/gui/texture_progress_bar.cpp b/scene/gui/texture_progress_bar.cpp index 6a926a0364..081d065efe 100644 --- a/scene/gui/texture_progress_bar.cpp +++ b/scene/gui/texture_progress_bar.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -629,26 +629,30 @@ void TextureProgressBar::_bind_methods() { 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_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::BOOL, "nine_patch_stretch"), "set_nine_patch_stretch", "get_nine_patch_stretch"); + + ADD_GROUP("Stretch Margin", "stretch_margin_"); + 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); + 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::VECTOR2, "texture_progress_offset"), "set_texture_progress_offset", "get_texture_progress_offset"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "fill_mode", PROPERTY_HINT_ENUM, "Left to Right,Right to Left,Top to Bottom,Bottom to Top,Clockwise,Counter Clockwise,Bilinear (Left and Right),Bilinear (Top and Bottom),Clockwise and Counter Clockwise"), "set_fill_mode", "get_fill_mode"); + ADD_GROUP("Tint", "tint_"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_under"), "set_tint_under", "get_tint_under"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_over"), "set_tint_over", "get_tint_over"); ADD_PROPERTY(PropertyInfo(Variant::COLOR, "tint_progress"), "set_tint_progress", "get_tint_progress"); + ADD_GROUP("Radial Fill", "radial_"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radial_initial_angle", PROPERTY_HINT_RANGE, "0.0,360.0,0.1,slider"), "set_radial_initial_angle", "get_radial_initial_angle"); - ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radial_fill_degrees", PROPERTY_HINT_RANGE, "0.0,360.0,0.1,slider"), "set_fill_degrees", "get_fill_degrees"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radial_initial_angle", PROPERTY_HINT_RANGE, "0.0,360.0,0.1,slider,degrees"), "set_radial_initial_angle", "get_radial_initial_angle"); + ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "radial_fill_degrees", PROPERTY_HINT_RANGE, "0.0,360.0,0.1,slider,degrees"), "set_fill_degrees", "get_fill_degrees"); 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", 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); diff --git a/scene/gui/texture_progress_bar.h b/scene/gui/texture_progress_bar.h index c508f41387..4d3e38e006 100644 --- a/scene/gui/texture_progress_bar.h +++ b/scene/gui/texture_progress_bar.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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/texture_rect.cpp b/scene/gui/texture_rect.cpp index 85c15cdae7..ecdf55caf0 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,92 +29,92 @@ /*************************************************************************/ #include "texture_rect.h" + #include "core/core_string_names.h" #include "servers/rendering_server.h" void TextureRect::_notification(int p_what) { - if (p_what == NOTIFICATION_DRAW) { - if (texture.is_null()) { - return; - } - - Size2 size; - Point2 offset; - Rect2 region; - bool tile = false; - - switch (stretch_mode) { - case STRETCH_SCALE_ON_EXPAND: { - size = expand ? get_size() : texture->get_size(); - } break; - case STRETCH_SCALE: { - size = get_size(); - } break; - case STRETCH_TILE: { - size = get_size(); - tile = true; - } break; - case STRETCH_KEEP: { - size = texture->get_size(); - } break; - case STRETCH_KEEP_CENTERED: { - offset = (get_size() - texture->get_size()) / 2; - size = texture->get_size(); - } break; - case STRETCH_KEEP_ASPECT_CENTERED: - case STRETCH_KEEP_ASPECT: { - size = get_size(); - int tex_width = texture->get_width() * size.height / texture->get_height(); - int tex_height = size.height; - - if (tex_width > size.width) { - tex_width = size.width; - tex_height = texture->get_height() * tex_width / texture->get_width(); - } - - if (stretch_mode == STRETCH_KEEP_ASPECT_CENTERED) { - offset.x += (size.width - tex_width) / 2; - offset.y += (size.height - tex_height) / 2; - } - - size.width = tex_width; - size.height = tex_height; - } break; - case STRETCH_KEEP_ASPECT_COVERED: { - size = get_size(); - - Size2 tex_size = texture->get_size(); - Size2 scale_size(size.width / tex_size.width, size.height / tex_size.height); - float scale = scale_size.width > scale_size.height ? scale_size.width : scale_size.height; - Size2 scaled_tex_size = tex_size * scale; - - region.position = ((scaled_tex_size - size) / scale).abs() / 2.0f; - region.size = size / scale; - } break; - } - - Ref<AtlasTexture> p_atlas = texture; - - if (p_atlas.is_valid() && region.has_no_area()) { - Size2 scale_size(size.width / texture->get_width(), size.height / texture->get_height()); - - offset.width += hflip ? p_atlas->get_margin().get_position().width * scale_size.width * 2 : 0; - offset.height += vflip ? p_atlas->get_margin().get_position().height * scale_size.height * 2 : 0; - } - - size.width *= hflip ? -1.0f : 1.0f; - size.height *= vflip ? -1.0f : 1.0f; - - if (region.has_no_area()) { - draw_texture_rect(texture, Rect2(offset, size), tile); - } else { - draw_texture_rect_region(texture, Rect2(offset, size), region); - } + switch (p_what) { + case NOTIFICATION_DRAW: { + if (texture.is_null()) { + return; + } + + Size2 size; + Point2 offset; + Rect2 region; + bool tile = false; + + switch (stretch_mode) { + case STRETCH_SCALE: { + size = get_size(); + } break; + case STRETCH_TILE: { + size = get_size(); + tile = true; + } break; + case STRETCH_KEEP: { + size = texture->get_size(); + } break; + case STRETCH_KEEP_CENTERED: { + offset = (get_size() - texture->get_size()) / 2; + size = texture->get_size(); + } break; + case STRETCH_KEEP_ASPECT_CENTERED: + case STRETCH_KEEP_ASPECT: { + size = get_size(); + int tex_width = texture->get_width() * size.height / texture->get_height(); + int tex_height = size.height; + + if (tex_width > size.width) { + tex_width = size.width; + tex_height = texture->get_height() * tex_width / texture->get_width(); + } + + if (stretch_mode == STRETCH_KEEP_ASPECT_CENTERED) { + offset.x += (size.width - tex_width) / 2; + offset.y += (size.height - tex_height) / 2; + } + + size.width = tex_width; + size.height = tex_height; + } break; + case STRETCH_KEEP_ASPECT_COVERED: { + size = get_size(); + + Size2 tex_size = texture->get_size(); + Size2 scale_size(size.width / tex_size.width, size.height / tex_size.height); + float scale = scale_size.width > scale_size.height ? scale_size.width : scale_size.height; + Size2 scaled_tex_size = tex_size * scale; + + region.position = ((scaled_tex_size - size) / scale).abs() / 2.0f; + region.size = size / scale; + } break; + } + + Ref<AtlasTexture> p_atlas = texture; + + if (p_atlas.is_valid() && region.has_no_area()) { + Size2 scale_size(size.width / texture->get_width(), size.height / texture->get_height()); + + offset.width += hflip ? p_atlas->get_margin().get_position().width * scale_size.width * 2 : 0; + offset.height += vflip ? p_atlas->get_margin().get_position().height * scale_size.height * 2 : 0; + } + + size.width *= hflip ? -1.0f : 1.0f; + size.height *= vflip ? -1.0f : 1.0f; + + if (region.has_no_area()) { + draw_texture_rect(texture, Rect2(offset, size), tile); + } else { + draw_texture_rect_region(texture, Rect2(offset, size), region); + } + } break; } } Size2 TextureRect::get_minimum_size() const { - if (!expand && !texture.is_null()) { + if (!ignore_texture_size && !texture.is_null()) { return texture->get_size(); } else { return Size2(); @@ -124,8 +124,8 @@ Size2 TextureRect::get_minimum_size() const { void TextureRect::_bind_methods() { ClassDB::bind_method(D_METHOD("set_texture", "texture"), &TextureRect::set_texture); ClassDB::bind_method(D_METHOD("get_texture"), &TextureRect::get_texture); - ClassDB::bind_method(D_METHOD("set_expand", "enable"), &TextureRect::set_expand); - ClassDB::bind_method(D_METHOD("has_expand"), &TextureRect::has_expand); + ClassDB::bind_method(D_METHOD("set_ignore_texture_size", "ignore"), &TextureRect::set_ignore_texture_size); + ClassDB::bind_method(D_METHOD("get_ignore_texture_size"), &TextureRect::get_ignore_texture_size); ClassDB::bind_method(D_METHOD("set_flip_h", "enable"), &TextureRect::set_flip_h); ClassDB::bind_method(D_METHOD("is_flipped_h"), &TextureRect::is_flipped_h); ClassDB::bind_method(D_METHOD("set_flip_v", "enable"), &TextureRect::set_flip_v); @@ -134,12 +134,11 @@ void TextureRect::_bind_methods() { ClassDB::bind_method(D_METHOD("get_stretch_mode"), &TextureRect::get_stretch_mode); ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "texture", PROPERTY_HINT_RESOURCE_TYPE, "Texture2D"), "set_texture", "get_texture"); - ADD_PROPERTY(PropertyInfo(Variant::BOOL, "expand"), "set_expand", "has_expand"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "stretch_mode", PROPERTY_HINT_ENUM, "Scale On Expand (Compat),Scale,Tile,Keep,Keep Centered,Keep Aspect,Keep Aspect Centered,Keep Aspect Covered"), "set_stretch_mode", "get_stretch_mode"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "ignore_texture_size"), "set_ignore_texture_size", "get_ignore_texture_size"); + ADD_PROPERTY(PropertyInfo(Variant::INT, "stretch_mode", PROPERTY_HINT_ENUM, "Scale,Tile,Keep,Keep Centered,Keep Aspect,Keep Aspect Centered,Keep Aspect Covered"), "set_stretch_mode", "get_stretch_mode"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_h"), "set_flip_h", "is_flipped_h"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "flip_v"), "set_flip_v", "is_flipped_v"); - BIND_ENUM_CONSTANT(STRETCH_SCALE_ON_EXPAND); BIND_ENUM_CONSTANT(STRETCH_SCALE); BIND_ENUM_CONSTANT(STRETCH_TILE); BIND_ENUM_CONSTANT(STRETCH_KEEP); @@ -179,14 +178,14 @@ Ref<Texture2D> TextureRect::get_texture() const { return texture; } -void TextureRect::set_expand(bool p_expand) { - expand = p_expand; +void TextureRect::set_ignore_texture_size(bool p_ignore) { + ignore_texture_size = p_ignore; update(); update_minimum_size(); } -bool TextureRect::has_expand() const { - return expand; +bool TextureRect::get_ignore_texture_size() const { + return ignore_texture_size; } void TextureRect::set_stretch_mode(StretchMode p_mode) { diff --git a/scene/gui/texture_rect.h b/scene/gui/texture_rect.h index 0f93d5732f..7d667b25a8 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-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 TextureRect : public Control { public: enum StretchMode { - STRETCH_SCALE_ON_EXPAND, //default, for backwards compatibility STRETCH_SCALE, STRETCH_TILE, STRETCH_KEEP, @@ -49,11 +48,11 @@ public: }; private: - bool expand = false; + bool ignore_texture_size = false; bool hflip = false; bool vflip = false; Ref<Texture2D> texture; - StretchMode stretch_mode = STRETCH_SCALE_ON_EXPAND; + StretchMode stretch_mode = STRETCH_SCALE; void _texture_changed(); @@ -66,8 +65,8 @@ public: void set_texture(const Ref<Texture2D> &p_tex); Ref<Texture2D> get_texture() const; - void set_expand(bool p_expand); - bool has_expand() const; + void set_ignore_texture_size(bool p_ignore); + bool get_ignore_texture_size() const; void set_stretch_mode(StretchMode p_mode); StretchMode get_stretch_mode() const; diff --git a/scene/gui/tree.cpp b/scene/gui/tree.cpp index 5a6ac7c0d2..d3e7540790 100644 --- a/scene/gui/tree.cpp +++ b/scene/gui/tree.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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,10 +37,9 @@ #include "core/os/os.h" #include "core/string/print_string.h" #include "core/string/translation.h" +#include "scene/gui/box_container.h" #include "scene/main/window.h" -#include "box_container.h" - #include <limits.h> Size2 TreeItem::Cell::get_icon_size() const { @@ -102,6 +101,7 @@ void TreeItem::_change_tree(Tree *p_tree) { if (tree->popup_edited_item == this) { tree->popup_edited_item = nullptr; + tree->popup_pressing_edited_item = nullptr; tree->pressing_for_editor = false; } @@ -198,6 +198,65 @@ bool TreeItem::is_indeterminate(int p_column) const { return cells[p_column].indeterminate; } +void TreeItem::propagate_check(int p_column, bool p_emit_signal) { + bool ch = cells[p_column].checked; + + if (p_emit_signal) { + tree->emit_signal(SNAME("check_propagated_to_item"), this, p_column); + } + _propagate_check_through_children(p_column, ch, p_emit_signal); + _propagate_check_through_parents(p_column, p_emit_signal); +} + +void TreeItem::_propagate_check_through_children(int p_column, bool p_checked, bool p_emit_signal) { + TreeItem *current = get_first_child(); + while (current) { + current->set_checked(p_column, p_checked); + if (p_emit_signal) { + current->tree->emit_signal(SNAME("check_propagated_to_item"), current, p_column); + } + current->_propagate_check_through_children(p_column, p_checked, p_emit_signal); + current = current->get_next(); + } +} + +void TreeItem::_propagate_check_through_parents(int p_column, bool p_emit_signal) { + TreeItem *current = get_parent(); + if (!current) { + return; + } + + bool all_unchecked_and_not_indeterminate = true; + bool any_unchecked_or_indeterminate = false; + + TreeItem *child_item = current->get_first_child(); + while (child_item) { + if (!child_item->is_checked(p_column)) { + any_unchecked_or_indeterminate = true; + if (child_item->is_indeterminate(p_column)) { + all_unchecked_and_not_indeterminate = false; + break; + } + } else { + all_unchecked_and_not_indeterminate = false; + } + child_item = child_item->get_next(); + } + + if (all_unchecked_and_not_indeterminate) { + current->set_checked(p_column, false); + } else if (any_unchecked_or_indeterminate) { + current->set_indeterminate(p_column, true); + } else { + current->set_checked(p_column, true); + } + + if (p_emit_signal) { + current->tree->emit_signal(SNAME("check_propagated_to_item"), current, p_column); + } + current->_propagate_check_through_parents(p_column, p_emit_signal); +} + void TreeItem::set_text(int p_column, String p_text) { ERR_FAIL_INDEX(p_column, cells.size()); cells.write[p_column].text = p_text; @@ -275,7 +334,7 @@ int TreeItem::get_opentype_feature(int p_column, const String &p_name) const { return cells[p_column].opentype_features[tag]; } -void TreeItem::set_structured_text_bidi_override(int p_column, Control::StructuredTextParser p_parser) { +void TreeItem::set_structured_text_bidi_override(int p_column, TextServer::StructuredTextParser p_parser) { ERR_FAIL_INDEX(p_column, cells.size()); if (cells[p_column].st_parser != p_parser) { @@ -287,8 +346,8 @@ void TreeItem::set_structured_text_bidi_override(int p_column, Control::Structur } } -Control::StructuredTextParser TreeItem::get_structured_text_bidi_override(int p_column) const { - ERR_FAIL_INDEX_V(p_column, cells.size(), Control::STRUCTURED_TEXT_NONE); +TextServer::StructuredTextParser TreeItem::get_structured_text_bidi_override(int p_column) const { + ERR_FAIL_INDEX_V(p_column, cells.size(), TextServer::STRUCTURED_TEXT_NONE); return cells[p_column].st_parser; } @@ -485,6 +544,21 @@ bool TreeItem::is_collapsed() { return collapsed; } +void TreeItem::set_visible(bool p_visible) { + if (visible == p_visible) { + return; + } + visible = p_visible; + if (tree) { + tree->update(); + _changed_notify(); + } +} + +bool TreeItem::is_visible() { + return visible; +} + void TreeItem::uncollapse_tree() { TreeItem *t = this; while (t) { @@ -496,8 +570,9 @@ void TreeItem::uncollapse_tree() { void TreeItem::set_custom_minimum_height(int p_height) { custom_min_height = p_height; - for (Cell &c : cells) + for (Cell &c : cells) { c.cached_minimum_size_dirty = true; + } _changed_notify(); } @@ -586,7 +661,7 @@ TreeItem *TreeItem::get_first_child() const { return first_child; } -TreeItem *TreeItem::get_prev_visible(bool p_wrap) { +TreeItem *TreeItem::_get_prev_visible(bool p_wrap) { TreeItem *current = this; TreeItem *prev = current->get_prev(); @@ -622,7 +697,21 @@ TreeItem *TreeItem::get_prev_visible(bool p_wrap) { return current; } -TreeItem *TreeItem::get_next_visible(bool p_wrap) { +TreeItem *TreeItem::get_prev_visible(bool p_wrap) { + TreeItem *loop = this; + TreeItem *prev = this->_get_prev_visible(p_wrap); + while (prev && !prev->is_visible()) { + prev = prev->_get_prev_visible(p_wrap); + if (prev == loop) { + // Check that we haven't looped all the way around to the start. + prev = nullptr; + break; + } + } + return prev; +} + +TreeItem *TreeItem::_get_next_visible(bool p_wrap) { TreeItem *current = this; if (!current->collapsed && current->first_child) { @@ -649,12 +738,37 @@ TreeItem *TreeItem::get_next_visible(bool p_wrap) { return current; } +TreeItem *TreeItem::get_next_visible(bool p_wrap) { + TreeItem *loop = this; + TreeItem *next = this->_get_next_visible(p_wrap); + while (next && !next->is_visible()) { + next = next->_get_next_visible(p_wrap); + if (next == loop) { + // Check that we haven't looped all the way around to the start. + next = nullptr; + break; + } + } + return next; +} + 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_visible_child_count() { + _create_children_cache(); + int visible_count = 0; + for (int i = 0; i < children_cache.size(); i++) { + if (children_cache[i]->is_visible()) { + visible_count += 1; + } + } + return visible_count; +} + int TreeItem::get_child_count() { _create_children_cache(); return children_cache.size(); @@ -845,6 +959,12 @@ 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()); @@ -1033,8 +1153,9 @@ bool TreeItem::get_expand_right(int p_column) const { void TreeItem::set_disable_folding(bool p_disable) { disable_folding = p_disable; - for (Cell &c : cells) + for (Cell &c : cells) { c.cached_minimum_size_dirty = true; + } _changed_notify(0); } @@ -1096,31 +1217,30 @@ Size2 TreeItem::get_minimum_size(int p_column) { return cell.cached_minimum_size; } -Variant TreeItem::_call_recursive_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error) { +void 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; r_error.argument = 0; - return Variant(); + return; } if (p_args[0]->get_type() != Variant::STRING && p_args[0]->get_type() != Variant::STRING_NAME) { r_error.error = Callable::CallError::CALL_ERROR_INVALID_ARGUMENT; r_error.argument = 0; r_error.expected = Variant::STRING_NAME; - return Variant(); + return; } StringName method = *p_args[0]; call_recursive(method, &p_args[1], p_argcount - 1, r_error); - return Variant(); } void recursive_call_aux(TreeItem *p_item, const StringName &p_method, const Variant **p_args, int p_argcount, Callable::CallError &r_error) { if (!p_item) { return; } - p_item->call(p_method, p_args, p_argcount, r_error); + p_item->callp(p_method, p_args, p_argcount, r_error); TreeItem *c = p_item->get_first_child(); while (c) { recursive_call_aux(c, p_method, p_args, p_argcount, r_error); @@ -1141,6 +1261,8 @@ void TreeItem::_bind_methods() { 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("propagate_check", "column", "emit_signal"), &TreeItem::propagate_check, DEFVAL(true)); + ClassDB::bind_method(D_METHOD("set_text", "column", "text"), &TreeItem::set_text); ClassDB::bind_method(D_METHOD("get_text", "column"), &TreeItem::get_text); @@ -1188,6 +1310,9 @@ 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("set_visible", "enable"), &TreeItem::set_visible); + ClassDB::bind_method(D_METHOD("is_visible"), &TreeItem::is_visible); + 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); @@ -1220,9 +1345,11 @@ void TreeItem::_bind_methods() { ClassDB::bind_method(D_METHOD("set_custom_as_button", "column", "enable"), &TreeItem::set_custom_as_button); ClassDB::bind_method(D_METHOD("is_custom_set_as_button", "column"), &TreeItem::is_custom_set_as_button); - ClassDB::bind_method(D_METHOD("add_button", "column", "button", "button_idx", "disabled", "tooltip"), &TreeItem::add_button, DEFVAL(-1), DEFVAL(false), DEFVAL("")); + ClassDB::bind_method(D_METHOD("add_button", "column", "button", "id", "disabled", "tooltip"), &TreeItem::add_button, DEFVAL(-1), DEFVAL(false), DEFVAL("")); ClassDB::bind_method(D_METHOD("get_button_count", "column"), &TreeItem::get_button_count); ClassDB::bind_method(D_METHOD("get_button_tooltip", "column", "button_idx"), &TreeItem::get_button_tooltip); + ClassDB::bind_method(D_METHOD("get_button_id", "column", "button_idx"), &TreeItem::get_button_id); + ClassDB::bind_method(D_METHOD("get_button_by_id", "column", "id"), &TreeItem::get_button_by_id); ClassDB::bind_method(D_METHOD("get_button", "column", "button_idx"), &TreeItem::get_button); ClassDB::bind_method(D_METHOD("set_button", "column", "button_idx", "button"), &TreeItem::set_button); ClassDB::bind_method(D_METHOD("erase_button", "column", "button_idx"), &TreeItem::erase_button); @@ -1256,10 +1383,10 @@ void TreeItem::_bind_methods() { 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("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); + ClassDB::bind_method(D_METHOD("remove_child", "child"), &TreeItem::remove_child); { MethodInfo mi; @@ -1270,6 +1397,7 @@ void TreeItem::_bind_methods() { } ADD_PROPERTY(PropertyInfo(Variant::BOOL, "collapsed"), "set_collapsed", "is_collapsed"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "visible"), "set_visible", "is_visible"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "disable_folding"), "set_disable_folding", "is_folding_disabled"); ADD_PROPERTY(PropertyInfo(Variant::INT, "custom_minimum_height", PROPERTY_HINT_RANGE, "0,1000,1"), "set_custom_minimum_height", "get_custom_minimum_height"); @@ -1342,8 +1470,8 @@ void Tree::update_cache() { 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.hseparation = get_theme_constant(SNAME("h_separation")); + cache.vseparation = get_theme_constant(SNAME("v_separation")); cache.item_margin = get_theme_constant(SNAME("item_margin")); cache.button_margin = get_theme_constant(SNAME("button_margin")); @@ -1375,7 +1503,7 @@ void Tree::update_cache() { } int Tree::compute_item_height(TreeItem *p_item) const { - if (p_item == root && hide_root) { + if ((p_item == root && hide_root) || !p_item->is_visible()) { return 0; } @@ -1436,6 +1564,9 @@ int Tree::compute_item_height(TreeItem *p_item) const { } int Tree::get_item_height(TreeItem *p_item) const { + if (!p_item->is_visible()) { + return 0; + } int height = compute_item_height(p_item); height += cache.vseparation; @@ -1616,6 +1747,10 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 return -1; //draw no more! } + if (!p_item->is_visible()) { + return 0; + } + RID ci = get_canvas_item(); int htotal = 0; @@ -1631,8 +1766,7 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 bool skip = (p_item == root && hide_root); if (!skip && (p_pos.y + label_h - cache.offset.y) > 0) { - //draw separation. - //if (p_item->get_parent()!=root || !hide_root) + // Draw separation. ERR_FAIL_COND_V(cache.font.is_null(), -1); @@ -1685,19 +1819,16 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 for (int j = p_item->cells[i].buttons.size() - 1; j >= 0; j--) { Ref<Texture2D> b = p_item->cells[i].buttons[j].texture; Size2 s = b->get_size() + cache.button_pressed->get_minimum_size(); - if (s.height < label_h) { - s.height = label_h; - } Point2i o = Point2i(ofs + w - s.width, p_pos.y) - cache.offset + p_draw_ofs; 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 + // Being pressed. Point2 od = o; if (rtl) { od.x = get_size().width - od.x - s.x; } - cache.button_pressed->draw(get_canvas_item(), Rect2(od, s)); + cache.button_pressed->draw(get_canvas_item(), Rect2(od.x, od.y, s.width, MAX(s.height, label_h))); } o.y += (label_h - s.height) / 2; @@ -1990,7 +2121,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->first_child) { //has children, draw the guide box + if (!p_item->disable_folding && !hide_folding && p_item->first_child && p_item->get_visible_child_count() != 0) { //has visible children, draw the guide box Ref<Texture2D> arrow; @@ -2020,7 +2151,6 @@ 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->first_child; int base_ofs = children_pos.y - cache.offset.y + p_draw_ofs.y; @@ -2028,82 +2158,97 @@ int Tree::draw_item(const Point2i &p_pos, const Point2 &p_draw_ofs, const Size2 int prev_hl_ofs = base_ofs; while (c) { + int child_h = -1; if (htotal >= 0) { - int child_h = draw_item(children_pos, p_draw_ofs, p_draw_size, c); + child_h = draw_item(children_pos, p_draw_ofs, p_draw_size, c); + } - // 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; + // Draw relationship lines. + if (cache.draw_relationship_lines > 0 && (!hide_root || c->parent != root) && c->is_visible()) { + 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); - } + if (c->get_visible_child_count() > 0) { + root_pos -= Point2i(cache.arrow->get_width(), 0); + } - float line_width = cache.relationship_line_width * Math::round(cache.base_scale); - float parent_line_width = cache.parent_hl_line_width * Math::round(cache.base_scale); - float children_line_width = cache.children_hl_line_width * Math::round(cache.base_scale); + float line_width = cache.relationship_line_width * Math::round(cache.base_scale); + float parent_line_width = cache.parent_hl_line_width * Math::round(cache.base_scale); + float children_line_width = cache.children_hl_line_width * Math::round(cache.base_scale); - Point2i parent_pos = Point2i(parent_ofs - cache.arrow->get_width() / 2, p_pos.y + label_h / 2 + cache.arrow->get_height() / 2) - cache.offset + p_draw_ofs; + Point2i parent_pos = Point2i(parent_ofs - cache.arrow->get_width() / 2, p_pos.y + label_h / 2 + cache.arrow->get_height() / 2) - cache.offset + p_draw_ofs; - int more_prev_ofs = 0; + int more_prev_ofs = 0; - if (root_pos.y + line_width >= 0) { - if (rtl) { - root_pos.x = get_size().width - root_pos.x; - parent_pos.x = get_size().width - parent_pos.x; - } + if (root_pos.y + line_width >= 0) { + if (rtl) { + root_pos.x = get_size().width - root_pos.x; + parent_pos.x = get_size().width - parent_pos.x; + } - // Order of parts on this bend: the horizontal line first, then the vertical line. - if (_is_branch_selected(c)) { - // If this item or one of its children is selected, we draw the line using parent highlight style. + // 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. + if (htotal >= 0) { 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)) { + if (htotal >= 0) { + 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); - 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 { + } else { + if (htotal >= 0) { 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, 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)) { + if (htotal >= 0) { 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); + } + 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 { + prev_hl_ofs = root_pos.y + Math::floor(parent_line_width / 2); + } else { + if (htotal >= 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 + Math::floor(line_width / 2)), Point2i(parent_pos.x, prev_ofs + Math::floor(line_width / 2)), 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 - } + prev_ofs = root_pos.y + more_prev_ofs; + } - htotal = -1; - children_pos.y = cache.offset.y + p_draw_size.height; - } else { - htotal += child_h; - children_pos.y += child_h; + if (child_h < 0) { + if (htotal == -1) { + break; // Last loop done, stop. + } + + if (cache.draw_relationship_lines == 0) { + return -1; // No need to draw anymore, full stop. } + + htotal = -1; + children_pos.y = cache.offset.y + p_draw_size.height; + } else { + htotal += child_h; + children_pos.y += child_h; } c = c->next; @@ -2189,14 +2334,11 @@ void Tree::select_single_item(TreeItem *p_selected, TreeItem *p_current, int p_c emit_signal(SNAME("item_selected")); emitted_row = true; } - /* - if (p_col==i) - p_current->selected_signal.call(p_col); - */ - } else if (c.selected) { - c.selected = false; - //p_current->deselected_signal.call(p_col); + if (p_selected != p_current) { + // Deselect other rows. + c.selected = false; + } } } else if (select_mode == SELECT_SINGLE || select_mode == SELECT_MULTI) { if (!r_in_range && &selected_cell == &c) { @@ -2305,6 +2447,11 @@ void Tree::_range_click_timeout() { } 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, MouseButton p_button, const Ref<InputEventWithModifiers> &p_mod) { + if (p_item && !p_item->is_visible()) { + // Skip any processing of invisible items. + return 0; + } + int item_h = compute_item_height(p_item) + cache.vseparation; bool skip = (p_item == root && hide_root); @@ -2316,12 +2463,9 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int return -1; } - if (!p_item->disable_folding && !hide_folding && (p_pos.x >= x_ofs && p_pos.x < (x_ofs + cache.item_margin))) { - if (p_item->first_child) { - p_item->set_collapsed(!p_item->is_collapsed()); - } - - return -1; //handled! + if (!p_item->disable_folding && !hide_folding && p_item->first_child && (p_pos.x >= x_ofs && p_pos.x < (x_ofs + cache.item_margin))) { + p_item->set_collapsed(!p_item->is_collapsed()); + return -1; } int x = p_pos.x; @@ -2417,7 +2561,6 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int cache.click_column = col; cache.click_pos = click_pos; update(); - //emit_signal(SNAME("button_pressed")); return -1; } @@ -2428,7 +2571,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int /* process selection */ 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 - + // Emits the "item_activated" signal. propagate_mouse_activated = true; incr_search.clear(); @@ -2439,9 +2582,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int if (!c.selected || p_button == MouseButton::RIGHT) { p_item->select(col); emit_signal(SNAME("multi_selected"), p_item, col, true); - if (p_button == MouseButton::RIGHT) { - emit_signal(SNAME("item_rmb_selected"), get_local_mouse_position()); - } + emit_signal(SNAME("item_mouse_selected"), get_local_mouse_position(), p_button); //p_item->selected_signal.call(col); } else { @@ -2456,9 +2597,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int bool inrange = false; select_single_item(p_item, root, col, selected_item, &inrange); - if (p_button == MouseButton::RIGHT) { - emit_signal(SNAME("item_rmb_selected"), get_local_mouse_position()); - } + emit_signal(SNAME("item_mouse_selected"), get_local_mouse_position(), p_button); } else { int icount = _count_selected_items(root); @@ -2470,9 +2609,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int select_single_item(p_item, root, col); } - if (p_button == MouseButton::RIGHT) { - emit_signal(SNAME("item_rmb_selected"), get_local_mouse_position()); - } + emit_signal(SNAME("item_mouse_selected"), get_local_mouse_position(), p_button); } } @@ -2509,11 +2646,11 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int if (force_edit_checkbox_only_on_checkbox) { if (x < cache.checked->get_width()) { p_item->set_checked(col, !c.checked); - item_edited(col, p_item); + item_edited(col, p_item, p_button); } } else { p_item->set_checked(col, !c.checked); - item_edited(col, p_item); + item_edited(col, p_item, p_button); } click_handled = true; //p_item->edited_signal.call(col); @@ -2555,17 +2692,17 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int p_item->set_range(col, c.val + (up ? 1.0 : -1.0) * c.step); - item_edited(col, p_item); + item_edited(col, p_item, p_button); } else if (p_button == MouseButton::RIGHT) { p_item->set_range(col, (up ? c.max : c.min)); - item_edited(col, p_item); + item_edited(col, p_item, p_button); } else if (p_button == MouseButton::WHEEL_UP) { p_item->set_range(col, c.val + c.step); - item_edited(col, p_item); + item_edited(col, p_item, p_button); } else if (p_button == MouseButton::WHEEL_DOWN) { p_item->set_range(col, c.val - c.step); - item_edited(col, p_item); + item_edited(col, p_item, p_button); } //p_item->edited_signal.call(col); @@ -2596,7 +2733,7 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int } if (!p_item->cells[col].custom_button || !on_arrow) { - item_edited(col, p_item, p_button == MouseButton::LEFT); + item_edited(col, p_item, p_button); } click_handled = true; return -1; @@ -2608,8 +2745,8 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int } click_handled = true; - popup_edited_item = p_item; - popup_edited_item_col = col; + popup_pressing_edited_item = p_item; + popup_pressing_edited_item_column = col; pressing_item_rect = Rect2(get_global_position() + Point2i(col_ofs, _get_title_button_height() + y_ofs) - cache.offset, Size2(col_width, item_h)); pressing_for_editor_text = editor_text; @@ -2643,8 +2780,8 @@ int Tree::propagate_mouse_event(const Point2i &p_pos, int x_ofs, int y_ofs, int item_h += child_h; } } - if (p_item == root && p_button == MouseButton::RIGHT) { - emit_signal(SNAME("empty_rmb"), get_local_mouse_position()); + if (p_item == root) { + emit_signal(SNAME("empty_clicked"), get_local_mouse_position(), p_button); } } @@ -3052,7 +3189,6 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { } Ref<InputEventMouseMotion> mm = p_event; - if (mm.is_valid()) { if (cache.font.is_null()) { // avoid a strange case that may corrupt stuff update_cache(); @@ -3144,10 +3280,16 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { update(); } - if (pressing_for_editor && popup_edited_item && (popup_edited_item->get_cell_mode(popup_edited_item_col) == TreeItem::CELL_MODE_RANGE)) { - //range drag + if (pressing_for_editor && popup_pressing_edited_item && (popup_pressing_edited_item->get_cell_mode(popup_pressing_edited_item_column) == TreeItem::CELL_MODE_RANGE)) { + /* This needs to happen now, because the popup can be closed when pressing another item, and must remain the popup edited item until it actually closes */ + popup_edited_item = popup_pressing_edited_item; + popup_edited_item_col = popup_pressing_edited_item_column; + + popup_pressing_edited_item = nullptr; + popup_pressing_edited_item_column = -1; if (!range_drag_enabled) { + //range drag Vector2 cpos = mm->get_position(); if (rtl) { cpos.x = get_size().width - cpos.x; @@ -3172,22 +3314,21 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { if (drag_touching && !drag_touching_deaccel) { drag_accum -= mm->get_relative().y; v_scroll->set_value(drag_from + drag_accum); - drag_speed = -mm->get_speed().y; + drag_speed = -mm->get_velocity().y; } } - Ref<InputEventMouseButton> b = p_event; - - if (b.is_valid()) { + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { if (cache.font.is_null()) { // avoid a strange case that may corrupt stuff update_cache(); } bool rtl = is_layout_rtl(); - if (!b->is_pressed()) { - if (b->get_button_index() == MouseButton::LEFT) { - Point2 pos = b->get_position(); + if (!mb->is_pressed()) { + if (mb->get_button_index() == MouseButton::LEFT) { + Point2 pos = mb->get_position(); if (rtl) { pos.x = get_size().width - pos.x; } @@ -3222,9 +3363,22 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { warp_mouse(range_drag_capture_pos); } else { Rect2 rect = get_selected()->get_meta("__focus_rect"); - Point2 mpos = b->get_position(); + Point2 mpos = mb->get_position(); + int icon_size_x = 0; + Ref<Texture2D> icon = get_selected()->get_icon(selected_col); + if (icon.is_valid()) { + Rect2i icon_region = get_selected()->get_icon_region(selected_col); + if (icon_region == Rect2i()) { + icon_size_x = icon->get_width(); + } else { + icon_size_x = icon_region.size.width; + } + } + // Icon is treated as if it is outside of the rect so that double clicking on it will emit the item_double_clicked signal. if (rtl) { - mpos.x = get_size().width - mpos.x; + mpos.x = get_size().width - (mpos.x + icon_size_x); + } else { + mpos.x -= icon_size_x; } if (rect.has_point(mpos)) { if (!edit_selected()) { @@ -3237,17 +3391,6 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { pressing_for_editor = false; } - 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(SNAME("button_pressed"), cache.click_item, cache.click_column, cache.click_id); - } - cache.click_type = Cache::CLICK_NONE; - cache.click_index = -1; - cache.click_id = -1; - cache.click_item = nullptr; - cache.click_column = 0; - if (drag_touching) { if (drag_speed == 0) { drag_touching_deaccel = false; @@ -3257,8 +3400,20 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { drag_touching_deaccel = true; } } - update(); } + + 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_clicked", cache.click_item, cache.click_column, cache.click_id, mb->get_button_index()); + } + + cache.click_type = Cache::CLICK_NONE; + cache.click_index = -1; + cache.click_id = -1; + cache.click_item = nullptr; + cache.click_column = 0; + update(); return; } @@ -3266,12 +3421,12 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { return; } - switch (b->get_button_index()) { + switch (mb->get_button_index()) { case MouseButton::RIGHT: case MouseButton::LEFT: { Ref<StyleBox> bg = cache.bg; - Point2 pos = b->get_position(); + Point2 pos = mb->get_position(); if (rtl) { pos.x = get_size().width - pos.x; } @@ -3281,7 +3436,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { pos.y -= _get_title_button_height(); if (pos.y < 0) { - if (b->get_button_index() == MouseButton::LEFT) { + if (mb->get_button_index() == MouseButton::LEFT) { pos.x += cache.offset.x; int len = 0; for (int i = 0; i < columns.size(); i++) { @@ -3298,10 +3453,8 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { break; } } + if (!root || (!root->get_first_child() && hide_root)) { - if (b->get_button_index() == MouseButton::RIGHT && allow_rmb_select) { - emit_signal(SNAME("empty_tree_rmb_selected"), get_local_mouse_position()); - } break; } @@ -3316,17 +3469,17 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { cache.rtl = is_layout_rtl(); blocked++; - propagate_mouse_event(pos + cache.offset, 0, 0, x_limit + cache.offset.width, b->is_double_click(), root, b->get_button_index(), b); + propagate_mouse_event(pos + cache.offset, 0, 0, x_limit + cache.offset.width, mb->is_double_click(), root, mb->get_button_index(), mb); blocked--; if (pressing_for_editor) { - pressing_pos = b->get_position(); + pressing_pos = mb->get_position(); if (rtl) { pressing_pos.x = get_size().width - pressing_pos.x; } } - if (b->get_button_index() == MouseButton::RIGHT) { + if (mb->get_button_index() == MouseButton::RIGHT) { break; } @@ -3349,8 +3502,8 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { set_physics_process_internal(true); } - if (b->get_button_index() == MouseButton::LEFT) { - if (get_item_at_position(b->get_position()) == nullptr && !b->is_shift_pressed() && !b->is_ctrl_pressed() && !b->is_command_pressed()) { + if (mb->get_button_index() == MouseButton::LEFT) { + if (get_item_at_position(mb->get_position()) == nullptr && !mb->is_shift_pressed() && !mb->is_ctrl_pressed() && !mb->is_command_pressed()) { emit_signal(SNAME("nothing_selected")); } } @@ -3364,7 +3517,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { } break; case MouseButton::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); + v_scroll->set_value(v_scroll->get_value() - v_scroll->get_page() * mb->get_factor() / 8); if (v_scroll->get_value() != prev_value) { accept_event(); } @@ -3372,7 +3525,7 @@ void Tree::gui_input(const Ref<InputEvent> &p_event) { } break; case MouseButton::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); + v_scroll->set_value(v_scroll->get_value() + v_scroll->get_page() * mb->get_factor() / 8); if (v_scroll->get_value() != prev_value) { accept_event(); } @@ -3566,178 +3719,187 @@ int Tree::_get_title_button_height() const { } void Tree::_notification(int p_what) { - if (p_what == NOTIFICATION_FOCUS_ENTER) { - if (get_viewport()) { - focus_in_id = get_viewport()->get_processed_events_count(); - } - } - if (p_what == NOTIFICATION_MOUSE_EXIT) { - if (cache.hover_type != Cache::CLICK_NONE) { - cache.hover_type = Cache::CLICK_NONE; - update(); - } - } - - if (p_what == NOTIFICATION_VISIBILITY_CHANGED) { - drag_touching = false; - } + switch (p_what) { + case NOTIFICATION_FOCUS_ENTER: { + if (get_viewport()) { + focus_in_id = get_viewport()->get_processed_events_count(); + } + } break; - if (p_what == NOTIFICATION_ENTER_TREE) { - update_cache(); - } - if (p_what == NOTIFICATION_DRAG_END) { - drop_mode_flags = 0; - scrolling = false; - set_physics_process_internal(false); - update(); - } - if (p_what == NOTIFICATION_DRAG_BEGIN) { - single_select_defer = nullptr; - if (cache.scroll_speed > 0) { - scrolling = true; - set_physics_process_internal(true); - } - } - if (p_what == NOTIFICATION_INTERNAL_PHYSICS_PROCESS) { - if (drag_touching) { - if (drag_touching_deaccel) { - float pos = v_scroll->get_value(); - pos += drag_speed * get_physics_process_delta_time(); + case NOTIFICATION_MOUSE_EXIT: { + if (cache.hover_type != Cache::CLICK_NONE) { + cache.hover_type = Cache::CLICK_NONE; + update(); + } + } break; - bool turnoff = false; - if (pos < 0) { - pos = 0; - turnoff = true; - set_physics_process_internal(false); - drag_touching = false; - drag_touching_deaccel = false; - } - if (pos > (v_scroll->get_max() - v_scroll->get_page())) { - pos = v_scroll->get_max() - v_scroll->get_page(); - turnoff = true; - } + case NOTIFICATION_VISIBILITY_CHANGED: { + drag_touching = false; + } break; - v_scroll->set_value(pos); - float sgn = drag_speed < 0 ? -1 : 1; - float val = Math::abs(drag_speed); - val -= 1000 * get_physics_process_delta_time(); + case NOTIFICATION_ENTER_TREE: { + update_cache(); + } break; - if (val < 0) { - turnoff = true; - } - drag_speed = sgn * val; + case NOTIFICATION_DRAG_END: { + drop_mode_flags = 0; + scrolling = false; + set_physics_process_internal(false); + update(); + } break; - if (turnoff) { - set_physics_process_internal(false); - drag_touching = false; - drag_touching_deaccel = false; - } + case NOTIFICATION_DRAG_BEGIN: { + single_select_defer = nullptr; + if (cache.scroll_speed > 0) { + scrolling = true; + set_physics_process_internal(true); } - } + } break; - Point2 mouse_position = get_viewport()->get_mouse_position() - get_global_position(); - if (scrolling && get_rect().grow(cache.scroll_border).has_point(mouse_position)) { - Point2 point; + case NOTIFICATION_INTERNAL_PHYSICS_PROCESS: { + if (drag_touching) { + if (drag_touching_deaccel) { + float pos = v_scroll->get_value(); + pos += drag_speed * get_physics_process_delta_time(); - if ((ABS(mouse_position.x) < ABS(mouse_position.x - get_size().width)) && (ABS(mouse_position.x) < cache.scroll_border)) { - point.x = mouse_position.x - cache.scroll_border; - } else if (ABS(mouse_position.x - get_size().width) < cache.scroll_border) { - point.x = mouse_position.x - (get_size().width - cache.scroll_border); - } + bool turnoff = false; + if (pos < 0) { + pos = 0; + turnoff = true; + set_physics_process_internal(false); + drag_touching = false; + drag_touching_deaccel = false; + } + if (pos > (v_scroll->get_max() - v_scroll->get_page())) { + pos = v_scroll->get_max() - v_scroll->get_page(); + turnoff = true; + } - if ((ABS(mouse_position.y) < ABS(mouse_position.y - get_size().height)) && (ABS(mouse_position.y) < cache.scroll_border)) { - point.y = mouse_position.y - cache.scroll_border; - } else if (ABS(mouse_position.y - get_size().height) < cache.scroll_border) { - point.y = mouse_position.y - (get_size().height - cache.scroll_border); + v_scroll->set_value(pos); + float sgn = drag_speed < 0 ? -1 : 1; + float val = Math::abs(drag_speed); + val -= 1000 * get_physics_process_delta_time(); + + if (val < 0) { + turnoff = true; + } + drag_speed = sgn * val; + + if (turnoff) { + set_physics_process_internal(false); + drag_touching = false; + drag_touching_deaccel = false; + } + } } - point *= cache.scroll_speed * get_physics_process_delta_time(); - point += get_scroll(); - h_scroll->set_value(point.x); - v_scroll->set_value(point.y); - } - } + Point2 mouse_position = get_viewport()->get_mouse_position() - get_global_position(); + if (scrolling && get_rect().grow(cache.scroll_border).has_point(mouse_position)) { + Point2 point; - if (p_what == NOTIFICATION_DRAW) { - update_cache(); - update_scrollbars(); - RID ci = get_canvas_item(); + if ((ABS(mouse_position.x) < ABS(mouse_position.x - get_size().width)) && (ABS(mouse_position.x) < cache.scroll_border)) { + point.x = mouse_position.x - cache.scroll_border; + } else if (ABS(mouse_position.x - get_size().width) < cache.scroll_border) { + point.x = mouse_position.x - (get_size().width - cache.scroll_border); + } - Ref<StyleBox> bg = cache.bg; - Color font_outline_color = get_theme_color(SNAME("font_outline_color")); - int outline_size = get_theme_constant(SNAME("outline_size")); + if ((ABS(mouse_position.y) < ABS(mouse_position.y - get_size().height)) && (ABS(mouse_position.y) < cache.scroll_border)) { + point.y = mouse_position.y - cache.scroll_border; + } else if (ABS(mouse_position.y - get_size().height) < cache.scroll_border) { + point.y = mouse_position.y - (get_size().height - cache.scroll_border); + } - 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; - } + point *= cache.scroll_speed * get_physics_process_delta_time(); + point += get_scroll(); + h_scroll->set_value(point.x); + v_scroll->set_value(point.y); + } + } break; - bg->draw(ci, Rect2(Point2(), get_size())); + case NOTIFICATION_DRAW: { + update_cache(); + update_scrollbars(); + RID ci = get_canvas_item(); + + Ref<StyleBox> bg = cache.bg; + 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; + } - int tbh = _get_title_button_height(); + bg->draw(ci, Rect2(Point2(), get_size())); - draw_ofs.y += tbh; - draw_size.y -= tbh; + int tbh = _get_title_button_height(); - cache.rtl = is_layout_rtl(); + draw_ofs.y += tbh; + draw_size.y -= tbh; - if (root && get_size().x > 0 && get_size().y > 0) { - draw_item(Point2(), draw_ofs, draw_size, root); - } + cache.rtl = is_layout_rtl(); - if (show_column_titles) { - //title buttons - 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(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; - 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); + if (root && get_size().x > 0 && get_size().y > 0) { + draw_item(Point2(), draw_ofs, draw_size, root); } - } - // 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 (show_column_titles) { + //title buttons + 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(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; + 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); + } + } - if (p_what == NOTIFICATION_THEME_CHANGED || p_what == NOTIFICATION_LAYOUT_DIRECTION_CHANGED || p_what == NOTIFICATION_TRANSLATION_CHANGED) { - update_cache(); - _update_all(); - } + // 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); + } + } break; - if (p_what == NOTIFICATION_RESIZED || p_what == NOTIFICATION_TRANSFORM_CHANGED) { - if (popup_edited_item != nullptr) { - Rect2 rect = popup_edited_item->get_meta("__focus_rect"); - Vector2 ofs(0, (text_editor->get_size().height - rect.size.height) / 2); - Point2i textedpos = get_global_position() + rect.position - ofs; + case NOTIFICATION_THEME_CHANGED: + case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: + case NOTIFICATION_TRANSLATION_CHANGED: { + update_cache(); + _update_all(); + } break; - if (cache.text_editor_position != textedpos) { - cache.text_editor_position = textedpos; - text_editor->set_position(textedpos); - value_editor->set_position(textedpos + Point2i(0, text_editor->get_size().height)); + case NOTIFICATION_RESIZED: + case NOTIFICATION_TRANSFORM_CHANGED: { + if (popup_edited_item != nullptr) { + Rect2 rect = popup_edited_item->get_meta("__focus_rect"); + Vector2 ofs(0, (text_editor->get_size().height - rect.size.height) / 2); + Point2i textedpos = get_global_position() + rect.position - ofs; + + if (cache.text_editor_position != textedpos) { + cache.text_editor_position = textedpos; + text_editor->set_position(textedpos); + value_editor->set_position(textedpos + Point2i(0, text_editor->get_size().height)); + } } - } + } break; } } @@ -3809,16 +3971,15 @@ TreeItem *Tree::get_last_item() const { return last; } -void Tree::item_edited(int p_column, TreeItem *p_item, bool p_lmb) { +void Tree::item_edited(int p_column, TreeItem *p_item, MouseButton p_custom_mouse_index) { 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(SNAME("item_edited")); - } else { - emit_signal(SNAME("item_rmb_edited")); + emit_signal(SNAME("item_edited")); + if (p_custom_mouse_index != MouseButton::NONE) { + emit_signal(SNAME("custom_item_clicked"), p_custom_mouse_index); } } @@ -3910,6 +4071,7 @@ void Tree::clear() { selected_item = nullptr; edited_item = nullptr; popup_edited_item = nullptr; + popup_pressing_edited_item = nullptr; update(); }; @@ -3987,10 +4149,6 @@ int Tree::get_edited_column() const { } TreeItem *Tree::get_next_selected(TreeItem *p_item) { - /* - if (!p_item) - return nullptr; - */ if (!root) { return nullptr; } @@ -4048,7 +4206,7 @@ int Tree::get_column_minimum_width(int p_column) const { depth += 1; } else { TreeItem *common_parent = item->get_parent(); - while (common_parent != next->get_parent()) { + while (common_parent != next->get_parent() && common_parent) { common_parent = common_parent->get_parent(); depth -= 1; } @@ -4229,12 +4387,16 @@ int Tree::get_pressed_button() const { return pressed_button; } -Rect2 Tree::get_item_rect(TreeItem *p_item, int p_column) const { +Rect2 Tree::get_item_rect(TreeItem *p_item, int p_column, int p_button) const { ERR_FAIL_NULL_V(p_item, Rect2()); ERR_FAIL_COND_V(p_item->tree != this, Rect2()); if (p_column != -1) { ERR_FAIL_INDEX_V(p_column, columns.size(), Rect2()); } + if (p_button != -1) { + ERR_FAIL_COND_V(p_column == -1, Rect2()); // pass a column if you want to pass a button + ERR_FAIL_INDEX_V(p_button, p_item->cells[p_column].buttons.size(), Rect2()); + } int ofs = get_item_offset(p_item); int height = compute_item_height(p_item); @@ -4252,6 +4414,19 @@ Rect2 Tree::get_item_rect(TreeItem *p_item, int p_column) const { } r.position.x = accum; r.size.x = get_column_width(p_column); + if (p_button != -1) { + const TreeItem::Cell &c = p_item->cells[p_column]; + Vector2 ofst = Vector2(r.position.x + r.size.x, r.position.y); + for (int j = c.buttons.size() - 1; j >= 0; j--) { + Ref<Texture2D> b = c.buttons[j].texture; + Size2 size = b->get_size() + cache.button_pressed->get_minimum_size(); + ofst.x -= size.x; + + if (j == p_button) { + return Rect2(ofst, size); + } + } + } } return r; @@ -4347,21 +4522,30 @@ Point2 Tree::get_scroll() const { return ofs; } -void Tree::scroll_to_item(TreeItem *p_item) { - if (!is_visible_in_tree()) { - // hack to work around crash in get_item_rect() if Tree is not in tree. - return; +void Tree::scroll_to_item(TreeItem *p_item, bool p_center_on_item) { + ERR_FAIL_NULL(p_item); + if (!is_visible_in_tree() || !p_item->is_visible()) { + return; // Hack to work around crash in get_item_rect() if Tree is not in tree. } - // make sure the scrollbar min and max are up to date with latest changes. update_scrollbars(); - const Rect2 r = get_item_rect(p_item); + const real_t tree_height = get_size().y; + const Rect2 item_rect = get_item_rect(p_item); + const real_t item_y = item_rect.position.y; + const real_t item_height = item_rect.size.y + cache.vseparation; - if (r.position.y <= v_scroll->get_value()) { - v_scroll->set_value(r.position.y); - } else if (r.position.y + r.size.y + 2 * cache.vseparation > v_scroll->get_value() + get_size().y) { - v_scroll->set_value(r.position.y + r.size.y + 2 * cache.vseparation - get_size().y); + if (p_center_on_item) { + v_scroll->set_value(item_y - (tree_height - item_height) / 2.0f); + } else { + if (item_y < v_scroll->get_value()) { + v_scroll->set_value(item_y); + } else { + const real_t new_position = item_y + item_height - tree_height; + if (new_position > v_scroll->get_value()) { + v_scroll->set_value(new_position); + } + } } } @@ -4464,7 +4648,7 @@ void Tree::_do_incr_search(const String &p_add) { TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_column, int &h, int §ion) const { Point2 pos = p_pos; - if (root != p_item || !hide_root) { + if ((root != p_item || !hide_root) && p_item->is_visible()) { h = compute_item_height(p_item) + cache.vseparation; if (pos.y < h) { if (drop_mode_flags == DROP_MODE_ON_ITEM) { @@ -4497,7 +4681,7 @@ TreeItem *Tree::_find_item_at_pos(TreeItem *p_item, const Point2 &p_pos, int &r_ h = 0; } - if (p_item->is_collapsed()) { + if (p_item->is_collapsed() || !p_item->is_visible()) { return nullptr; // do not try children, it's collapsed } @@ -4753,7 +4937,7 @@ bool Tree::get_allow_reselect() const { void Tree::_bind_methods() { 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("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_custom_minimum_width", "column", "min_width"), &Tree::set_column_custom_minimum_width); @@ -4768,7 +4952,7 @@ void Tree::_bind_methods() { ClassDB::bind_method(D_METHOD("set_hide_root", "enable"), &Tree::set_hide_root); ClassDB::bind_method(D_METHOD("is_root_hidden"), &Tree::is_root_hidden); - ClassDB::bind_method(D_METHOD("get_next_selected", "from"), &Tree::_get_next_selected); + ClassDB::bind_method(D_METHOD("get_next_selected", "from"), &Tree::get_next_selected); ClassDB::bind_method(D_METHOD("get_selected"), &Tree::get_selected); ClassDB::bind_method(D_METHOD("get_selected_column"), &Tree::get_selected_column); ClassDB::bind_method(D_METHOD("get_pressed_button"), &Tree::get_pressed_button); @@ -4782,10 +4966,11 @@ void Tree::_bind_methods() { 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_area_rect", "item", "column", "button_index"), &Tree::get_item_rect, DEFVAL(-1), DEFVAL(-1)); ClassDB::bind_method(D_METHOD("get_item_at_position", "position"), &Tree::get_item_at_position); ClassDB::bind_method(D_METHOD("get_column_at_position", "position"), &Tree::get_column_at_position); ClassDB::bind_method(D_METHOD("get_drop_section_at_position", "position"), &Tree::get_drop_section_at_position); + ClassDB::bind_method(D_METHOD("get_button_id_at_position", "position"), &Tree::get_button_id_at_position); ClassDB::bind_method(D_METHOD("ensure_cursor_is_visible"), &Tree::ensure_cursor_is_visible); @@ -4806,7 +4991,7 @@ void Tree::_bind_methods() { 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("scroll_to_item", "item", "center_on_item"), &Tree::scroll_to_item, DEFVAL(false)); 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); @@ -4827,7 +5012,7 @@ void Tree::_bind_methods() { ClassDB::bind_method(D_METHOD("get_allow_reselect"), &Tree::get_allow_reselect); ADD_PROPERTY(PropertyInfo(Variant::INT, "columns"), "set_columns", "get_columns"); - ADD_PROPERTY(PropertyInfo(Variant::INT, "column_titles_visible"), "set_column_titles_visible", "are_column_titles_visible"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "column_titles_visible"), "set_column_titles_visible", "are_column_titles_visible"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "allow_reselect"), "set_allow_reselect", "get_allow_reselect"); 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"); @@ -4840,16 +5025,15 @@ void Tree::_bind_methods() { ADD_SIGNAL(MethodInfo("item_selected")); ADD_SIGNAL(MethodInfo("cell_selected")); ADD_SIGNAL(MethodInfo("multi_selected", PropertyInfo(Variant::OBJECT, "item", PROPERTY_HINT_RESOURCE_TYPE, "TreeItem"), PropertyInfo(Variant::INT, "column"), PropertyInfo(Variant::BOOL, "selected"))); - ADD_SIGNAL(MethodInfo("item_rmb_selected", PropertyInfo(Variant::VECTOR2, "position"))); - ADD_SIGNAL(MethodInfo("empty_rmb", PropertyInfo(Variant::VECTOR2, "position"))); - ADD_SIGNAL(MethodInfo("empty_tree_rmb_selected", PropertyInfo(Variant::VECTOR2, "position"))); + ADD_SIGNAL(MethodInfo("item_mouse_selected", PropertyInfo(Variant::VECTOR2, "position"), PropertyInfo(Variant::INT, "mouse_button_index"))); + ADD_SIGNAL(MethodInfo("empty_clicked", PropertyInfo(Variant::VECTOR2, "position"), PropertyInfo(Variant::INT, "mouse_button_index"))); ADD_SIGNAL(MethodInfo("item_edited")); - ADD_SIGNAL(MethodInfo("item_rmb_edited")); + ADD_SIGNAL(MethodInfo("custom_item_clicked", PropertyInfo(Variant::INT, "mouse_button_index"))); 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_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("check_propagated_to_item", PropertyInfo(Variant::OBJECT, "item", PROPERTY_HINT_RESOURCE_TYPE, "TreeItem"), PropertyInfo(Variant::INT, "column"))); + ADD_SIGNAL(MethodInfo("button_clicked", PropertyInfo(Variant::OBJECT, "item", PROPERTY_HINT_RESOURCE_TYPE, "TreeItem"), PropertyInfo(Variant::INT, "column"), PropertyInfo(Variant::INT, "id"), PropertyInfo(Variant::INT, "mouse_button_index"))); ADD_SIGNAL(MethodInfo("custom_popup_edited", PropertyInfo(Variant::BOOL, "arrow_clicked"))); ADD_SIGNAL(MethodInfo("item_activated")); ADD_SIGNAL(MethodInfo("column_title_pressed", PropertyInfo(Variant::INT, "column"))); diff --git a/scene/gui/tree.h b/scene/gui/tree.h index a190567f0f..0a8dd3204a 100644 --- a/scene/gui/tree.h +++ b/scene/gui/tree.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 */ @@ -65,7 +65,7 @@ private: Ref<TextLine> text_buf; Dictionary opentype_features; String language; - Control::StructuredTextParser st_parser = Control::STRUCTURED_TEXT_DEFAULT; + TextServer::StructuredTextParser st_parser = TextServer::STRUCTURED_TEXT_DEFAULT; Array st_args; Control::TextDirection text_direction = Control::TEXT_DIRECTION_INHERITED; bool dirty = true; @@ -124,6 +124,7 @@ private: Vector<Cell> cells; bool collapsed = false; // won't show children + bool visible = true; bool disable_folding = false; int custom_min_height = 0; @@ -134,7 +135,7 @@ private: Vector<TreeItem *> children_cache; bool is_root = false; // for tree root - Tree *tree; // tree (for reference) + Tree *tree = nullptr; // tree (for reference) TreeItem(Tree *p_tree); @@ -188,18 +189,8 @@ protected: return d; } - void _remove_child(Object *p_child) { - 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); + void _call_recursive_bind(const Variant **p_args, int p_argcount, Callable::CallError &r_error); public: /* cell mode */ @@ -212,6 +203,17 @@ public: bool is_checked(int p_column) const; bool is_indeterminate(int p_column) const; + void propagate_check(int p_column, bool p_emit_signal = true); + +private: + // Check helpers. + void _propagate_check_through_children(int p_column, bool p_checked, bool p_emit_signal); + void _propagate_check_through_parents(int p_column, bool p_emit_signal); + + TreeItem *_get_prev_visible(bool p_wrap = false); + TreeItem *_get_next_visible(bool p_wrap = false); + +public: void set_text(int p_column, String p_text); String get_text(int p_column) const; @@ -222,8 +224,8 @@ public: 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(int p_column, TextServer::StructuredTextParser p_parser); + TextServer::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; @@ -250,6 +252,7 @@ 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); @@ -274,6 +277,9 @@ public: void set_collapsed(bool p_collapsed); bool is_collapsed(); + void set_visible(bool p_visible); + bool is_visible(); + void uncollapse_tree(); void set_custom_minimum_height(int p_height); @@ -336,6 +342,7 @@ public: TreeItem *get_next_visible(bool p_wrap = false); TreeItem *get_child(int p_idx); + int get_visible_child_count(); int get_child_count(); Array get_children(); int get_index(); @@ -380,6 +387,9 @@ private: TreeItem *selected_item = nullptr; TreeItem *edited_item = nullptr; + TreeItem *popup_pressing_edited_item = nullptr; // Candidate. + int popup_pressing_edited_item_column = -1; + TreeItem *drop_mode_over = nullptr; int drop_mode_section = 0; @@ -429,18 +439,18 @@ private: bool show_column_titles = false; - VBoxContainer *popup_editor_vb; + VBoxContainer *popup_editor_vb = nullptr; - Popup *popup_editor; + Popup *popup_editor = nullptr; LineEdit *text_editor = nullptr; - HSlider *value_editor; + HSlider *value_editor = nullptr; bool updating_value_editor = false; uint64_t focus_in_id = 0; PopupMenu *popup_menu = nullptr; Vector<ColumnInfo> columns; - Timer *range_click_timer; + Timer *range_click_timer = nullptr; TreeItem *range_item_last = nullptr; bool range_up_last = false; void _range_click_timeout(); @@ -464,7 +474,7 @@ private: void _notification(int p_what); - void item_edited(int p_column, TreeItem *p_item, bool p_lmb = true); + void item_edited(int p_column, TreeItem *p_item, MouseButton p_custom_mouse_index = MouseButton::NONE); void item_changed(int p_column, TreeItem *p_item); void item_selected(int p_column, TreeItem *p_item); void item_deselected(int p_column, TreeItem *p_item); @@ -554,8 +564,8 @@ private: int _get_title_button_height() const; void _scroll_moved(float p_value); - HScrollBar *h_scroll; - VScrollBar *v_scroll; + HScrollBar *h_scroll = nullptr; + VScrollBar *v_scroll = nullptr; bool h_scroll_enabled = true; bool v_scroll_enabled = true; @@ -609,23 +619,6 @@ private: protected: static void _bind_methods(); - //bind helpers - TreeItem *_create_item(Object *p_parent, int p_idx = -1) { - return create_item(Object::cast_to<TreeItem>(p_parent), p_idx); - } - - TreeItem *_get_next_selected(Object *p_item) { - return get_next_selected(Object::cast_to<TreeItem>(p_item)); - } - - Rect2 _get_item_rect(Object *p_item, int p_column) const { - 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; @@ -691,7 +684,7 @@ public: Rect2 get_custom_popup_rect() const; int get_item_offset(TreeItem *p_item) const; - Rect2 get_item_rect(TreeItem *p_item, int p_column = -1) const; + Rect2 get_item_rect(TreeItem *p_item, int p_column = -1, int p_button = -1) const; bool edit_selected(); bool is_editing(); @@ -701,7 +694,7 @@ public: TreeItem *get_item_with_text(const String &p_find) const; Point2 get_scroll() const; - void scroll_to_item(TreeItem *p_item); + void scroll_to_item(TreeItem *p_item, bool p_center_on_item = false); void set_h_scroll_enabled(bool p_enable); bool is_h_scroll_enabled() const; void set_v_scroll_enabled(bool p_enable); diff --git a/scene/gui/video_stream_player.cpp b/scene/gui/video_stream_player.cpp index a11d56a2ed..20bc9a1028 100644 --- a/scene/gui/video_stream_player.cpp +++ b/scene/gui/video_stream_player.cpp @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 @@ int VideoStreamPlayer::_audio_mix_callback(void *p_udata, const float *p_data, i ERR_FAIL_NULL_V(p_udata, 0); ERR_FAIL_NULL_V(p_data, 0); - VideoStreamPlayer *vp = (VideoStreamPlayer *)p_udata; + VideoStreamPlayer *vp = static_cast<VideoStreamPlayer *>(p_udata); int todo = MIN(vp->resampler.get_writer_space(), p_frames); @@ -77,7 +77,7 @@ int VideoStreamPlayer::_audio_mix_callback(void *p_udata, const float *p_data, i void VideoStreamPlayer::_mix_audios(void *p_self) { ERR_FAIL_NULL(p_self); - reinterpret_cast<VideoStreamPlayer *>(p_self)->_mix_audio(); + static_cast<VideoStreamPlayer *>(p_self)->_mix_audio(); } // Called from audio thread @@ -134,7 +134,6 @@ void VideoStreamPlayer::_notification(int p_notification) { if (stream.is_valid() && autoplay && !Engine::get_singleton()->is_editor_hint()) { play(); } - } break; case NOTIFICATION_EXIT_TREE: { @@ -162,7 +161,6 @@ void VideoStreamPlayer::_notification(int p_notification) { if (!playback->is_playing()) { emit_signal(SceneStringNames::get_singleton()->finished); } - } break; case NOTIFICATION_DRAW: { @@ -175,10 +173,9 @@ void VideoStreamPlayer::_notification(int p_notification) { Size2 s = expand ? get_size() : texture->get_size(); draw_texture_rect(texture, Rect2(Point2(), s), false); - } break; - }; -}; + } +} Size2 VideoStreamPlayer::get_minimum_size() const { if (!expand && !texture.is_null()) { @@ -243,11 +240,11 @@ void VideoStreamPlayer::set_stream(const Ref<VideoStream> &p_stream) { if (!expand) { update_minimum_size(); } -}; +} Ref<VideoStream> VideoStreamPlayer::get_stream() const { return stream; -}; +} void VideoStreamPlayer::play() { ERR_FAIL_COND(!is_inside_tree()); @@ -257,10 +254,8 @@ void VideoStreamPlayer::play() { playback->stop(); playback->play(); set_process_internal(true); - // AudioServer::get_singleton()->stream_set_active(stream_rid,true); - // AudioServer::get_singleton()->stream_set_volume_scale(stream_rid,volume); last_audio_time = 0; -}; +} void VideoStreamPlayer::stop() { if (!is_inside_tree()) { @@ -271,11 +266,10 @@ void VideoStreamPlayer::stop() { } playback->stop(); - // AudioServer::get_singleton()->stream_set_active(stream_rid,false); resampler.flush(); set_process_internal(false); last_audio_time = 0; -}; +} bool VideoStreamPlayer::is_playing() const { if (playback.is_null()) { @@ -283,16 +277,16 @@ bool VideoStreamPlayer::is_playing() const { } return playback->is_playing(); -}; +} void VideoStreamPlayer::set_paused(bool p_paused) { paused = p_paused; if (playback.is_valid()) { playback->set_paused(p_paused); set_process_internal(!p_paused); - }; + } last_audio_time = 0; -}; +} bool VideoStreamPlayer::is_paused() const { return paused; @@ -316,11 +310,11 @@ int VideoStreamPlayer::get_audio_track() const { void VideoStreamPlayer::set_volume(float p_vol) { volume = p_vol; -}; +} float VideoStreamPlayer::get_volume() const { return volume; -}; +} void VideoStreamPlayer::set_volume_db(float p_db) { if (p_db < -79) { @@ -328,7 +322,7 @@ void VideoStreamPlayer::set_volume_db(float p_db) { } else { set_volume(Math::db2linear(p_db)); } -}; +} float VideoStreamPlayer::get_volume_db() const { if (volume == 0) { @@ -336,21 +330,21 @@ float VideoStreamPlayer::get_volume_db() const { } else { return Math::linear2db(volume); } -}; +} String VideoStreamPlayer::get_stream_name() const { if (stream.is_null()) { return "<No Stream>"; } return stream->get_name(); -}; +} float VideoStreamPlayer::get_stream_position() const { if (playback.is_null()) { return 0; } return playback->get_playback_position(); -}; +} void VideoStreamPlayer::set_stream_position(float p_position) { if (playback.is_valid()) { @@ -368,14 +362,14 @@ Ref<Texture2D> VideoStreamPlayer::get_video_texture() const { void VideoStreamPlayer::set_autoplay(bool p_enable) { autoplay = p_enable; -}; +} bool VideoStreamPlayer::has_autoplay() const { return autoplay; -}; +} void VideoStreamPlayer::set_bus(const StringName &p_bus) { - //if audio is active, must lock this + // If audio is active, must lock this. AudioServer::get_singleton()->lock(); bus = p_bus; AudioServer::get_singleton()->unlock(); @@ -449,8 +443,7 @@ void VideoStreamPlayer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "audio_track", PROPERTY_HINT_RANGE, "0,128,1"), "set_audio_track", "get_audio_track"); 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_db", PROPERTY_HINT_RANGE, "-80,24,0.01,suffix:dB"), "set_volume_db", "get_volume_db"); 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"); @@ -464,7 +457,5 @@ void VideoStreamPlayer::_bind_methods() { VideoStreamPlayer::VideoStreamPlayer() {} VideoStreamPlayer::~VideoStreamPlayer() { - // if (stream_rid.is_valid()) - // AudioServer::get_singleton()->free(stream_rid); - resampler.clear(); //Not necessary here, but make in consistent with other "stream_player" classes -}; + resampler.clear(); // Not necessary here, but make in consistent with other "stream_player" classes. +} diff --git a/scene/gui/video_stream_player.h b/scene/gui/video_stream_player.h index ad4a3dd9e9..130b2901f1 100644 --- a/scene/gui/video_stream_player.h +++ b/scene/gui/video_stream_player.h @@ -5,8 +5,8 @@ /* GODOT ENGINE */ /* https://godotengine.org */ /*************************************************************************/ -/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */ -/* Copyright (c) 2014-2021 Godot Engine contributors (cf. AUTHORS.md). */ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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/view_panner.cpp b/scene/gui/view_panner.cpp new file mode 100644 index 0000000000..892d0aba29 --- /dev/null +++ b/scene/gui/view_panner.cpp @@ -0,0 +1,184 @@ +/*************************************************************************/ +/* view_panner.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 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 "view_panner.h" + +#include "core/input/input.h" +#include "core/input/shortcut.h" +#include "core/os/keyboard.h" + +bool ViewPanner::gui_input(const Ref<InputEvent> &p_event, Rect2 p_canvas_rect) { + Ref<InputEventMouseButton> mb = p_event; + if (mb.is_valid()) { + Vector2 scroll_vec = Vector2((mb->get_button_index() == MouseButton::WHEEL_RIGHT) - (mb->get_button_index() == MouseButton::WHEEL_LEFT), (mb->get_button_index() == MouseButton::WHEEL_DOWN) - (mb->get_button_index() == MouseButton::WHEEL_UP)); + if (scroll_vec != Vector2()) { + if (control_scheme == SCROLL_PANS) { + if (mb->is_ctrl_pressed()) { + scroll_vec.y *= mb->get_factor(); + callback_helper(zoom_callback, varray(scroll_vec, mb->get_position(), mb->is_alt_pressed())); + return true; + } else { + Vector2 panning; + if (mb->is_shift_pressed()) { + panning.x += mb->get_factor() * scroll_vec.y; + panning.y += mb->get_factor() * scroll_vec.x; + } else { + panning.y += mb->get_factor() * scroll_vec.y; + panning.x += mb->get_factor() * scroll_vec.x; + } + callback_helper(scroll_callback, varray(panning, mb->is_alt_pressed())); + return true; + } + } else { + if (mb->is_ctrl_pressed()) { + Vector2 panning; + if (mb->is_shift_pressed()) { + panning.x += mb->get_factor() * scroll_vec.y; + panning.y += mb->get_factor() * scroll_vec.x; + } else { + panning.y += mb->get_factor() * scroll_vec.y; + panning.x += mb->get_factor() * scroll_vec.x; + } + callback_helper(scroll_callback, varray(panning, mb->is_alt_pressed())); + return true; + } else if (!mb->is_shift_pressed()) { + scroll_vec.y *= mb->get_factor(); + callback_helper(zoom_callback, varray(scroll_vec, mb->get_position(), mb->is_alt_pressed())); + return true; + } + } + } + + // Alt is not used for button presses, so ignore it. + if (mb->is_alt_pressed()) { + return false; + } + + bool is_drag_event = mb->get_button_index() == MouseButton::MIDDLE || + (enable_rmb && mb->get_button_index() == MouseButton::RIGHT) || + (!simple_panning_enabled && mb->get_button_index() == MouseButton::LEFT && is_panning()) || + (force_drag && mb->get_button_index() == MouseButton::LEFT); + + if (is_drag_event) { + if (mb->is_pressed()) { + is_dragging = true; + } else { + is_dragging = false; + } + return mb->get_button_index() != MouseButton::LEFT || mb->is_pressed(); // Don't consume LMB release events (it fixes some selection problems). + } + } + + Ref<InputEventMouseMotion> mm = p_event; + if (mm.is_valid()) { + if (is_dragging) { + if (p_canvas_rect != Rect2()) { + callback_helper(pan_callback, varray(Input::get_singleton()->warp_mouse_motion(mm, p_canvas_rect))); + } else { + callback_helper(pan_callback, varray(mm->get_relative())); + } + return true; + } + } + + Ref<InputEventKey> k = p_event; + if (k.is_valid()) { + if (pan_view_shortcut.is_valid() && pan_view_shortcut->matches_event(k)) { + pan_key_pressed = k->is_pressed(); + if (simple_panning_enabled || (Input::get_singleton()->get_mouse_button_mask() & MouseButton::LEFT) != MouseButton::NONE) { + is_dragging = pan_key_pressed; + } + return true; + } + } + + return false; +} + +void ViewPanner::release_pan_key() { + pan_key_pressed = false; + is_dragging = false; +} + +void ViewPanner::callback_helper(Callable p_callback, Vector<Variant> p_args) { + const Variant **argptr = (const Variant **)alloca(sizeof(Variant *) * p_args.size()); + for (int i = 0; i < p_args.size(); i++) { + argptr[i] = &p_args[i]; + } + + Variant result; + Callable::CallError ce; + p_callback.call(argptr, p_args.size(), result, ce); +} + +void ViewPanner::set_callbacks(Callable p_scroll_callback, Callable p_pan_callback, Callable p_zoom_callback) { + scroll_callback = p_scroll_callback; + pan_callback = p_pan_callback; + zoom_callback = p_zoom_callback; +} + +void ViewPanner::set_control_scheme(ControlScheme p_scheme) { + control_scheme = p_scheme; +} + +void ViewPanner::set_enable_rmb(bool p_enable) { + enable_rmb = p_enable; +} + +void ViewPanner::set_pan_shortcut(Ref<Shortcut> p_shortcut) { + pan_view_shortcut = p_shortcut; + pan_key_pressed = false; +} + +void ViewPanner::set_simple_panning_enabled(bool p_enabled) { + simple_panning_enabled = p_enabled; +} + +void ViewPanner::setup(ControlScheme p_scheme, Ref<Shortcut> p_shortcut, bool p_simple_panning) { + set_control_scheme(p_scheme); + set_pan_shortcut(p_shortcut); + set_simple_panning_enabled(p_simple_panning); +} + +bool ViewPanner::is_panning() const { + return is_dragging || pan_key_pressed; +} + +void ViewPanner::set_force_drag(bool p_force) { + force_drag = p_force; +} + +ViewPanner::ViewPanner() { + Array inputs; + inputs.append(InputEventKey::create_reference(Key::SPACE)); + + pan_view_shortcut.instantiate(); + pan_view_shortcut->set_events(inputs); +} diff --git a/scene/gui/view_panner.h b/scene/gui/view_panner.h new file mode 100644 index 0000000000..5b820c5f8f --- /dev/null +++ b/scene/gui/view_panner.h @@ -0,0 +1,83 @@ +/*************************************************************************/ +/* view_panner.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/*************************************************************************/ + +#ifndef VIEW_PANNER_H +#define VIEW_PANNER_H + +#include "core/object/ref_counted.h" + +class InputEvent; +class Shortcut; + +class ViewPanner : public RefCounted { + GDCLASS(ViewPanner, RefCounted); + +public: + enum ControlScheme { + SCROLL_ZOOMS, + SCROLL_PANS, + }; + +private: + bool is_dragging = false; + bool pan_key_pressed = false; + bool force_drag = false; + + bool enable_rmb = false; + bool simple_panning_enabled = false; + + Ref<Shortcut> pan_view_shortcut; + + Callable scroll_callback; + Callable pan_callback; + Callable zoom_callback; + + void callback_helper(Callable p_callback, Vector<Variant> p_args); + ControlScheme control_scheme = SCROLL_ZOOMS; + +public: + void set_callbacks(Callable p_scroll_callback, Callable p_pan_callback, Callable p_zoom_callback); + void set_control_scheme(ControlScheme p_scheme); + void set_enable_rmb(bool p_enable); + void set_pan_shortcut(Ref<Shortcut> p_shortcut); + void set_simple_panning_enabled(bool p_enabled); + + void setup(ControlScheme p_scheme, Ref<Shortcut> p_shortcut, bool p_simple_panning); + + bool is_panning() const; + void set_force_drag(bool p_force); + + bool gui_input(const Ref<InputEvent> &p_ev, Rect2 p_canvas_rect = Rect2()); + void release_pan_key(); + + ViewPanner(); +}; + +#endif // VIEW_PANNER_H |