1/**************************************************************************/
2/* control_editor_plugin.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 "control_editor_plugin.h"
32
33#include "editor/editor_node.h"
34#include "editor/editor_scale.h"
35#include "editor/editor_settings.h"
36#include "editor/editor_string_names.h"
37#include "editor/editor_undo_redo_manager.h"
38#include "editor/plugins/canvas_item_editor_plugin.h"
39#include "scene/gui/grid_container.h"
40#include "scene/gui/separator.h"
41
42// Inspector controls.
43
44void ControlPositioningWarning::_update_warning() {
45 if (!control_node) {
46 title_icon->set_texture(nullptr);
47 title_label->set_text("");
48 hint_label->set_text("");
49 return;
50 }
51
52 Node *parent_node = control_node->get_parent_control();
53 if (!parent_node) {
54 title_icon->set_texture(get_editor_theme_icon(SNAME("SubViewport")));
55 title_label->set_text(TTR("This node doesn't have a control parent."));
56 hint_label->set_text(TTR("Use the appropriate layout properties depending on where you are going to put it."));
57 } else if (Object::cast_to<Container>(parent_node)) {
58 title_icon->set_texture(get_editor_theme_icon(SNAME("ContainerLayout")));
59 title_label->set_text(TTR("This node is a child of a container."));
60 hint_label->set_text(TTR("Use container properties for positioning."));
61 } else {
62 title_icon->set_texture(get_editor_theme_icon(SNAME("ControlLayout")));
63 title_label->set_text(TTR("This node is a child of a regular control."));
64 hint_label->set_text(TTR("Use anchors and the rectangle for positioning."));
65 }
66
67 bg_panel->add_theme_style_override("panel", get_theme_stylebox(SNAME("bg_group_note"), SNAME("EditorProperty")));
68}
69
70void ControlPositioningWarning::_update_toggler() {
71 Ref<Texture2D> arrow;
72 if (hint_label->is_visible()) {
73 arrow = get_theme_icon(SNAME("arrow"), SNAME("Tree"));
74 set_tooltip_text(TTR("Collapse positioning hint."));
75 } else {
76 if (is_layout_rtl()) {
77 arrow = get_theme_icon(SNAME("arrow_collapsed"), SNAME("Tree"));
78 } else {
79 arrow = get_theme_icon(SNAME("arrow_collapsed_mirrored"), SNAME("Tree"));
80 }
81 set_tooltip_text(TTR("Expand positioning hint."));
82 }
83
84 hint_icon->set_texture(arrow);
85}
86
87void ControlPositioningWarning::set_control(Control *p_node) {
88 control_node = p_node;
89 _update_warning();
90}
91
92void ControlPositioningWarning::gui_input(const Ref<InputEvent> &p_event) {
93 Ref<InputEventMouseButton> mb = p_event;
94 if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
95 bool state = !hint_label->is_visible();
96
97 hint_filler_left->set_visible(state);
98 hint_label->set_visible(state);
99 hint_filler_right->set_visible(state);
100
101 _update_toggler();
102 }
103}
104
105void ControlPositioningWarning::_notification(int p_notification) {
106 switch (p_notification) {
107 case NOTIFICATION_ENTER_TREE:
108 case NOTIFICATION_THEME_CHANGED:
109 _update_warning();
110 _update_toggler();
111 break;
112 }
113}
114
115ControlPositioningWarning::ControlPositioningWarning() {
116 set_mouse_filter(MOUSE_FILTER_STOP);
117
118 bg_panel = memnew(PanelContainer);
119 bg_panel->set_mouse_filter(MOUSE_FILTER_IGNORE);
120 add_child(bg_panel);
121
122 grid = memnew(GridContainer);
123 grid->set_columns(3);
124 bg_panel->add_child(grid);
125
126 title_icon = memnew(TextureRect);
127 title_icon->set_stretch_mode(TextureRect::StretchMode::STRETCH_KEEP_CENTERED);
128 grid->add_child(title_icon);
129
130 title_label = memnew(Label);
131 title_label->set_autowrap_mode(TextServer::AutowrapMode::AUTOWRAP_WORD);
132 title_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
133 title_label->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
134 grid->add_child(title_label);
135
136 hint_icon = memnew(TextureRect);
137 hint_icon->set_stretch_mode(TextureRect::StretchMode::STRETCH_KEEP_CENTERED);
138 grid->add_child(hint_icon);
139
140 // Filler.
141 hint_filler_left = memnew(Control);
142 hint_filler_left->hide();
143 grid->add_child(hint_filler_left);
144
145 hint_label = memnew(Label);
146 hint_label->set_autowrap_mode(TextServer::AutowrapMode::AUTOWRAP_WORD);
147 hint_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
148 hint_label->set_vertical_alignment(VerticalAlignment::VERTICAL_ALIGNMENT_CENTER);
149 hint_label->hide();
150 grid->add_child(hint_label);
151
152 // Filler.
153 hint_filler_right = memnew(Control);
154 hint_filler_right->hide();
155 grid->add_child(hint_filler_right);
156}
157
158void EditorPropertyAnchorsPreset::_set_read_only(bool p_read_only) {
159 options->set_disabled(p_read_only);
160};
161
162void EditorPropertyAnchorsPreset::_option_selected(int p_which) {
163 int64_t val = options->get_item_metadata(p_which);
164 emit_changed(get_edited_property(), val);
165}
166
167void EditorPropertyAnchorsPreset::update_property() {
168 int64_t which = get_edited_property_value();
169
170 for (int i = 0; i < options->get_item_count(); i++) {
171 Variant val = options->get_item_metadata(i);
172 if (val != Variant() && which == (int64_t)val) {
173 options->select(i);
174 return;
175 }
176 }
177}
178
179void EditorPropertyAnchorsPreset::setup(const Vector<String> &p_options) {
180 options->clear();
181
182 Vector<String> split_after;
183 split_after.append("Custom");
184 split_after.append("PresetFullRect");
185 split_after.append("PresetBottomLeft");
186 split_after.append("PresetCenter");
187
188 for (int i = 0, j = 0; i < p_options.size(); i++, j++) {
189 Vector<String> text_split = p_options[i].split(":");
190 int64_t current_val = text_split[1].to_int();
191
192 String option_name = text_split[0];
193 if (option_name.begins_with("Preset")) {
194 String preset_name = option_name.trim_prefix("Preset");
195 String humanized_name = preset_name.capitalize();
196 String icon_name = "ControlAlign" + preset_name;
197 options->add_icon_item(EditorNode::get_singleton()->get_editor_theme()->get_icon(icon_name, EditorStringName(EditorIcons)), humanized_name);
198 } else {
199 options->add_item(option_name);
200 }
201
202 options->set_item_metadata(j, current_val);
203 if (split_after.has(option_name)) {
204 options->add_separator();
205 j++;
206 }
207 }
208}
209
210EditorPropertyAnchorsPreset::EditorPropertyAnchorsPreset() {
211 options = memnew(OptionButton);
212 options->set_clip_text(true);
213 options->set_flat(true);
214 add_child(options);
215 add_focusable(options);
216 options->connect("item_selected", callable_mp(this, &EditorPropertyAnchorsPreset::_option_selected));
217}
218
219void EditorPropertySizeFlags::_set_read_only(bool p_read_only) {
220 for (CheckBox *check : flag_checks) {
221 check->set_disabled(p_read_only);
222 }
223 flag_presets->set_disabled(p_read_only);
224};
225
226void EditorPropertySizeFlags::_preset_selected(int p_which) {
227 int preset = flag_presets->get_item_id(p_which);
228 if (preset == SIZE_FLAGS_PRESET_CUSTOM) {
229 flag_options->set_visible(true);
230 return;
231 }
232 flag_options->set_visible(false);
233
234 uint32_t value = 0;
235 switch (preset) {
236 case SIZE_FLAGS_PRESET_FILL:
237 value = Control::SIZE_FILL;
238 break;
239 case SIZE_FLAGS_PRESET_SHRINK_BEGIN:
240 value = Control::SIZE_SHRINK_BEGIN;
241 break;
242 case SIZE_FLAGS_PRESET_SHRINK_CENTER:
243 value = Control::SIZE_SHRINK_CENTER;
244 break;
245 case SIZE_FLAGS_PRESET_SHRINK_END:
246 value = Control::SIZE_SHRINK_END;
247 break;
248 }
249
250 bool is_expand = flag_expand->is_visible() && flag_expand->is_pressed();
251 if (is_expand) {
252 value |= Control::SIZE_EXPAND;
253 }
254
255 emit_changed(get_edited_property(), value);
256}
257
258void EditorPropertySizeFlags::_expand_toggled() {
259 uint32_t value = get_edited_property_value();
260
261 if (flag_expand->is_visible() && flag_expand->is_pressed()) {
262 value |= Control::SIZE_EXPAND;
263 } else {
264 value ^= Control::SIZE_EXPAND;
265 }
266
267 // Keep the custom preset selected as we toggle individual flags.
268 keep_selected_preset = true;
269 emit_changed(get_edited_property(), value);
270}
271
272void EditorPropertySizeFlags::_flag_toggled() {
273 uint32_t value = 0;
274 for (int i = 0; i < flag_checks.size(); i++) {
275 if (flag_checks[i]->is_pressed()) {
276 int flag_value = flag_checks[i]->get_meta("_value");
277 value |= flag_value;
278 }
279 }
280
281 bool is_expand = flag_expand->is_visible() && flag_expand->is_pressed();
282 if (is_expand) {
283 value |= Control::SIZE_EXPAND;
284 }
285
286 // Keep the custom preset selected as we toggle individual flags.
287 keep_selected_preset = true;
288 emit_changed(get_edited_property(), value);
289}
290
291void EditorPropertySizeFlags::update_property() {
292 uint32_t value = get_edited_property_value();
293
294 for (int i = 0; i < flag_checks.size(); i++) {
295 int flag_value = flag_checks[i]->get_meta("_value");
296 if (value & flag_value) {
297 flag_checks[i]->set_pressed(true);
298 } else {
299 flag_checks[i]->set_pressed(false);
300 }
301 }
302
303 bool is_expand = value & Control::SIZE_EXPAND;
304 flag_expand->set_pressed(is_expand);
305
306 if (keep_selected_preset) {
307 keep_selected_preset = false;
308 return;
309 }
310
311 FlagPreset preset = SIZE_FLAGS_PRESET_CUSTOM;
312 if (value == Control::SIZE_FILL || value == (Control::SIZE_FILL | Control::SIZE_EXPAND)) {
313 preset = SIZE_FLAGS_PRESET_FILL;
314 } else if (value == Control::SIZE_SHRINK_BEGIN || value == (Control::SIZE_SHRINK_BEGIN | Control::SIZE_EXPAND)) {
315 preset = SIZE_FLAGS_PRESET_SHRINK_BEGIN;
316 } else if (value == Control::SIZE_SHRINK_CENTER || value == (Control::SIZE_SHRINK_CENTER | Control::SIZE_EXPAND)) {
317 preset = SIZE_FLAGS_PRESET_SHRINK_CENTER;
318 } else if (value == Control::SIZE_SHRINK_END || value == (Control::SIZE_SHRINK_END | Control::SIZE_EXPAND)) {
319 preset = SIZE_FLAGS_PRESET_SHRINK_END;
320 }
321
322 int preset_idx = flag_presets->get_item_index(preset);
323 if (preset_idx >= 0) {
324 flag_presets->select(preset_idx);
325 }
326 flag_options->set_visible(preset == SIZE_FLAGS_PRESET_CUSTOM);
327}
328
329void EditorPropertySizeFlags::setup(const Vector<String> &p_options, bool p_vertical) {
330 vertical = p_vertical;
331
332 if (p_options.size() == 0) {
333 flag_presets->clear();
334 flag_presets->add_item(TTR("Container Default"));
335 flag_presets->set_disabled(true);
336 flag_expand->set_visible(false);
337 return;
338 }
339
340 HashMap<int, String> flags;
341 for (int i = 0, j = 0; i < p_options.size(); i++, j++) {
342 Vector<String> text_split = p_options[i].split(":");
343 int64_t current_val = text_split[1].to_int();
344 flags[current_val] = text_split[0];
345
346 if (current_val == SIZE_EXPAND) {
347 continue;
348 }
349
350 CheckBox *cb = memnew(CheckBox);
351 cb->set_text(text_split[0]);
352 cb->set_clip_text(true);
353 cb->set_meta("_value", current_val);
354 cb->connect("pressed", callable_mp(this, &EditorPropertySizeFlags::_flag_toggled));
355 add_focusable(cb);
356
357 flag_options->add_child(cb);
358 flag_checks.append(cb);
359 }
360
361 Control *gui_base = EditorNode::get_singleton()->get_gui_base();
362 String wide_preset_icon = SNAME("ControlAlignHCenterWide");
363 String begin_preset_icon = SNAME("ControlAlignCenterLeft");
364 String end_preset_icon = SNAME("ControlAlignCenterRight");
365 if (vertical) {
366 wide_preset_icon = SNAME("ControlAlignVCenterWide");
367 begin_preset_icon = SNAME("ControlAlignCenterTop");
368 end_preset_icon = SNAME("ControlAlignCenterBottom");
369 }
370
371 flag_presets->clear();
372 if (flags.has(SIZE_FILL)) {
373 flag_presets->add_icon_item(gui_base->get_editor_theme_icon(wide_preset_icon), TTR("Fill"), SIZE_FLAGS_PRESET_FILL);
374 }
375 // Shrink Begin is the same as no flags at all, as such it cannot be disabled.
376 flag_presets->add_icon_item(gui_base->get_editor_theme_icon(begin_preset_icon), TTR("Shrink Begin"), SIZE_FLAGS_PRESET_SHRINK_BEGIN);
377 if (flags.has(SIZE_SHRINK_CENTER)) {
378 flag_presets->add_icon_item(gui_base->get_editor_theme_icon(SNAME("ControlAlignCenter")), TTR("Shrink Center"), SIZE_FLAGS_PRESET_SHRINK_CENTER);
379 }
380 if (flags.has(SIZE_SHRINK_END)) {
381 flag_presets->add_icon_item(gui_base->get_editor_theme_icon(end_preset_icon), TTR("Shrink End"), SIZE_FLAGS_PRESET_SHRINK_END);
382 }
383 flag_presets->add_separator();
384 flag_presets->add_item(TTR("Custom"), SIZE_FLAGS_PRESET_CUSTOM);
385
386 flag_expand->set_visible(flags.has(SIZE_EXPAND));
387}
388
389EditorPropertySizeFlags::EditorPropertySizeFlags() {
390 VBoxContainer *vb = memnew(VBoxContainer);
391 add_child(vb);
392
393 flag_presets = memnew(OptionButton);
394 flag_presets->set_clip_text(true);
395 flag_presets->set_flat(true);
396 vb->add_child(flag_presets);
397 add_focusable(flag_presets);
398 set_label_reference(flag_presets);
399 flag_presets->connect("item_selected", callable_mp(this, &EditorPropertySizeFlags::_preset_selected));
400
401 flag_options = memnew(VBoxContainer);
402 flag_options->hide();
403 vb->add_child(flag_options);
404
405 flag_expand = memnew(CheckBox);
406 flag_expand->set_text(TTR("Expand"));
407 vb->add_child(flag_expand);
408 add_focusable(flag_expand);
409 flag_expand->connect("pressed", callable_mp(this, &EditorPropertySizeFlags::_expand_toggled));
410}
411
412bool EditorInspectorPluginControl::can_handle(Object *p_object) {
413 return Object::cast_to<Control>(p_object) != nullptr;
414}
415
416void EditorInspectorPluginControl::parse_group(Object *p_object, const String &p_group) {
417 Control *control = Object::cast_to<Control>(p_object);
418 if (!control || p_group != "Layout") {
419 return;
420 }
421
422 ControlPositioningWarning *pos_warning = memnew(ControlPositioningWarning);
423 pos_warning->set_control(control);
424 add_custom_control(pos_warning);
425}
426
427bool EditorInspectorPluginControl::parse_property(Object *p_object, const Variant::Type p_type, const String &p_path, const PropertyHint p_hint, const String &p_hint_text, const BitField<PropertyUsageFlags> p_usage, const bool p_wide) {
428 Control *control = Object::cast_to<Control>(p_object);
429 if (!control) {
430 return false;
431 }
432
433 if (p_path == "anchors_preset") {
434 EditorPropertyAnchorsPreset *prop_editor = memnew(EditorPropertyAnchorsPreset);
435 Vector<String> options = p_hint_text.split(",");
436 prop_editor->setup(options);
437 add_property_editor(p_path, prop_editor);
438
439 return true;
440 }
441
442 if (p_path == "size_flags_horizontal" || p_path == "size_flags_vertical") {
443 EditorPropertySizeFlags *prop_editor = memnew(EditorPropertySizeFlags);
444 Vector<String> options;
445 if (!p_hint_text.is_empty()) {
446 options = p_hint_text.split(",");
447 }
448 prop_editor->setup(options, p_path == "size_flags_vertical");
449 add_property_editor(p_path, prop_editor);
450
451 return true;
452 }
453
454 return false;
455}
456
457// Toolbars controls.
458
459Size2 ControlEditorPopupButton::get_minimum_size() const {
460 Vector2 base_size = Vector2(26, 26) * EDSCALE;
461
462 if (arrow_icon.is_null()) {
463 return base_size;
464 }
465
466 Vector2 final_size;
467 final_size.x = base_size.x + arrow_icon->get_width();
468 final_size.y = MAX(base_size.y, arrow_icon->get_height());
469
470 return final_size;
471}
472
473void ControlEditorPopupButton::toggled(bool p_pressed) {
474 if (!p_pressed) {
475 return;
476 }
477
478 Size2 size = get_size() * get_viewport()->get_canvas_transform().get_scale();
479
480 popup_panel->set_size(Size2(size.width, 0));
481 Point2 gp = get_screen_position();
482 gp.y += size.y;
483 if (is_layout_rtl()) {
484 gp.x += size.width - popup_panel->get_size().width;
485 }
486 popup_panel->set_position(gp);
487
488 popup_panel->popup();
489}
490
491void ControlEditorPopupButton::_popup_visibility_changed(bool p_visible) {
492 set_pressed(p_visible);
493}
494
495void ControlEditorPopupButton::_notification(int p_what) {
496 switch (p_what) {
497 case NOTIFICATION_ENTER_TREE:
498 case NOTIFICATION_THEME_CHANGED: {
499 arrow_icon = get_theme_icon("select_arrow", "Tree");
500 } break;
501
502 case NOTIFICATION_DRAW: {
503 if (arrow_icon.is_valid()) {
504 Vector2 arrow_pos = Point2(26, 0) * EDSCALE;
505 arrow_pos.y = get_size().y / 2 - arrow_icon->get_height() / 2;
506 draw_texture(arrow_icon, arrow_pos);
507 }
508 } break;
509
510 case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
511 popup_panel->set_layout_direction((Window::LayoutDirection)get_layout_direction());
512 } break;
513
514 case NOTIFICATION_VISIBILITY_CHANGED: {
515 if (!is_visible_in_tree()) {
516 popup_panel->hide();
517 }
518 } break;
519 }
520}
521
522ControlEditorPopupButton::ControlEditorPopupButton() {
523 set_flat(true);
524 set_toggle_mode(true);
525 set_focus_mode(FOCUS_NONE);
526
527 popup_panel = memnew(PopupPanel);
528 popup_panel->set_theme_type_variation("ControlEditorPopupPanel");
529 add_child(popup_panel);
530 popup_panel->connect("about_to_popup", callable_mp(this, &ControlEditorPopupButton::_popup_visibility_changed).bind(true));
531 popup_panel->connect("popup_hide", callable_mp(this, &ControlEditorPopupButton::_popup_visibility_changed).bind(false));
532
533 popup_vbox = memnew(VBoxContainer);
534 popup_panel->add_child(popup_vbox);
535}
536
537void ControlEditorPresetPicker::_add_row_button(HBoxContainer *p_row, const int p_preset, const String &p_name) {
538 ERR_FAIL_COND(preset_buttons.has(p_preset));
539
540 Button *b = memnew(Button);
541 b->set_custom_minimum_size(Size2i(36, 36) * EDSCALE);
542 b->set_icon_alignment(HORIZONTAL_ALIGNMENT_CENTER);
543 b->set_tooltip_text(p_name);
544 b->set_flat(true);
545 p_row->add_child(b);
546 b->connect("pressed", callable_mp(this, &ControlEditorPresetPicker::_preset_button_pressed).bind(p_preset));
547
548 preset_buttons[p_preset] = b;
549}
550
551void ControlEditorPresetPicker::_add_separator(BoxContainer *p_box, Separator *p_separator) {
552 p_separator->add_theme_constant_override("separation", grid_separation);
553 p_separator->set_custom_minimum_size(Size2i(1, 1));
554 p_box->add_child(p_separator);
555}
556
557void AnchorPresetPicker::_preset_button_pressed(const int p_preset) {
558 emit_signal("anchors_preset_selected", p_preset);
559}
560
561void AnchorPresetPicker::_notification(int p_notification) {
562 switch (p_notification) {
563 case NOTIFICATION_ENTER_TREE:
564 case NOTIFICATION_THEME_CHANGED: {
565 preset_buttons[PRESET_TOP_LEFT]->set_icon(get_editor_theme_icon(SNAME("ControlAlignTopLeft")));
566 preset_buttons[PRESET_CENTER_TOP]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenterTop")));
567 preset_buttons[PRESET_TOP_RIGHT]->set_icon(get_editor_theme_icon(SNAME("ControlAlignTopRight")));
568
569 preset_buttons[PRESET_CENTER_LEFT]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenterLeft")));
570 preset_buttons[PRESET_CENTER]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenter")));
571 preset_buttons[PRESET_CENTER_RIGHT]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenterRight")));
572
573 preset_buttons[PRESET_BOTTOM_LEFT]->set_icon(get_editor_theme_icon(SNAME("ControlAlignBottomLeft")));
574 preset_buttons[PRESET_CENTER_BOTTOM]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenterBottom")));
575 preset_buttons[PRESET_BOTTOM_RIGHT]->set_icon(get_editor_theme_icon(SNAME("ControlAlignBottomRight")));
576
577 preset_buttons[PRESET_TOP_WIDE]->set_icon(get_editor_theme_icon(SNAME("ControlAlignTopWide")));
578 preset_buttons[PRESET_HCENTER_WIDE]->set_icon(get_editor_theme_icon(SNAME("ControlAlignHCenterWide")));
579 preset_buttons[PRESET_BOTTOM_WIDE]->set_icon(get_editor_theme_icon(SNAME("ControlAlignBottomWide")));
580
581 preset_buttons[PRESET_LEFT_WIDE]->set_icon(get_editor_theme_icon(SNAME("ControlAlignLeftWide")));
582 preset_buttons[PRESET_VCENTER_WIDE]->set_icon(get_editor_theme_icon(SNAME("ControlAlignVCenterWide")));
583 preset_buttons[PRESET_RIGHT_WIDE]->set_icon(get_editor_theme_icon(SNAME("ControlAlignRightWide")));
584
585 preset_buttons[PRESET_FULL_RECT]->set_icon(get_editor_theme_icon(SNAME("ControlAlignFullRect")));
586 } break;
587 }
588}
589
590void AnchorPresetPicker::_bind_methods() {
591 ADD_SIGNAL(MethodInfo("anchors_preset_selected", PropertyInfo(Variant::INT, "preset")));
592}
593
594AnchorPresetPicker::AnchorPresetPicker() {
595 VBoxContainer *main_vb = memnew(VBoxContainer);
596 main_vb->add_theme_constant_override("separation", grid_separation);
597 add_child(main_vb);
598
599 HBoxContainer *top_row = memnew(HBoxContainer);
600 top_row->set_alignment(BoxContainer::ALIGNMENT_CENTER);
601 top_row->add_theme_constant_override("separation", grid_separation);
602 main_vb->add_child(top_row);
603
604 _add_row_button(top_row, PRESET_TOP_LEFT, TTR("Top Left"));
605 _add_row_button(top_row, PRESET_CENTER_TOP, TTR("Center Top"));
606 _add_row_button(top_row, PRESET_TOP_RIGHT, TTR("Top Right"));
607 _add_separator(top_row, memnew(VSeparator));
608 _add_row_button(top_row, PRESET_TOP_WIDE, TTR("Top Wide"));
609
610 HBoxContainer *mid_row = memnew(HBoxContainer);
611 mid_row->set_alignment(BoxContainer::ALIGNMENT_CENTER);
612 mid_row->add_theme_constant_override("separation", grid_separation);
613 main_vb->add_child(mid_row);
614
615 _add_row_button(mid_row, PRESET_CENTER_LEFT, TTR("Center Left"));
616 _add_row_button(mid_row, PRESET_CENTER, TTR("Center"));
617 _add_row_button(mid_row, PRESET_CENTER_RIGHT, TTR("Center Right"));
618 _add_separator(mid_row, memnew(VSeparator));
619 _add_row_button(mid_row, PRESET_HCENTER_WIDE, TTR("HCenter Wide"));
620
621 HBoxContainer *bot_row = memnew(HBoxContainer);
622 bot_row->set_alignment(BoxContainer::ALIGNMENT_CENTER);
623 bot_row->add_theme_constant_override("separation", grid_separation);
624 main_vb->add_child(bot_row);
625
626 _add_row_button(bot_row, PRESET_BOTTOM_LEFT, TTR("Bottom Left"));
627 _add_row_button(bot_row, PRESET_CENTER_BOTTOM, TTR("Center Bottom"));
628 _add_row_button(bot_row, PRESET_BOTTOM_RIGHT, TTR("Bottom Right"));
629 _add_separator(bot_row, memnew(VSeparator));
630 _add_row_button(bot_row, PRESET_BOTTOM_WIDE, TTR("Bottom Wide"));
631
632 _add_separator(main_vb, memnew(HSeparator));
633
634 HBoxContainer *extra_row = memnew(HBoxContainer);
635 extra_row->set_alignment(BoxContainer::ALIGNMENT_CENTER);
636 extra_row->add_theme_constant_override("separation", grid_separation);
637 main_vb->add_child(extra_row);
638
639 _add_row_button(extra_row, PRESET_LEFT_WIDE, TTR("Left Wide"));
640 _add_row_button(extra_row, PRESET_VCENTER_WIDE, TTR("VCenter Wide"));
641 _add_row_button(extra_row, PRESET_RIGHT_WIDE, TTR("Right Wide"));
642 _add_separator(extra_row, memnew(VSeparator));
643 _add_row_button(extra_row, PRESET_FULL_RECT, TTR("Full Rect"));
644}
645
646void SizeFlagPresetPicker::_preset_button_pressed(const int p_preset) {
647 int flags = (SizeFlags)p_preset;
648 if (expand_button->is_pressed()) {
649 flags |= SIZE_EXPAND;
650 }
651
652 emit_signal("size_flags_selected", flags);
653}
654
655void SizeFlagPresetPicker::set_allowed_flags(Vector<SizeFlags> &p_flags) {
656 preset_buttons[SIZE_SHRINK_BEGIN]->set_disabled(!p_flags.has(SIZE_SHRINK_BEGIN));
657 preset_buttons[SIZE_SHRINK_CENTER]->set_disabled(!p_flags.has(SIZE_SHRINK_CENTER));
658 preset_buttons[SIZE_SHRINK_END]->set_disabled(!p_flags.has(SIZE_SHRINK_END));
659 preset_buttons[SIZE_FILL]->set_disabled(!p_flags.has(SIZE_FILL));
660
661 expand_button->set_disabled(!p_flags.has(SIZE_EXPAND));
662 if (p_flags.has(SIZE_EXPAND)) {
663 expand_button->set_tooltip_text(TTR("Enable to also set the Expand flag.\nDisable to only set Shrink/Fill flags."));
664 } else {
665 expand_button->set_pressed(false);
666 expand_button->set_tooltip_text(TTR("Some parents of the selected nodes do not support the Expand flag."));
667 }
668}
669
670void SizeFlagPresetPicker::_notification(int p_notification) {
671 switch (p_notification) {
672 case NOTIFICATION_ENTER_TREE:
673 case NOTIFICATION_THEME_CHANGED: {
674 if (vertical) {
675 preset_buttons[SIZE_SHRINK_BEGIN]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenterTop")));
676 preset_buttons[SIZE_SHRINK_CENTER]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenter")));
677 preset_buttons[SIZE_SHRINK_END]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenterBottom")));
678
679 preset_buttons[SIZE_FILL]->set_icon(get_editor_theme_icon(SNAME("ControlAlignVCenterWide")));
680 } else {
681 preset_buttons[SIZE_SHRINK_BEGIN]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenterLeft")));
682 preset_buttons[SIZE_SHRINK_CENTER]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenter")));
683 preset_buttons[SIZE_SHRINK_END]->set_icon(get_editor_theme_icon(SNAME("ControlAlignCenterRight")));
684
685 preset_buttons[SIZE_FILL]->set_icon(get_editor_theme_icon(SNAME("ControlAlignHCenterWide")));
686 }
687 } break;
688 }
689}
690
691void SizeFlagPresetPicker::_bind_methods() {
692 ADD_SIGNAL(MethodInfo("size_flags_selected", PropertyInfo(Variant::INT, "size_flags")));
693}
694
695SizeFlagPresetPicker::SizeFlagPresetPicker(bool p_vertical) {
696 vertical = p_vertical;
697
698 VBoxContainer *main_vb = memnew(VBoxContainer);
699 add_child(main_vb);
700
701 HBoxContainer *main_row = memnew(HBoxContainer);
702 main_row->set_alignment(BoxContainer::ALIGNMENT_CENTER);
703 main_row->add_theme_constant_override("separation", grid_separation);
704 main_vb->add_child(main_row);
705
706 _add_row_button(main_row, SIZE_SHRINK_BEGIN, TTR("Shrink Begin"));
707 _add_row_button(main_row, SIZE_SHRINK_CENTER, TTR("Shrink Center"));
708 _add_row_button(main_row, SIZE_SHRINK_END, TTR("Shrink End"));
709 _add_separator(main_row, memnew(VSeparator));
710 _add_row_button(main_row, SIZE_FILL, TTR("Fill"));
711
712 expand_button = memnew(CheckBox);
713 expand_button->set_flat(true);
714 expand_button->set_text(TTR("Align with Expand"));
715 expand_button->set_tooltip_text(TTR("Enable to also set the Expand flag.\nDisable to only set Shrink/Fill flags."));
716 main_vb->add_child(expand_button);
717}
718
719// Toolbar.
720
721void ControlEditorToolbar::_anchors_preset_selected(int p_preset) {
722 LayoutPreset preset = (LayoutPreset)p_preset;
723 List<Node *> selection = editor_selection->get_selected_node_list();
724
725 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
726 undo_redo->create_action(TTR("Change Anchors, Offsets, Grow Direction"));
727
728 for (Node *E : selection) {
729 Control *control = Object::cast_to<Control>(E);
730 if (control) {
731 undo_redo->add_do_property(control, "layout_mode", LayoutMode::LAYOUT_MODE_ANCHORS);
732 undo_redo->add_do_property(control, "anchors_preset", preset);
733 undo_redo->add_undo_method(control, "_edit_set_state", control->_edit_get_state());
734 }
735 }
736
737 undo_redo->commit_action();
738
739 anchors_mode = false;
740 anchor_mode_button->set_pressed(anchors_mode);
741}
742
743void ControlEditorToolbar::_anchors_to_current_ratio() {
744 List<Node *> selection = editor_selection->get_selected_node_list();
745
746 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
747 undo_redo->create_action(TTR("Change Anchors, Offsets (Keep Ratio)"));
748
749 for (Node *E : selection) {
750 Control *control = Object::cast_to<Control>(E);
751 if (control) {
752 Point2 top_left_anchor = _position_to_anchor(control, Point2());
753 Point2 bottom_right_anchor = _position_to_anchor(control, control->get_size());
754 undo_redo->add_do_method(control, "set_anchor", SIDE_LEFT, top_left_anchor.x, false, true);
755 undo_redo->add_do_method(control, "set_anchor", SIDE_RIGHT, bottom_right_anchor.x, false, true);
756 undo_redo->add_do_method(control, "set_anchor", SIDE_TOP, top_left_anchor.y, false, true);
757 undo_redo->add_do_method(control, "set_anchor", SIDE_BOTTOM, bottom_right_anchor.y, false, true);
758 undo_redo->add_do_method(control, "set_meta", "_edit_use_anchors_", true);
759
760 const bool use_anchors = control->get_meta("_edit_use_anchors_", false);
761 undo_redo->add_undo_method(control, "_edit_set_state", control->_edit_get_state());
762 if (use_anchors) {
763 undo_redo->add_undo_method(control, "set_meta", "_edit_use_anchors_", true);
764 } else {
765 undo_redo->add_undo_method(control, "remove_meta", "_edit_use_anchors_");
766 }
767
768 anchors_mode = true;
769 anchor_mode_button->set_pressed(anchors_mode);
770 }
771 }
772
773 undo_redo->commit_action();
774}
775
776void ControlEditorToolbar::_anchor_mode_toggled(bool p_status) {
777 List<Control *> selection = _get_edited_controls();
778 for (Control *E : selection) {
779 if (Object::cast_to<Container>(E->get_parent())) {
780 continue;
781 }
782
783 if (p_status) {
784 E->set_meta("_edit_use_anchors_", true);
785 } else {
786 E->remove_meta("_edit_use_anchors_");
787 }
788 }
789
790 anchors_mode = p_status;
791 CanvasItemEditor::get_singleton()->update_viewport();
792}
793
794void ControlEditorToolbar::_container_flags_selected(int p_flags, bool p_vertical) {
795 List<Node *> selection = editor_selection->get_selected_node_list();
796
797 EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton();
798 if (p_vertical) {
799 undo_redo->create_action(TTR("Change Vertical Size Flags"));
800 } else {
801 undo_redo->create_action(TTR("Change Horizontal Size Flags"));
802 }
803
804 for (Node *E : selection) {
805 Control *control = Object::cast_to<Control>(E);
806 if (control) {
807 if (p_vertical) {
808 undo_redo->add_do_method(control, "set_v_size_flags", p_flags);
809 } else {
810 undo_redo->add_do_method(control, "set_h_size_flags", p_flags);
811 }
812 undo_redo->add_undo_method(control, "_edit_set_state", control->_edit_get_state());
813 }
814 }
815
816 undo_redo->commit_action();
817}
818
819Vector2 ControlEditorToolbar::_position_to_anchor(const Control *p_control, Vector2 position) {
820 ERR_FAIL_NULL_V(p_control, Vector2());
821
822 Rect2 parent_rect = p_control->get_parent_anchorable_rect();
823
824 Vector2 output;
825 if (p_control->is_layout_rtl()) {
826 output.x = (parent_rect.size.x == 0) ? 0.0 : (parent_rect.size.x - p_control->get_transform().xform(position).x - parent_rect.position.x) / parent_rect.size.x;
827 } else {
828 output.x = (parent_rect.size.x == 0) ? 0.0 : (p_control->get_transform().xform(position).x - parent_rect.position.x) / parent_rect.size.x;
829 }
830 output.y = (parent_rect.size.y == 0) ? 0.0 : (p_control->get_transform().xform(position).y - parent_rect.position.y) / parent_rect.size.y;
831 return output;
832}
833
834bool ControlEditorToolbar::_is_node_locked(const Node *p_node) {
835 return p_node->get_meta("_edit_lock_", false);
836}
837
838List<Control *> ControlEditorToolbar::_get_edited_controls() {
839 List<Control *> selection;
840 for (const KeyValue<Node *, Object *> &E : editor_selection->get_selection()) {
841 Control *control = Object::cast_to<Control>(E.key);
842 if (control && control->is_visible_in_tree() && control->get_viewport() == EditorNode::get_singleton()->get_scene_root() && !_is_node_locked(control)) {
843 selection.push_back(control);
844 }
845 }
846
847 return selection;
848}
849
850void ControlEditorToolbar::_selection_changed() {
851 // Update toolbar visibility.
852 bool has_controls = false;
853 bool has_control_parents = false;
854 bool has_container_parents = false;
855
856 // Also update which size flags can be configured for the selected nodes.
857 Vector<SizeFlags> allowed_h_flags = {
858 SIZE_SHRINK_BEGIN,
859 SIZE_SHRINK_CENTER,
860 SIZE_SHRINK_END,
861 SIZE_FILL,
862 SIZE_EXPAND,
863 };
864 Vector<SizeFlags> allowed_v_flags = {
865 SIZE_SHRINK_BEGIN,
866 SIZE_SHRINK_CENTER,
867 SIZE_SHRINK_END,
868 SIZE_FILL,
869 SIZE_EXPAND,
870 };
871
872 for (const KeyValue<Node *, Object *> &E : editor_selection->get_selection()) {
873 Control *control = Object::cast_to<Control>(E.key);
874 if (!control) {
875 continue;
876 }
877 has_controls = true;
878
879 if (Object::cast_to<Control>(control->get_parent())) {
880 has_control_parents = true;
881 }
882 if (Object::cast_to<Container>(control->get_parent())) {
883 has_container_parents = true;
884
885 Container *parent_container = Object::cast_to<Container>(control->get_parent());
886
887 Vector<int> container_h_flags = parent_container->get_allowed_size_flags_horizontal();
888 Vector<SizeFlags> tmp_flags = allowed_h_flags.duplicate();
889 for (int i = 0; i < allowed_h_flags.size(); i++) {
890 if (!container_h_flags.has((int)allowed_h_flags[i])) {
891 tmp_flags.erase(allowed_h_flags[i]);
892 }
893 }
894 allowed_h_flags = tmp_flags;
895
896 Vector<int> container_v_flags = parent_container->get_allowed_size_flags_vertical();
897 tmp_flags = allowed_v_flags.duplicate();
898 for (int i = 0; i < allowed_v_flags.size(); i++) {
899 if (!container_v_flags.has((int)allowed_v_flags[i])) {
900 tmp_flags.erase(allowed_v_flags[i]);
901 }
902 }
903 allowed_v_flags = tmp_flags;
904 }
905 }
906
907 // Set general toolbar visibility.
908 set_visible(has_controls);
909
910 // Set anchor tools visibility.
911 if (has_controls && (!has_control_parents || !has_container_parents)) {
912 anchors_button->set_visible(true);
913 anchor_mode_button->set_visible(true);
914
915 // Update anchor mode.
916 int nb_valid_controls = 0;
917 int nb_anchors_mode = 0;
918
919 List<Node *> selection = editor_selection->get_selected_node_list();
920 for (Node *E : selection) {
921 Control *control = Object::cast_to<Control>(E);
922 if (!control) {
923 continue;
924 }
925 if (Object::cast_to<Container>(control->get_parent())) {
926 continue;
927 }
928
929 nb_valid_controls++;
930 if (control->get_meta("_edit_use_anchors_", false)) {
931 nb_anchors_mode++;
932 }
933 }
934
935 anchors_mode = (nb_valid_controls == nb_anchors_mode);
936 anchor_mode_button->set_pressed(anchors_mode);
937 } else {
938 anchors_button->set_visible(false);
939 anchor_mode_button->set_visible(false);
940 anchor_mode_button->set_pressed(false);
941 }
942
943 // Set container tools visibility.
944 if (has_controls && (!has_control_parents || has_container_parents)) {
945 containers_button->set_visible(true);
946
947 // Update allowed size flags.
948 if (has_container_parents) {
949 container_h_picker->set_allowed_flags(allowed_h_flags);
950 container_v_picker->set_allowed_flags(allowed_v_flags);
951 } else {
952 Vector<SizeFlags> allowed_all_flags = {
953 SIZE_SHRINK_BEGIN,
954 SIZE_SHRINK_CENTER,
955 SIZE_SHRINK_END,
956 SIZE_FILL,
957 SIZE_EXPAND,
958 };
959
960 container_h_picker->set_allowed_flags(allowed_all_flags);
961 container_v_picker->set_allowed_flags(allowed_all_flags);
962 }
963 } else {
964 containers_button->set_visible(false);
965 }
966}
967
968void ControlEditorToolbar::_notification(int p_what) {
969 switch (p_what) {
970 case NOTIFICATION_ENTER_TREE:
971 case NOTIFICATION_THEME_CHANGED: {
972 anchors_button->set_icon(get_editor_theme_icon(SNAME("ControlLayout")));
973 anchor_mode_button->set_icon(get_editor_theme_icon(SNAME("Anchor")));
974 containers_button->set_icon(get_editor_theme_icon(SNAME("ContainerLayout")));
975 } break;
976 }
977}
978
979ControlEditorToolbar::ControlEditorToolbar() {
980 // Anchor and offset tools.
981 anchors_button = memnew(ControlEditorPopupButton);
982 anchors_button->set_tooltip_text(TTR("Presets for the anchor and offset values of a Control node."));
983 add_child(anchors_button);
984
985 Label *anchors_label = memnew(Label);
986 anchors_label->set_text(TTR("Anchor preset"));
987 anchors_button->get_popup_hbox()->add_child(anchors_label);
988 AnchorPresetPicker *anchors_picker = memnew(AnchorPresetPicker);
989 anchors_picker->set_h_size_flags(SIZE_SHRINK_CENTER);
990 anchors_button->get_popup_hbox()->add_child(anchors_picker);
991 anchors_picker->connect("anchors_preset_selected", callable_mp(this, &ControlEditorToolbar::_anchors_preset_selected));
992
993 anchors_button->get_popup_hbox()->add_child(memnew(HSeparator));
994
995 Button *keep_ratio_button = memnew(Button);
996 keep_ratio_button->set_text_alignment(HORIZONTAL_ALIGNMENT_LEFT);
997 keep_ratio_button->set_text(TTR("Set to Current Ratio"));
998 keep_ratio_button->set_tooltip_text(TTR("Adjust anchors and offsets to match the current rect size."));
999 anchors_button->get_popup_hbox()->add_child(keep_ratio_button);
1000 keep_ratio_button->connect("pressed", callable_mp(this, &ControlEditorToolbar::_anchors_to_current_ratio));
1001
1002 anchor_mode_button = memnew(Button);
1003 anchor_mode_button->set_flat(true);
1004 anchor_mode_button->set_toggle_mode(true);
1005 anchor_mode_button->set_tooltip_text(TTR("When active, moving Control nodes changes their anchors instead of their offsets."));
1006 add_child(anchor_mode_button);
1007 anchor_mode_button->connect("toggled", callable_mp(this, &ControlEditorToolbar::_anchor_mode_toggled));
1008
1009 // Container tools.
1010 containers_button = memnew(ControlEditorPopupButton);
1011 containers_button->set_tooltip_text(TTR("Sizing settings for children of a Container node."));
1012 add_child(containers_button);
1013
1014 Label *container_h_label = memnew(Label);
1015 container_h_label->set_text(TTR("Horizontal alignment"));
1016 containers_button->get_popup_hbox()->add_child(container_h_label);
1017 container_h_picker = memnew(SizeFlagPresetPicker(false));
1018 containers_button->get_popup_hbox()->add_child(container_h_picker);
1019 container_h_picker->connect("size_flags_selected", callable_mp(this, &ControlEditorToolbar::_container_flags_selected).bind(false));
1020
1021 containers_button->get_popup_hbox()->add_child(memnew(HSeparator));
1022
1023 Label *container_v_label = memnew(Label);
1024 container_v_label->set_text(TTR("Vertical alignment"));
1025 containers_button->get_popup_hbox()->add_child(container_v_label);
1026 container_v_picker = memnew(SizeFlagPresetPicker(true));
1027 containers_button->get_popup_hbox()->add_child(container_v_picker);
1028 container_v_picker->connect("size_flags_selected", callable_mp(this, &ControlEditorToolbar::_container_flags_selected).bind(true));
1029
1030 // Editor connections.
1031 editor_selection = EditorNode::get_singleton()->get_editor_selection();
1032 editor_selection->add_editor_plugin(this);
1033 editor_selection->connect("selection_changed", callable_mp(this, &ControlEditorToolbar::_selection_changed));
1034
1035 singleton = this;
1036}
1037
1038ControlEditorToolbar *ControlEditorToolbar::singleton = nullptr;
1039
1040// Editor plugin.
1041
1042ControlEditorPlugin::ControlEditorPlugin() {
1043 toolbar = memnew(ControlEditorToolbar);
1044 toolbar->hide();
1045 add_control_to_container(CONTAINER_CANVAS_EDITOR_MENU, toolbar);
1046
1047 Ref<EditorInspectorPluginControl> plugin;
1048 plugin.instantiate();
1049 add_inspector_plugin(plugin);
1050}
1051