Repository: godotengine/godot (language: cpp)

Issue:
Setting SpinBox value with signal gets overwritten by the LineEdit's text value when the LineEdit is about to lose focus.
### Tested versions

- Reproducible in v4.4.dev7.official [46c8f8c5c]
- Reproducible in v4.3.stable.official [77dcf97d8]
- Reproducible in v4.2.2.stable.official [15073afe3]

### System information

Godot v4.3.stable - Windows 10.0.19045 - GLES3 (Compatibility) - NVIDIA GeForce GTX 750 Ti (NVIDIA; 32.0.15.6094) - Intel(R) Core(TM) i5-6400 CPU @ 2.70GHz (4 Threads)

### Issue description

When setting a SpinBox value with a signal **when the SpinBox's LineEdit still has focus** and is about to lose focus the value is first set correctly but then is set again to the LineEdit's displayed value. If the value is set with `set_value_no_signal` to prevent emmiting a signal, the SpinBox will emit the `value_changed` signal with the incorrect value.

https://github.com/user-attachments/assets/32618d3e-910a-418e-b913-0fc2db197b9d


### Steps to reproduce

Using the MRP:

1. Click on the SpinBox's LineEdit.
2. Click on the LineEdit next to it (Or any other node. This is just so that the SpinBox loses focus)
3. The value will be set correctly (and a signal emmited depending on what method to set the value was used)
4. The value will then be set to whatever the SpinBox's LineEdit is displaying instead of the set value and a signal will be emmited.

The MRP has prints to inform of the values the moment before and after they are set.

I've seen that the issue only occours if the value is set right before the SpinBox loses focus.

The issue is prevented if you ALSO set the SpinBox's value to its TextBox:
`spin_box.get_line_edit().text = str(spin_box.value)`

### Minimal reproduction project (MRP)

[debugging.zip](https://github.com/user-attachments/files/18256719/debugging.zip)



Discussion / hints:

Fix patch:
diff --git a/scene/gui/spin_box.cpp b/scene/gui/spin_box.cpp
index ffe6e2bb9785..e76170623955 100644
--- a/scene/gui/spin_box.cpp
+++ b/scene/gui/spin_box.cpp
@@ -40,7 +40,7 @@ Size2 SpinBox::get_minimum_size() const {
 	return ms;
 }
 
-void SpinBox::_update_text(bool p_keep_line_edit) {
+void SpinBox::_update_text(bool p_only_update_if_value_changed) {
 	double step = get_step();
 	if (use_custom_arrow_step && custom_arrow_step != 0.0) {
 		step = custom_arrow_step;
@@ -50,6 +50,11 @@ void SpinBox::_update_text(bool p_keep_line_edit) {
 		value = TS->format_number(value);
 	}
 
+	if (p_only_update_if_value_changed && value == last_text_value) {
+		return;
+	}
+	last_text_value = value;
+
 	if (!line_edit->is_editing()) {
 		if (!prefix.is_empty()) {
 			value = prefix + " " + value;
@@ -58,17 +63,12 @@ void SpinBox::_update_text(bool p_keep_line_edit) {
 			value += " " + suffix;
 		}
 	}
-
-	if (p_keep_line_edit && value == last_updated_text && value != line_edit->get_text()) {
-		return;
-	}
-
 	line_edit->set_text_with_selection(value);
-	last_updated_text = value;
 }
 
 void SpinBox::_text_submitted(const String &p_string) {
 	if (p_string.is_empty()) {
+		_update_text();
 		return;
 	}
 
@@ -288,14 +288,12 @@ void SpinBox::_line_edit_editing_toggled(bool p_toggled_on) {
 			line_edit->select_all();
 		}
 	} else {
-		// Discontinue because the focus_exit was caused by canceling or the text is empty.
 		if (Input::get_singleton()->is_action_pressed("ui_cancel") || line_edit->get_text().is_empty()) {
-			_update_text();
-			return;
+			_update_text(); // Revert text if editing was canceled.
+		} else {
+			_update_text(true); // Update text in case value was changed this frame (e.g. on `focus_exited`).
+			_text_submitted(line_edit->get_text());
 		}
-
-		line_edit->deselect();
-		_text_submitted(line_edit->get_text());
 	}
 }
 
diff --git a/scene/gui/spin_box.h b/scene/gui/spin_box.h
index 564294649c94..79076d3f4692 100644
--- a/scene/gui/spin_box.h
+++ b/scene/gui/spin_box.h
@@ -58,13 +58,13 @@ class SpinBox : public Range {
 	void _range_click_timeout();
 	void _release_mouse_from_drag_mode();
 
-	void _update_text(bool p_keep_line_edit = false);
+	void _update_text(bool p_only_update_if_value_changed = false);
 	void _text_submitted(const String &p_string);
 	void _text_changed(const String &p_string);
 
 	String prefix;
 	String suffix;
-	String last_updated_text;
+	String last_text_value;
 	double custom_arrow_step = 0.0;
 	bool use_custom_arrow_step = false;
 


Test patch: