Browse Source

Add fit_to_longest_item to OptionButton

kobewi 3 years ago
parent
commit
24d02dfb47
5 changed files with 128 additions and 39 deletions
  1. 4 0
      doc/classes/OptionButton.xml
  2. 56 37
      scene/gui/button.cpp
  3. 3 1
      scene/gui/button.h
  4. 58 1
      scene/gui/option_button.cpp
  5. 7 0
      scene/gui/option_button.h

+ 4 - 0
doc/classes/OptionButton.xml

@@ -194,6 +194,10 @@
 	<members>
 		<member name="action_mode" type="int" setter="set_action_mode" getter="get_action_mode" overrides="BaseButton" enum="BaseButton.ActionMode" default="0" />
 		<member name="alignment" type="int" setter="set_text_alignment" getter="get_text_alignment" overrides="Button" enum="HorizontalAlignment" default="0" />
+		<member name="fit_to_longest_item" type="bool" setter="set_fit_to_longest_item" getter="is_fit_to_longest_item" default="true">
+			If [code]true[/code], minimum size will be determined by the longest item's text, instead of the currently selected one's.
+			[b]Note:[/b] For performance reasons, the minimum size doesn't update immediately when adding, removing or modifying items.
+		</member>
 		<member name="item_count" type="int" setter="set_item_count" getter="get_item_count" default="0">
 			The number of items to select from.
 		</member>

+ 56 - 37
scene/gui/button.cpp

@@ -34,39 +34,14 @@
 #include "servers/rendering_server.h"
 
 Size2 Button::get_minimum_size() const {
-	Size2 minsize = text_buf->get_size();
-	if (clip_text || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING) {
-		minsize.width = 0;
-	}
-
-	if (!expand_icon) {
-		Ref<Texture2D> _icon;
-		if (icon.is_null() && has_theme_icon(SNAME("icon"))) {
-			_icon = Control::get_theme_icon(SNAME("icon"));
-		} else {
-			_icon = icon;
-		}
-
-		if (!_icon.is_null()) {
-			minsize.height = MAX(minsize.height, _icon->get_height());
-
-			if (icon_alignment != HORIZONTAL_ALIGNMENT_CENTER) {
-				minsize.width += _icon->get_width();
-				if (!xl_text.is_empty()) {
-					minsize.width += get_theme_constant(SNAME("h_separation"));
-				}
-			} else {
-				minsize.width = MAX(minsize.width, _icon->get_width());
-			}
-		}
-	}
-	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);
+	Ref<Texture2D> _icon;
+	if (icon.is_null() && has_theme_icon(SNAME("icon"))) {
+		_icon = Control::get_theme_icon(SNAME("icon"));
+	} else {
+		_icon = icon;
 	}
 
-	return get_theme_stylebox(SNAME("normal"))->get_minimum_size() + minsize;
+	return get_minimum_size_for_text_and_icon("", _icon);
 }
 
 void Button::_set_internal_margin(Side p_side, float p_value) {
@@ -352,18 +327,62 @@ void Button::_notification(int p_what) {
 	}
 }
 
