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
41void 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
47void AcceptDialog::_parent_focused() {
48 if (!is_exclusive() && get_flag(FLAG_POPUP)) {
49 _cancel_pressed();
50 }
51}
52
53void 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
108void 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
115void 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
124void 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
143String AcceptDialog::get_text() const {
144 return message_label->get_text();
145}
146
147void 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
160void AcceptDialog::set_hide_on_ok(bool p_hide) {
161 hide_on_ok = p_hide;
162}
163
164bool AcceptDialog::get_hide_on_ok() const {
165 return hide_on_ok;
166}
167
168void AcceptDialog::set_close_on_escape(bool p_hide) {
169 close_on_escape = p_hide;
170}
171
172bool AcceptDialog::get_close_on_escape() const {
173 return close_on_escape;
174}
175
176void AcceptDialog::set_autowrap(bool p_autowrap) {
177 message_label->set_autowrap_mode(p_autowrap ? TextServer::AUTOWRAP_WORD : TextServer::AUTOWRAP_OFF);
178}
179
180bool AcceptDialog::has_autowrap() {
181 return message_label->get_autowrap_mode() != TextServer::AUTOWRAP_OFF;
182}
183
184void 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
193String AcceptDialog::get_ok_button_text() const {
194 return ok_button->get_text();
195}
196
197void 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
205void 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
239Size2 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
277void AcceptDialog::_custom_action(const String &p_action) {
278 emit_signal(SNAME("custom_action"), p_action);
279 custom_action(p_action);
280}
281
282void 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
289Button *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
318Button *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
331void 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
363void 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
397bool AcceptDialog::swap_cancel_ok = false;
398void AcceptDialog::set_swap_cancel_ok(bool p_swap) {
399 swap_cancel_ok = p_swap;
400}
401
402AcceptDialog::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
434AcceptDialog::~AcceptDialog() {
435}
436
437// ConfirmationDialog
438
439void ConfirmationDialog::set_cancel_button_text(String p_cancel_button_text) {
440 cancel->set_text(p_cancel_button_text);
441}
442
443String ConfirmationDialog::get_cancel_button_text() const {
444 return cancel->get_text();
445}
446
447void 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
455Button *ConfirmationDialog::get_cancel_button() {
456 return cancel;
457}
458
459ConfirmationDialog::ConfirmationDialog() {
460 set_title(TTRC("Please Confirm..."));
461 set_min_size(Size2(200, 70));
462
463 cancel = add_cancel_button();
464}
465