1 | /**************************************************************************/ |
2 | /* dialogs.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 "dialogs.h" |
32 | |
33 | #include "core/os/keyboard.h" |
34 | #include "core/string/print_string.h" |
35 | #include "core/string/translation.h" |
36 | #include "scene/gui/line_edit.h" |
37 | #include "scene/theme/theme_db.h" |
38 | |
39 | // AcceptDialog |
40 | |
41 | void AcceptDialog::_input_from_window(const Ref<InputEvent> &p_event) { |
42 | if (close_on_escape && p_event->is_action_pressed(SNAME("ui_cancel" ), false, true)) { |
43 | _cancel_pressed(); |
44 | } |
45 | } |
46 | |
47 | void AcceptDialog::_parent_focused() { |
48 | if (!is_exclusive() && get_flag(FLAG_POPUP)) { |
49 | _cancel_pressed(); |
50 | } |
51 | } |
52 | |
53 | void AcceptDialog::_notification(int p_what) { |
54 | switch (p_what) { |
55 | case NOTIFICATION_POST_ENTER_TREE: { |
56 | if (is_visible()) { |
57 | get_ok_button()->grab_focus(); |
58 | } |
59 | } break; |
60 | case NOTIFICATION_VISIBILITY_CHANGED: { |
61 | if (is_visible()) { |
62 | if (get_ok_button()->is_inside_tree()) { |
63 | get_ok_button()->grab_focus(); |
64 | } |
65 | _update_child_rects(); |
66 | |
67 | parent_visible = get_parent_visible_window(); |
68 | if (parent_visible) { |
69 | parent_visible->connect("focus_entered" , callable_mp(this, &AcceptDialog::_parent_focused)); |
70 | } |
71 | } else { |
72 | if (parent_visible) { |
73 | parent_visible->disconnect("focus_entered" , callable_mp(this, &AcceptDialog::_parent_focused)); |
74 | parent_visible = nullptr; |
75 | } |
76 | } |
77 | } break; |
78 | |
79 | case NOTIFICATION_THEME_CHANGED: { |
80 | bg_panel->add_theme_style_override("panel" , theme_cache.panel_style); |
81 | |
82 | child_controls_changed(); |
83 | if (is_visible()) { |
84 | _update_child_rects(); |
85 | } |
86 | } break; |
87 | |
88 | case NOTIFICATION_EXIT_TREE: { |
89 | if (parent_visible) { |
90 | parent_visible->disconnect("focus_entered" , callable_mp(this, &AcceptDialog::_parent_focused)); |
91 | parent_visible = nullptr; |
92 | } |
93 | } break; |
94 | |
95 | case NOTIFICATION_READY: |
96 | case NOTIFICATION_WM_SIZE_CHANGED: { |
97 | if (is_visible()) { |
98 | _update_child_rects(); |
99 | } |
100 | } break; |
101 | |
102 | case NOTIFICATION_WM_CLOSE_REQUEST: { |
103 | _cancel_pressed(); |
104 | } break; |
105 | } |
106 | } |
107 | |
108 | void AcceptDialog::_text_submitted(const String &p_text) { |
109 | if (get_ok_button() && get_ok_button()->is_disabled()) { |
110 | return; // Do not allow submission if OK button is disabled. |
111 | } |
112 | _ok_pressed(); |
113 | } |
114 | |
115 | void AcceptDialog::_ok_pressed() { |
116 | if (hide_on_ok) { |
117 | set_visible(false); |
118 | } |
119 | ok_pressed(); |
120 | emit_signal(SNAME("confirmed" )); |
121 | set_input_as_handled(); |
122 | } |
123 | |
124 | void AcceptDialog::_cancel_pressed() { |
125 | Window *parent_window = parent_visible; |
126 | if (parent_visible) { |
127 | parent_visible->disconnect("focus_entered" , callable_mp(this, &AcceptDialog::_parent_focused)); |
128 | parent_visible = nullptr; |
129 | } |
130 | |
131 | call_deferred(SNAME("hide" )); |
132 | |
133 | emit_signal(SNAME("canceled" )); |
134 | |
135 | cancel_pressed(); |
136 | |
137 | if (parent_window) { |
138 | //parent_window->grab_focus(); |
139 | } |
140 | set_input_as_handled(); |
141 | } |
142 | |
143 | String AcceptDialog::get_text() const { |
144 | return message_label->get_text(); |
145 | } |
146 | |
147 | void AcceptDialog::set_text(String p_text) { |
148 | if (message_label->get_text() == p_text) { |
149 | return; |
150 | } |
151 | |
152 | message_label->set_text(p_text); |
153 | |
154 | child_controls_changed(); |
155 | if (is_visible()) { |
156 | _update_child_rects(); |
157 | } |
158 | } |
159 | |
160 | void AcceptDialog::set_hide_on_ok(bool p_hide) { |
161 | hide_on_ok = p_hide; |
162 | } |
163 | |
164 | bool AcceptDialog::get_hide_on_ok() const { |
165 | return hide_on_ok; |
166 | } |
167 | |
168 | void AcceptDialog::set_close_on_escape(bool p_hide) { |
169 | close_on_escape = p_hide; |
170 | } |
171 | |
172 | bool AcceptDialog::get_close_on_escape() const { |
173 | return close_on_escape; |
174 | } |
175 | |
176 | void AcceptDialog::set_autowrap(bool p_autowrap) { |
177 | message_label->set_autowrap_mode(p_autowrap ? TextServer::AUTOWRAP_WORD : TextServer::AUTOWRAP_OFF); |
178 | } |
179 | |
180 | bool AcceptDialog::has_autowrap() { |
181 | return message_label->get_autowrap_mode() != TextServer::AUTOWRAP_OFF; |
182 | } |
183 | |
184 | void AcceptDialog::set_ok_button_text(String p_ok_button_text) { |
185 | ok_button->set_text(p_ok_button_text); |
186 | |
187 | child_controls_changed(); |
188 | if (is_visible()) { |
189 | _update_child_rects(); |
190 | } |
191 | } |
192 | |
193 | String AcceptDialog::get_ok_button_text() const { |
194 | return ok_button->get_text(); |
195 | } |
196 | |
197 | void AcceptDialog::register_text_enter(Control *p_line_edit) { |
198 | ERR_FAIL_NULL(p_line_edit); |
199 | LineEdit *line_edit = Object::cast_to<LineEdit>(p_line_edit); |
200 | if (line_edit) { |
201 | line_edit->connect("text_submitted" , callable_mp(this, &AcceptDialog::_text_submitted)); |
202 | } |
203 | } |
204 | |
205 | void AcceptDialog::_update_child_rects() { |
206 | Size2 dlg_size = get_size(); |
207 | float h_margins = theme_cache.panel_style->get_margin(SIDE_LEFT) + theme_cache.panel_style->get_margin(SIDE_RIGHT); |
208 | float v_margins = theme_cache.panel_style->get_margin(SIDE_TOP) + theme_cache.panel_style->get_margin(SIDE_BOTTOM); |
209 | |
210 | // Fill the entire size of the window with the background. |
211 | bg_panel->set_position(Point2()); |
212 | bg_panel->set_size(dlg_size); |
213 | |
214 | // Place the buttons from the bottom edge to their minimum required size. |
215 | Size2 buttons_minsize = buttons_hbox->get_combined_minimum_size(); |
216 | Size2 buttons_size = Size2(dlg_size.x - h_margins, buttons_minsize.y); |
217 | Point2 buttons_position = Point2(theme_cache.panel_style->get_margin(SIDE_LEFT), dlg_size.y - theme_cache.panel_style->get_margin(SIDE_BOTTOM) - buttons_size.y); |
218 | buttons_hbox->set_position(buttons_position); |
219 | buttons_hbox->set_size(buttons_size); |
220 | |
221 | // Place the content from the top to fill the rest of the space (minus the separation). |
222 | Point2 content_position = Point2(theme_cache.panel_style->get_margin(SIDE_LEFT), theme_cache.panel_style->get_margin(SIDE_TOP)); |
223 | Size2 content_size = Size2(dlg_size.x - h_margins, dlg_size.y - v_margins - buttons_size.y - theme_cache.buttons_separation); |
224 | |
225 | for (int i = 0; i < get_child_count(); i++) { |
226 | Control *c = Object::cast_to<Control>(get_child(i)); |
227 | if (!c) { |
228 | continue; |
229 | } |
230 | if (c == buttons_hbox || c == bg_panel || c->is_set_as_top_level()) { |
231 | continue; |
232 | } |
233 | |
234 | c->set_position(content_position); |
235 | c->set_size(content_size); |
236 | } |
237 | } |
238 | |
239 | Size2 AcceptDialog::_get_contents_minimum_size() const { |
240 | // First, we then iterate over the label and any other custom controls |
241 | // to try and find the size that encompasses all content. |
242 | Size2 content_minsize; |
243 | for (int i = 0; i < get_child_count(); i++) { |
244 | Control *c = Object::cast_to<Control>(get_child(i)); |
245 | if (!c) { |
246 | continue; |
247 | } |
248 | |
249 | // Buttons will be included afterwards. |
250 | // The panel only displays the stylebox and doesn't contribute to the size. |
251 | if (c == buttons_hbox || c == bg_panel || c->is_set_as_top_level()) { |
252 | continue; |
253 | } |
254 | |
255 | Size2 child_minsize = c->get_combined_minimum_size(); |
256 | content_minsize.x = MAX(child_minsize.x, content_minsize.x); |
257 | content_minsize.y = MAX(child_minsize.y, content_minsize.y); |
258 | } |
259 | |
260 | // Then we take the background panel as it provides the offsets, |
261 | // which are always added to the minimum size. |
262 | if (theme_cache.panel_style.is_valid()) { |
263 | content_minsize += theme_cache.panel_style->get_minimum_size(); |
264 | } |
265 | |
266 | // Then we add buttons. Horizontally we're interested in whichever |
267 | // value is the biggest. Vertically buttons add to the overall size. |
268 | Size2 buttons_minsize = buttons_hbox->get_combined_minimum_size(); |
269 | content_minsize.x = MAX(buttons_minsize.x, content_minsize.x); |
270 | content_minsize.y += buttons_minsize.y; |
271 | // Plus there is a separation size added on top. |
272 | content_minsize.y += theme_cache.buttons_separation; |
273 | |
274 | return content_minsize; |
275 | } |
276 | |
277 | void AcceptDialog::_custom_action(const String &p_action) { |
278 | emit_signal(SNAME("custom_action" ), p_action); |
279 | custom_action(p_action); |
280 | } |
281 | |
282 | void AcceptDialog::_custom_button_visibility_changed(Button *button) { |
283 | Control *right_spacer = Object::cast_to<Control>(button->get_meta("__right_spacer" )); |
284 | if (right_spacer) { |
285 | right_spacer->set_visible(button->is_visible()); |
286 | } |
287 | } |
288 | |
289 | Button *AcceptDialog::add_button(const String &p_text, bool p_right, const String &p_action) { |
290 | Button *button = memnew(Button); |
291 | button->set_text(p_text); |
292 | |
293 | Control *right_spacer; |
294 | if (p_right) { |
295 | buttons_hbox->add_child(button); |
296 | right_spacer = buttons_hbox->add_spacer(); |
297 | } else { |
298 | buttons_hbox->add_child(button); |
299 | buttons_hbox->move_child(button, 0); |
300 | right_spacer = buttons_hbox->add_spacer(true); |
301 | } |
302 | button->set_meta("__right_spacer" , right_spacer); |
303 | |
304 | button->connect("visibility_changed" , callable_mp(this, &AcceptDialog::_custom_button_visibility_changed).bind(button)); |
305 | |
306 | child_controls_changed(); |
307 | if (is_visible()) { |
308 | _update_child_rects(); |
309 | } |
310 | |
311 | if (!p_action.is_empty()) { |
312 | button->connect("pressed" , callable_mp(this, &AcceptDialog::_custom_action).bind(p_action)); |
313 | } |
314 | |
315 | return button; |
316 | } |
317 | |
318 | Button *AcceptDialog::add_cancel_button(const String &p_cancel) { |
319 | String c = p_cancel; |
320 | if (p_cancel.is_empty()) { |
321 | c = "Cancel" ; |
322 | } |
323 | |
324 | Button *b = swap_cancel_ok ? add_button(c, true) : add_button(c); |
325 | |
326 | b->connect("pressed" , callable_mp(this, &AcceptDialog::_cancel_pressed)); |
327 | |
328 | return b; |
329 | } |
330 | |
331 | void AcceptDialog::remove_button(Control *p_button) { |
332 | Button *button = Object::cast_to<Button>(p_button); |
333 | ERR_FAIL_NULL(button); |
334 | ERR_FAIL_COND_MSG(button->get_parent() != buttons_hbox, vformat("Cannot remove button %s as it does not belong to this dialog." , button->get_name())); |
335 | ERR_FAIL_COND_MSG(button == ok_button, "Cannot remove dialog's OK button." ); |
336 | |
337 | Control *right_spacer = Object::cast_to<Control>(button->get_meta("__right_spacer" )); |
338 | if (right_spacer) { |
339 | ERR_FAIL_COND_MSG(right_spacer->get_parent() != buttons_hbox, vformat("Cannot remove button %s as its associated spacer does not belong to this dialog." , button->get_name())); |
340 | } |
341 | |
342 | button->disconnect("visibility_changed" , callable_mp(this, &AcceptDialog::_custom_button_visibility_changed)); |
343 | if (button->is_connected("pressed" , callable_mp(this, &AcceptDialog::_custom_action))) { |
344 | button->disconnect("pressed" , callable_mp(this, &AcceptDialog::_custom_action)); |
345 | } |
346 | if (button->is_connected("pressed" , callable_mp(this, &AcceptDialog::_cancel_pressed))) { |
347 | button->disconnect("pressed" , callable_mp(this, &AcceptDialog::_cancel_pressed)); |
348 | } |
349 | |
350 | if (right_spacer) { |
351 | buttons_hbox->remove_child(right_spacer); |
352 | button->remove_meta("__right_spacer" ); |
353 | right_spacer->queue_free(); |
354 | } |
355 | buttons_hbox->remove_child(button); |
356 | |
357 | child_controls_changed(); |
358 | if (is_visible()) { |
359 | _update_child_rects(); |
360 | } |
361 | } |
362 | |
363 | void AcceptDialog::_bind_methods() { |
364 | ClassDB::bind_method(D_METHOD("get_ok_button" ), &AcceptDialog::get_ok_button); |
365 | ClassDB::bind_method(D_METHOD("get_label" ), &AcceptDialog::get_label); |
366 | ClassDB::bind_method(D_METHOD("set_hide_on_ok" , "enabled" ), &AcceptDialog::set_hide_on_ok); |
367 | ClassDB::bind_method(D_METHOD("get_hide_on_ok" ), &AcceptDialog::get_hide_on_ok); |
368 | ClassDB::bind_method(D_METHOD("set_close_on_escape" , "enabled" ), &AcceptDialog::set_close_on_escape); |
369 | ClassDB::bind_method(D_METHOD("get_close_on_escape" ), &AcceptDialog::get_close_on_escape); |
370 | ClassDB::bind_method(D_METHOD("add_button" , "text" , "right" , "action" ), &AcceptDialog::add_button, DEFVAL(false), DEFVAL("" )); |
371 | ClassDB::bind_method(D_METHOD("add_cancel_button" , "name" ), &AcceptDialog::add_cancel_button); |
372 | ClassDB::bind_method(D_METHOD("remove_button" , "button" ), &AcceptDialog::remove_button); |
373 | ClassDB::bind_method(D_METHOD("register_text_enter" , "line_edit" ), &AcceptDialog::register_text_enter); |
374 | ClassDB::bind_method(D_METHOD("set_text" , "text" ), &AcceptDialog::set_text); |
375 | ClassDB::bind_method(D_METHOD("get_text" ), &AcceptDialog::get_text); |
376 | ClassDB::bind_method(D_METHOD("set_autowrap" , "autowrap" ), &AcceptDialog::set_autowrap); |
377 | ClassDB::bind_method(D_METHOD("has_autowrap" ), &AcceptDialog::has_autowrap); |
378 | ClassDB::bind_method(D_METHOD("set_ok_button_text" , "text" ), &AcceptDialog::set_ok_button_text); |
379 | ClassDB::bind_method(D_METHOD("get_ok_button_text" ), &AcceptDialog::get_ok_button_text); |
380 | |
381 | ADD_SIGNAL(MethodInfo("confirmed" )); |
382 | ADD_SIGNAL(MethodInfo("canceled" )); |
383 | ADD_SIGNAL(MethodInfo("custom_action" , PropertyInfo(Variant::STRING_NAME, "action" ))); |
384 | |
385 | ADD_PROPERTY(PropertyInfo(Variant::STRING, "ok_button_text" ), "set_ok_button_text" , "get_ok_button_text" ); |
386 | |
387 | ADD_GROUP("Dialog" , "dialog_" ); |
388 | ADD_PROPERTY(PropertyInfo(Variant::STRING, "dialog_text" , PROPERTY_HINT_MULTILINE_TEXT), "set_text" , "get_text" ); |
389 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dialog_hide_on_ok" ), "set_hide_on_ok" , "get_hide_on_ok" ); |
390 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dialog_close_on_escape" ), "set_close_on_escape" , "get_close_on_escape" ); |
391 | ADD_PROPERTY(PropertyInfo(Variant::BOOL, "dialog_autowrap" ), "set_autowrap" , "has_autowrap" ); |
392 | |
393 | BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, AcceptDialog, panel_style, "panel" ); |
394 | BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, AcceptDialog, buttons_separation); |
395 | } |
396 | |
397 | bool AcceptDialog::swap_cancel_ok = false; |
398 | void AcceptDialog::set_swap_cancel_ok(bool p_swap) { |
399 | swap_cancel_ok = p_swap; |
400 | } |
401 | |
402 | AcceptDialog::AcceptDialog() { |
403 | set_wrap_controls(true); |
404 | set_visible(false); |
405 | set_transient(true); |
406 | set_exclusive(true); |
407 | set_clamp_to_embedder(true); |
408 | |
409 | bg_panel = memnew(Panel); |
410 | add_child(bg_panel, false, INTERNAL_MODE_FRONT); |
411 | |
412 | buttons_hbox = memnew(HBoxContainer); |
413 | |
414 | message_label = memnew(Label); |
415 | message_label->set_anchor(SIDE_RIGHT, Control::ANCHOR_END); |
416 | message_label->set_anchor(SIDE_BOTTOM, Control::ANCHOR_END); |
417 | add_child(message_label, false, INTERNAL_MODE_FRONT); |
418 | |
419 | add_child(buttons_hbox, false, INTERNAL_MODE_FRONT); |
420 | |
421 | buttons_hbox->add_spacer(); |
422 | ok_button = memnew(Button); |
423 | ok_button->set_text("OK" ); |
424 | buttons_hbox->add_child(ok_button); |
425 | buttons_hbox->add_spacer(); |
426 | |
427 | ok_button->connect("pressed" , callable_mp(this, &AcceptDialog::_ok_pressed)); |
428 | |
429 | set_title(TTRC("Alert!" )); |
430 | |
431 | connect("window_input" , callable_mp(this, &AcceptDialog::_input_from_window)); |
432 | } |
433 | |
434 | AcceptDialog::~AcceptDialog() { |
435 | } |
436 | |
437 | // ConfirmationDialog |
438 | |
439 | void ConfirmationDialog::set_cancel_button_text(String p_cancel_button_text) { |
440 | cancel->set_text(p_cancel_button_text); |
441 | } |
442 | |
443 | String ConfirmationDialog::get_cancel_button_text() const { |
444 | return cancel->get_text(); |
445 | } |
446 | |
447 | void ConfirmationDialog::_bind_methods() { |
448 | ClassDB::bind_method(D_METHOD("get_cancel_button" ), &ConfirmationDialog::get_cancel_button); |
449 | ClassDB::bind_method(D_METHOD("set_cancel_button_text" , "text" ), &ConfirmationDialog::set_cancel_button_text); |
450 | ClassDB::bind_method(D_METHOD("get_cancel_button_text" ), &ConfirmationDialog::get_cancel_button_text); |
451 | |
452 | ADD_PROPERTY(PropertyInfo(Variant::STRING, "cancel_button_text" ), "set_cancel_button_text" , "get_cancel_button_text" ); |
453 | } |
454 | |
455 | Button *ConfirmationDialog::get_cancel_button() { |
456 | return cancel; |
457 | } |
458 | |
459 | ConfirmationDialog::ConfirmationDialog() { |
460 | set_title(TTRC("Please Confirm..." )); |
461 | set_min_size(Size2(200, 70)); |
462 | |
463 | cancel = add_cancel_button(); |
464 | } |
465 | |