-void Button::_shape() {
+Size2 Button::get_minimum_size_for_text_and_icon(const String &p_text, Ref<Texture2D> p_icon) const {
+	Ref<TextParagraph> paragraph;
+	if (p_text.is_empty()) {
+		paragraph = text_buf;
+	} else {
+		paragraph.instantiate();
+		const_cast<Button *>(this)->_shape(paragraph, p_text);
+	}
+
+	Size2 minsize = paragraph->get_size();
+	if (clip_text || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING) {
+		minsize.width = 0;
+	}
+
+	if (!expand_icon && !p_icon.is_null()) {
+		minsize.height = MAX(minsize.height, p_icon->get_height());
+
+		if (icon_alignment != HORIZONTAL_ALIGNMENT_CENTER) {
+			minsize.width += p_icon->get_width();
+			if (!xl_text.is_empty() || !p_text.is_empty()) {
+				minsize.width += get_theme_constant(SNAME("hseparation"));
+			}
+		} else {
+			minsize.width = MAX(minsize.width, p_icon->get_width());
+		}
+	}
+
+	if (!xl_text.is_empty() || !p_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;
+}
+
+void Button::_shape(Ref<TextParagraph> p_paragraph, String p_text) {
+	if (p_paragraph.is_null()) {
+		p_paragraph = text_buf;
+	}
+
+	if (p_text.is_empty()) {
+		p_text = xl_text;
+	}
+
 	Ref<Font> font = get_theme_font(SNAME("font"));
 	int font_size = get_theme_font_size(SNAME("font_size"));
 
-	text_buf->clear();
+	p_paragraph->clear();
 	if (text_direction == Control::TEXT_DIRECTION_INHERITED) {
-		text_buf->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR);
+		p_paragraph->set_direction(is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR);
 	} else {
-		text_buf->set_direction((TextServer::Direction)text_direction);
+		p_paragraph->set_direction((TextServer::Direction)text_direction);
 	}
-	text_buf->add_string(xl_text, font, font_size, language);
-	text_buf->set_text_overrun_behavior(overrun_behavior);
+	p_paragraph->add_string(p_text, font, font_size, language);
+	p_paragraph->set_text_overrun_behavior(overrun_behavior);
 }
 
 void Button::set_text_overrun_behavior(TextServer::OverrunBehavior p_behavior) {

+ 3 - 1
scene/gui/button.h

@@ -54,7 +54,7 @@ private:
 	HorizontalAlignment icon_alignment = HORIZONTAL_ALIGNMENT_LEFT;
 	float _internal_margin[4] = {};
 
-	void _shape();
+	void _shape(Ref<TextParagraph> p_paragraph = Ref<TextParagraph>(), String p_text = "");
 
 protected:
 	void _set_internal_margin(Side p_side, float p_value);
@@ -64,6 +64,8 @@ protected:
 public:
 	virtual Size2 get_minimum_size() const override;
 
+	Size2 get_minimum_size_for_text_and_icon(const String &p_text, Ref<Texture2D> p_icon) const;
+
 	void set_text(const String &p_text);
 	String get_text() const;
 

+ 58 - 1
scene/gui/option_button.cpp

@@ -35,7 +35,12 @@
 static const int NONE_SELECTED = -1;
 
 Size2 OptionButton::get_minimum_size() const {
-	Size2 minsize = Button::get_minimum_size();
+	Size2 minsize;
+	if (fit_to_longest_item) {
+		minsize = _cached_size;
+	} else {
+		minsize = Button::get_minimum_size();
+	}
 
 	if (has_theme_icon(SNAME("arrow"))) {
 		const Size2 padding = get_theme_stylebox(SNAME("normal"))->get_minimum_size();
@@ -107,6 +112,7 @@ void OptionButton::_notification(int p_what) {
 					_set_internal_margin(SIDE_RIGHT, Control::get_theme_icon(SNAME("arrow"))->get_width());
 				}
 			}
+			_refresh_size_cache();
 		} break;
 
 		case NOTIFICATION_VISIBILITY_CHANGED: {
@@ -135,6 +141,10 @@ bool OptionButton::_set(const StringName &p_name, const Variant &p_value) {
 			_select(idx, false);
 		}
 
+		if (property == "text" || property == "icon") {
+			_queue_refresh_cache();
+		}
+
 		return valid;
 	}
 	return false;
@@ -208,6 +218,7 @@ void OptionButton::add_icon_item(const Ref<Texture2D> &p_icon, const String &p_l
 	if (first_selectable) {
 		select(get_item_count() - 1);
 	}
+	_queue_refresh_cache();
 }
 
 void OptionButton::add_item(const String &p_label, int p_id) {
@@ -216,6 +227,7 @@ void OptionButton::add_item(const String &p_label, int p_id) {
 	if (first_selectable) {
 		select(get_item_count() - 1);
 	}
+	_queue_refresh_cache();
 }
 
 void OptionButton::set_item_text(int p_idx, const String &p_text) {
@@ -224,6 +236,7 @@ void OptionButton::set_item_text(int p_idx, const String &p_text) {
 	if (current == p_idx) {
 		set_text(p_text);
 	}
+	_queue_refresh_cache();
 }
 
 void OptionButton::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) {
@@ -232,6 +245,7 @@ void OptionButton::set_item_icon(int p_idx, const Ref<Texture2D> &p_icon) {
 	if (current == p_idx) {
 		set_icon(p_icon);
 	}
+	_queue_refresh_cache();
 }
 
 void OptionButton::set_item_id(int p_idx, int p_id) {
@@ -301,6 +315,7 @@ void OptionButton::set_item_count(int p_count) {
 		}
 	}
 
+	_refresh_size_cache();
 	notify_property_list_changed();
 }
 
@@ -333,6 +348,19 @@ int OptionButton::get_item_count() const {
 	return popup->get_item_count();
 }
 
+void OptionButton::set_fit_to_longest_item(bool p_fit) {
+	if (p_fit == fit_to_longest_item) {
+		return;
+	}
+	fit_to_longest_item = p_fit;
+
+	_refresh_size_cache();
+}
+
+bool OptionButton::is_fit_to_longest_item() const {
+	return fit_to_longest_item;
+}
+
 void OptionButton::add_separator(const String &p_text) {
 	popup->add_separator(p_text);
 }
@@ -341,6 +369,7 @@ void OptionButton::clear() {
 	popup->clear();
 	set_text("");
 	current = NONE_SELECTED;
+	_refresh_size_cache();
 }
 
 void OptionButton::_select(int p_which, bool p_emit) {
@@ -380,6 +409,29 @@ void OptionButton::_select_int(int p_which) {
 	_select(p_which, false);
 }
 
+void OptionButton::_refresh_size_cache() {
+	cache_refresh_pending = false;
+
+	if (!fit_to_longest_item) {
+		return;
+	}
+
+	_cached_size = Vector2();
+	for (int i = 0; i < get_item_count(); i++) {
+		_cached_size = _cached_size.max(get_minimum_size_for_text_and_icon(get_item_text(i), get_item_icon(i)));
+	}
+	update_minimum_size();
+}
+
+void OptionButton::_queue_refresh_cache() {
+	if (cache_refresh_pending) {
+		return;
+	}
+	cache_refresh_pending = true;
+
+	callable_mp(this, &OptionButton::_refresh_size_cache).call_deferredp(nullptr, 0);
+}
+
 void OptionButton::select(int p_idx) {
 	_select(p_idx, false);
 }
@@ -405,6 +457,7 @@ void OptionButton::remove_item(int p_idx) {
 	if (current == p_idx) {
 		_select(NONE_SELECTED);
 	}
+	_queue_refresh_cache();
 }
 
 PopupMenu *OptionButton::get_popup() const {
@@ -453,10 +506,13 @@ void OptionButton::_bind_methods() {
 	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));
+	ClassDB::bind_method(D_METHOD("set_fit_to_longest_item", "fit"), &OptionButton::set_fit_to_longest_item);
+	ClassDB::bind_method(D_METHOD("is_fit_to_longest_item"), &OptionButton::is_fit_to_longest_item);
 
 	// "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_PROPERTY(PropertyInfo(Variant::BOOL, "fit_to_longest_item"), "set_fit_to_longest_item", "is_fit_to_longest_item");
 	ADD_SIGNAL(MethodInfo("item_selected", PropertyInfo(Variant::INT, "index")));
 	ADD_SIGNAL(MethodInfo("item_focused", PropertyInfo(Variant::INT, "index")));
 }
@@ -482,6 +538,7 @@ OptionButton::OptionButton(const String &p_text) :
 	popup->connect("index_pressed", callable_mp(this, &OptionButton::_selected));
 	popup->connect("id_focused", callable_mp(this, &OptionButton::_focused));
 	popup->connect("popup_hide", callable_mp((BaseButton *)this, &BaseButton::set_pressed).bind(false));
+	_refresh_size_cache();
 }
 
 OptionButton::~OptionButton() {

+ 7 - 0
scene/gui/option_button.h

@@ -39,11 +39,16 @@ class OptionButton : public Button {
 
 	PopupMenu *popup = nullptr;
 	int current = -1;
+	bool fit_to_longest_item = true;
+	Vector2 _cached_size;
+	bool cache_refresh_pending = false;
 
 	void _focused(int p_which);
 	void _selected(int p_which);
 	void _select(int p_which, bool p_emit = false);
 	void _select_int(int p_which);
+	void _refresh_size_cache();
+	void _queue_refresh_cache();
 
 	virtual void pressed() override;
 
@@ -85,6 +90,8 @@ public:
 
 	void set_item_count(int p_count);
 	int get_item_count() const;
+	void set_fit_to_longest_item(bool p_fit);
+	bool is_fit_to_longest_item() const;
 
 	void add_separator(const String &p_text = "");