1 | /**************************************************************************/ |
2 | /* spin_box.cpp */ |
3 | /**************************************************************************/ |
4 | /* This file is part of: */ |
5 | /* GODOT ENGINE */ |
6 | /* https://godotengine.org */ |
7 | /**************************************************************************/ |
8 | /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ |
9 | /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ |
10 | /* */ |
11 | /* Permission is hereby granted, free of charge, to any person obtaining */ |
12 | /* a copy of this software and associated documentation files (the */ |
13 | /* "Software"), to deal in the Software without restriction, including */ |
14 | /* without limitation the rights to use, copy, modify, merge, publish, */ |
15 | /* distribute, sublicense, and/or sell copies of the Software, and to */ |
16 | /* permit persons to whom the Software is furnished to do so, subject to */ |
17 | /* the following conditions: */ |
18 | /* */ |
19 | /* The above copyright notice and this permission notice shall be */ |
20 | /* included in all copies or substantial portions of the Software. */ |
21 | /* */ |
22 | /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ |
23 | /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ |
24 | /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ |
25 | /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ |
26 | /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ |
27 | /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ |
28 | /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ |
29 | /**************************************************************************/ |
30 | |
31 | #include "spin_box.h" |
32 | |
33 | #include "core/input/input.h" |
34 | #include "core/math/expression.h" |
35 | #include "scene/theme/theme_db.h" |
36 | |
37 | Size2 SpinBox::get_minimum_size() const { |
38 | Size2 ms = line_edit->get_combined_minimum_size(); |
39 | ms.width += last_w; |
40 | return ms; |
41 | } |
42 | |
43 | void SpinBox::_update_text() { |
44 | String value = String::num(get_value(), Math::range_step_decimals(get_step())); |
45 | if (is_localizing_numeral_system()) { |
46 | value = TS->format_number(value); |
47 | } |
48 | |
49 | if (!line_edit->has_focus()) { |
50 | if (!prefix.is_empty()) { |
51 | value = prefix + " " + value; |
52 | } |
53 | if (!suffix.is_empty()) { |
54 | value += " " + suffix; |
55 | } |
56 | } |
57 | |
58 | line_edit->set_text_with_selection(value); |
59 | } |
60 | |
61 | void SpinBox::_text_submitted(const String &p_string) { |
62 | Ref<Expression> expr; |
63 | expr.instantiate(); |
64 | |
65 | String num = TS->parse_number(p_string); |
66 | // Ignore the prefix and suffix in the expression. |
67 | Error err = expr->parse(num.trim_prefix(prefix + " " ).trim_suffix(" " + suffix)); |
68 | if (err != OK) { |
69 | _update_text(); |
70 | return; |
71 | } |
72 | |
73 | Variant value = expr->execute(Array(), nullptr, false, true); |
74 | if (value.get_type() != Variant::NIL) { |
75 | set_value(value); |
76 | } |
77 | _update_text(); |
78 | } |
79 | |
80 | void SpinBox::_text_changed(const String &p_string) { |
81 | int cursor_pos = line_edit->get_caret_column(); |
82 | |
83 | _text_submitted(p_string); |
84 | |
85 | // Line edit 'set_text' method resets the cursor position so we need to undo that. |
86 | line_edit->set_caret_column(cursor_pos); |
87 | } |
88 | |
89 | LineEdit *SpinBox::get_line_edit() { |
90 | return line_edit; |
91 | } |
92 | |
93 | void SpinBox::_line_edit_input(const Ref<InputEvent> &p_event) { |
94 | } |
95 | |
96 | void SpinBox::_range_click_timeout() { |
97 | if (!drag.enabled && Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) { |
98 | bool up = get_local_mouse_position().y < (get_size().height / 2); |
99 | double step = get_custom_arrow_step() != 0.0 ? get_custom_arrow_step() : get_step(); |
100 | set_value(get_value() + (up ? step : -step)); |
101 | |
102 | if (range_click_timer->is_one_shot()) { |
103 | range_click_timer->set_wait_time(0.075); |
104 | range_click_timer->set_one_shot(false); |
105 | range_click_timer->start(); |
106 | } |
107 | |
108 | } else { |
109 | range_click_timer->stop(); |
110 | } |
111 | } |
112 | |
113 | void SpinBox::_release_mouse() { |
114 | if (drag.enabled) { |
115 | drag.enabled = false; |
116 | Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_HIDDEN); |
117 | warp_mouse(drag.capture_pos); |
118 | Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_VISIBLE); |
119 | } |
120 | } |
121 | |
122 | void SpinBox::gui_input(const Ref<InputEvent> &p_event) { |
123 | ERR_FAIL_COND(p_event.is_null()); |
124 | |
125 | if (!is_editable()) { |
126 | return; |
127 | } |
128 | |
129 | Ref<InputEventMouseButton> mb = p_event; |
130 | |
131 | double step = get_custom_arrow_step() != 0.0 ? get_custom_arrow_step() : get_step(); |
132 | |
133 | if (mb.is_valid() && mb->is_pressed()) { |
134 | bool up = mb->get_position().y < (get_size().height / 2); |
135 | |
136 | switch (mb->get_button_index()) { |
137 | case MouseButton::LEFT: { |
138 | line_edit->grab_focus(); |
139 | |
140 | set_value(get_value() + (up ? step : -step)); |
141 | |
142 | range_click_timer->set_wait_time(0.6); |
143 | range_click_timer->set_one_shot(true); |
144 | range_click_timer->start(); |
145 | |
146 | drag.allowed = true; |
147 | drag.capture_pos = mb->get_position(); |
148 | } break; |
149 | case MouseButton::RIGHT: { |
150 | line_edit->grab_focus(); |
151 | set_value((up ? get_max() : get_min())); |
152 | } break; |
153 | case MouseButton::WHEEL_UP: { |
154 | if (line_edit->has_focus()) { |
155 | set_value(get_value() + step * mb->get_factor()); |
156 | accept_event(); |
157 | } |
158 | } break; |
159 | case MouseButton::WHEEL_DOWN: { |
160 | if (line_edit->has_focus()) { |
161 | set_value(get_value() - step * mb->get_factor()); |
162 | accept_event(); |
163 | } |
164 | } break; |
165 | default: |
166 | break; |
167 | } |
168 | } |
169 | |
170 | if (mb.is_valid() && !mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) { |
171 | //set_default_cursor_shape(CURSOR_ARROW); |
172 | range_click_timer->stop(); |
173 | _release_mouse(); |
174 | drag.allowed = false; |
175 | line_edit->clear_pending_select_all_on_focus(); |
176 | } |
177 | |
178 | Ref<InputEventMouseMotion> mm = p_event; |
179 | |
180 | if (mm.is_valid() && (mm->get_button_mask().has_flag(MouseButtonMask::LEFT))) { |
181 | if (drag.enabled) { |
182 | drag.diff_y += mm->get_relative().y; |
183 | double diff_y = -0.01 * Math::pow(ABS(drag.diff_y), 1.8) * SIGN(drag.diff_y); |
184 | set_value(CLAMP(drag.base_val + step * diff_y, get_min(), get_max())); |
185 | } else if (drag.allowed && drag.capture_pos.distance_to(mm->get_position()) > 2) { |
186 | Input::get_singleton()->set_mouse_mode(Input::MOUSE_MODE_CAPTURED); |
187 | drag.enabled = true; |
188 | drag.base_val = get_value(); |
189 | drag.diff_y = 0; |
190 | } |
191 | } |
192 | } |
193 | |
194 | void SpinBox::_line_edit_focus_enter() { |
195 | int col = line_edit->get_caret_column(); |
196 | _update_text(); |
197 | line_edit->set_caret_column(col); |
198 | |
199 | // LineEdit text might change and it clears any selection. Have to re-select here. |
200 | if (line_edit->is_select_all_on_focus() && !Input::get_singleton()->is_mouse_button_pressed(MouseButton::LEFT)) { |
201 | line_edit->select_all(); |
202 | } |
203 | } |
204 | |
205 | void SpinBox::_line_edit_focus_exit() { |
206 | // Discontinue because the focus_exit was caused by left-clicking the arrows. |
207 | const Viewport *viewport = get_viewport(); |
208 | if (!viewport || viewport->gui_get_focus_owner() == get_line_edit()) { |
209 | return; |
210 | } |
211 | // Discontinue because the focus_exit was caused by right-click context menu. |
212 | if (line_edit->is_menu_visible()) { |
213 | return; |
214 | } |
215 | // Discontinue because the focus_exit was caused by canceling. |
216 | if (Input::get_singleton()->is_action_pressed("ui_cancel" )) { |
217 | _update_text(); |
218 | return; |
219 | } |
220 | |
221 | _text_submitted(line_edit->get_text()); |
222 | } |
223 | |
224 | inline void SpinBox::_adjust_width_for_icon(const Ref<Texture2D> &icon) { |
225 | int w = icon->get_width(); |
226 | if ((w != last_w)) { |
227 | line_edit->set_offset(SIDE_LEFT, 0); |
228 | line_edit->set_offset(SIDE_RIGHT, -w); |
229 | last_w = w; |
230 | } |
231 | } |
232 | |
233 | void SpinBox::_notification(int p_what) { |
234 | switch (p_what) { |
235 | case NOTIFICATION_DRAW: { |
236 | _update_text(); |
237 | _adjust_width_for_icon(theme_cache.updown_icon); |
238 | |
239 | RID ci = get_canvas_item(); |
240 | Size2i size = get_size(); |
241 | |
242 | if (is_layout_rtl()) { |
243 | theme_cache.updown_icon->draw(ci, Point2i(0, (size.height - theme_cache.updown_icon->get_height()) / 2)); |
244 | } else { |
245 | theme_cache.updown_icon->draw(ci, Point2i(size.width - theme_cache.updown_icon->get_width(), (size.height - theme_cache.updown_icon->get_height()) / 2)); |
246 | } |
247 | } break; |
248 | |
249 | case NOTIFICATION_ENTER_TREE: { |
250 | _adjust_width_for_icon(theme_cache.updown_icon); |
251 | _update_text(); |
252 | } break; |
253 | |
254 | case NOTIFICATION_EXIT_TREE: { |
255 | _release_mouse(); |
256 | } break; |
257 | |
258 | case NOTIFICATION_TRANSLATION_CHANGED: { |
259 | queue_redraw(); |
260 | } break; |
261 | |
262 | case NOTIFICATION_THEME_CHANGED: { |
263 | call_deferred(SNAME("update_minimum_size" )); |
264 | get_line_edit()->call_deferred(SNAME("update_minimum_size" )); |
265 | } break; |
266 | |
267 | case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: { |
268 | queue_redraw(); |
269 | } break; |
270 | } |
271 | } |
272 | |
273 | void SpinBox::set_horizontal_alignment(HorizontalAlignment p_alignment) { |
274 | line_edit->set_horizontal_alignment(p_alignment); |
275 | } |
276 | |
277 | HorizontalAlignment SpinBox::get_horizontal_alignment() const { |
278 | return line_edit->get_horizontal_alignment(); |
279 | } |
280 | |
281 | void SpinBox::set_suffix(const String &p_suffix) { |
282 | if (suffix == p_suffix) { |
283 | return; |
284 | } |
285 | |
286 | suffix = p_suffix; |
287 | _update_text(); |
288 | } |
289 | |
290 | String SpinBox::get_suffix() const { |
291 | return suffix; |
292 | } |
293 | |
294 | void SpinBox::set_prefix(const String &p_prefix) { |
295 | if (prefix == p_prefix) { |
296 | return; |
297 | } |
298 | |
299 | prefix = p_prefix; |
300 | _update_text(); |
301 | } |
302 | |
303 | String SpinBox::get_prefix() const { |
304 | return prefix; |
305 | } |
306 | |
307 | void SpinBox::set_update_on_text_changed(bool p_enabled) { |
308 | if (update_on_text_changed == p_enabled) { |
309 | return; |
310 | } |
311 | |
312 | update_on_text_changed = p_enabled; |
313 | |
314 | if (p_enabled) { |
315 | line_edit->connect("text_changed" , callable_mp(this, &SpinBox::_text_changed), CONNECT_DEFERRED); |
316 | } else { |
317 | line_edit->disconnect("text_changed" , callable_mp(this, &SpinBox::_text_changed)); |
318 | } |
319 | } |
320 | |
321 | bool SpinBox::get_update_on_text_changed() const { |
322 | return update_on_text_changed; |
323 | } |
324 | |
325 | void SpinBox::set_select_all_on_focus(bool p_enabled) { |
326 | line_edit->set_select_all_on_focus(p_enabled); |
327 | } |
328 | |
329 | bool SpinBox::is_select_all_on_focus() const { |
330 | return line_edit->is_select_all_on_focus(); |
331 | } |
332 | |
333 | void SpinBox::set_editable(bool p_enabled) { |
334 | line_edit->set_editable(p_enabled); |
335 | } |
336 | |
337 | bool SpinBox::is_editable() const { |
338 | return line_edit->is_editable(); |
339 | } |
340 | |
341 | void SpinBox::apply() { |
342 | _text_submitted(line_edit->get_text()); |
343 | } |
344 | |
345 | void SpinBox::set_custom_arrow_step(double p_custom_arrow_step) { |
346 | custom_arrow_step = p_custom_arrow_step; |
347 | } |
348 | |
349 | double SpinBox::get_custom_arrow_step() const { |
350 | return custom_arrow_step; |
351 | } |
352 | |
353 | void SpinBox::_bind_methods() { |
354 | ClassDB::bind_method(D_METHOD("set_horizontal_alignment" , "alignment" ), &SpinBox::set_horizontal_alignment); |
355 | ClassDB::bind_method(D_METHOD("get_horizontal_alignment" ), &SpinBox::get_horizontal_alignment); |
356 | ClassDB::bind_method(D_METHOD("set_suffix" , "suffix" ), &SpinBox::set_suffix); |
357 | ClassDB::bind_method(D_METHOD("get_suffix" ), &SpinBox::get_suffix); |
358 | ClassDB::bind_method(D_METHOD("set_prefix" , "prefix" ), &SpinBox::set_prefix); |
359 | ClassDB::bind_method(D_METHOD("get_prefix" ), &SpinBox::get_prefix); |
360 | ClassDB::bind_method(D_METHOD("set_editable" , "enabled" ), &SpinBox::set_editable); |
361 | ClassDB::bind_method(D_METHOD("set_custom_arrow_step" , "arrow_step" ), &SpinBox::set_custom_arrow_step); |
362 | ClassDB::bind_method(D_METHOD("get_custom_arrow_step" ), &SpinBox::get_custom_arrow_step); |
363 | ClassDB::bind_method(D_METHOD("is_editable" ), &SpinBox::is_editable); |
364 | ClassDB::bind_method(D_METHOD("set_update_on_text_changed" , "enabled" ), &SpinBox::set_update_on_text_changed); |
365 | ClassDB::bind_method(D_METHOD("get_update_on_text_changed" ), &SpinBox::get_update_on_text_changed); |
366 | ClassDB::bind_method(D_METHOD("set_select_all_on_focus" , "enabled" ), &SpinBox::set_select_all_on_focus); |
367 | ClassDB::bind_method(D_METHOD("is_select_all_on_focus" ), &SpinBox::is_select_all_on_focus); |
368 | ClassDB::bind_method(D_METHOD("apply" ), &SpinBox::apply); |
369 | ClassDB::bind_method(D_METHOD("get_line_edit" ), &SpinBox::get_line_edit); |
370 | |
371 | ADD_PROPERTY(PropertyInfo(Variant::INT, "alignment" , PROPERTY_HINT_ENUM, "Left,Center,Right,Fill" ), "set_horizontal_alignment" , "get_horizontal_alignment" ); |
372 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "editable" ), "set_editable" , "is_editable" ); |
373 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "update_on_text_changed" ), "set_update_on_text_changed" , "get_update_on_text_changed" ); |
374 | ADD_PROPERTY(PropertyInfo(Variant::STRING, "prefix" ), "set_prefix" , "get_prefix" ); |
375 | ADD_PROPERTY(PropertyInfo(Variant::STRING, "suffix" ), "set_suffix" , "get_suffix" ); |
376 | ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "custom_arrow_step" , PROPERTY_HINT_RANGE, "0,10000,0.0001,or_greater" ), "set_custom_arrow_step" , "get_custom_arrow_step" ); |
377 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "select_all_on_focus" ), "set_select_all_on_focus" , "is_select_all_on_focus" ); |
378 | |
379 | BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, SpinBox, updown_icon, "updown" ); |
380 | } |
381 | |
382 | SpinBox::SpinBox() { |
383 | line_edit = memnew(LineEdit); |
384 | add_child(line_edit, false, INTERNAL_MODE_FRONT); |
385 | |
386 | line_edit->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT); |
387 | line_edit->set_mouse_filter(MOUSE_FILTER_PASS); |
388 | line_edit->set_horizontal_alignment(HORIZONTAL_ALIGNMENT_LEFT); |
389 | |
390 | line_edit->connect("text_submitted" , callable_mp(this, &SpinBox::_text_submitted), CONNECT_DEFERRED); |
391 | line_edit->connect("focus_entered" , callable_mp(this, &SpinBox::_line_edit_focus_enter), CONNECT_DEFERRED); |
392 | line_edit->connect("focus_exited" , callable_mp(this, &SpinBox::_line_edit_focus_exit), CONNECT_DEFERRED); |
393 | line_edit->connect("gui_input" , callable_mp(this, &SpinBox::_line_edit_input)); |
394 | |
395 | range_click_timer = memnew(Timer); |
396 | range_click_timer->connect("timeout" , callable_mp(this, &SpinBox::_range_click_timeout)); |
397 | add_child(range_click_timer, false, INTERNAL_MODE_FRONT); |
398 | } |
399 | |