| 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 | |