1/**************************************************************************/
2/* tab_container.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 "tab_container.h"
32
33#include "scene/gui/box_container.h"
34#include "scene/gui/label.h"
35#include "scene/gui/texture_rect.h"
36#include "scene/theme/theme_db.h"
37
38int TabContainer::_get_top_margin() const {
39 int height = 0;
40 if (tabs_visible && get_tab_count() > 0) {
41 height = tab_bar->get_minimum_size().height;
42 }
43
44 return height;
45}
46
47void TabContainer::gui_input(const Ref<InputEvent> &p_event) {
48 ERR_FAIL_COND(p_event.is_null());
49
50 Ref<InputEventMouseButton> mb = p_event;
51
52 Popup *popup = get_popup();
53
54 if (mb.is_valid() && mb->is_pressed() && mb->get_button_index() == MouseButton::LEFT) {
55 Point2 pos = mb->get_position();
56 Size2 size = get_size();
57
58 // Click must be on tabs in the tab header area.
59 if (pos.y > _get_top_margin()) {
60 return;
61 }
62
63 // Handle menu button.
64 if (is_layout_rtl()) {
65 if (popup && pos.x < theme_cache.menu_icon->get_width()) {
66 emit_signal(SNAME("pre_popup_pressed"));
67
68 Vector2 popup_pos = get_screen_position();
69 popup_pos.y += theme_cache.menu_icon->get_height();
70
71 popup->set_position(popup_pos);
72 popup->popup();
73 return;
74 }
75 } else {
76 if (popup && pos.x > size.width - theme_cache.menu_icon->get_width()) {
77 emit_signal(SNAME("pre_popup_pressed"));
78
79 Vector2 popup_pos = get_screen_position();
80 popup_pos.x += size.width - popup->get_size().width;
81 popup_pos.y += theme_cache.menu_icon->get_height();
82
83 popup->set_position(popup_pos);
84 popup->popup();
85 return;
86 }
87 }
88 }
89
90 Ref<InputEventMouseMotion> mm = p_event;
91
92 if (mm.is_valid()) {
93 Point2 pos = mm->get_position();
94 Size2 size = get_size();
95
96 // Mouse must be on tabs in the tab header area.
97 if (pos.y > _get_top_margin()) {
98 if (menu_hovered) {
99 menu_hovered = false;
100 queue_redraw();
101 }
102 return;
103 }
104
105 if (popup) {
106 if (is_layout_rtl()) {
107 if (pos.x <= theme_cache.menu_icon->get_width()) {
108 if (!menu_hovered) {
109 menu_hovered = true;
110 queue_redraw();
111 return;
112 }
113 } else if (menu_hovered) {
114 menu_hovered = false;
115 queue_redraw();
116 }
117 } else {
118 if (pos.x >= size.width - theme_cache.menu_icon->get_width()) {
119 if (!menu_hovered) {
120 menu_hovered = true;
121 queue_redraw();
122 return;
123 }
124 } else if (menu_hovered) {
125 menu_hovered = false;
126 queue_redraw();
127 }
128 }
129
130 if (menu_hovered) {
131 return;
132 }
133 }
134 }
135}
136
137void TabContainer::_notification(int p_what) {
138 switch (p_what) {
139 case NOTIFICATION_ENTER_TREE: {
140 // If some nodes happen to be renamed outside the tree, the tab names need to be updated manually.
141 if (get_tab_count() > 0) {
142 _refresh_tab_names();
143 }
144 } break;
145
146 case NOTIFICATION_READY:
147 case NOTIFICATION_RESIZED: {
148 _update_margins();
149 } break;
150
151 case NOTIFICATION_DRAW: {
152 RID canvas = get_canvas_item();
153 Size2 size = get_size();
154
155 // Draw only the tab area if the header is hidden.
156 if (!tabs_visible) {
157 theme_cache.panel_style->draw(canvas, Rect2(0, 0, size.width, size.height));
158 return;
159 }
160
161 int header_height = _get_top_margin();
162
163 // Draw background for the tabbar.
164 theme_cache.tabbar_style->draw(canvas, Rect2(0, 0, size.width, header_height));
165 // Draw the background for the tab's content.
166 theme_cache.panel_style->draw(canvas, Rect2(0, header_height, size.width, size.height - header_height));
167
168 // Draw the popup menu.
169 if (get_popup()) {
170 int x = is_layout_rtl() ? 0 : get_size().width - theme_cache.menu_icon->get_width();
171
172 if (menu_hovered) {
173 theme_cache.menu_hl_icon->draw(get_canvas_item(), Point2(x, (header_height - theme_cache.menu_hl_icon->get_height()) / 2));
174 } else {
175 theme_cache.menu_icon->draw(get_canvas_item(), Point2(x, (header_height - theme_cache.menu_icon->get_height()) / 2));
176 }
177 }
178 } break;
179
180 case NOTIFICATION_TRANSLATION_CHANGED:
181 case NOTIFICATION_LAYOUT_DIRECTION_CHANGED:
182 case NOTIFICATION_THEME_CHANGED: {
183 theme_changing = true;
184 callable_mp(this, &TabContainer::_on_theme_changed).call_deferred(); // Wait until all changed theme.
185 } break;
186 }
187}
188
189void TabContainer::_on_theme_changed() {
190 if (!theme_changing) {
191 return;
192 }
193
194 tab_bar->add_theme_style_override(SNAME("tab_unselected"), theme_cache.tab_unselected_style);
195 tab_bar->add_theme_style_override(SNAME("tab_hovered"), theme_cache.tab_hovered_style);
196 tab_bar->add_theme_style_override(SNAME("tab_selected"), theme_cache.tab_selected_style);
197 tab_bar->add_theme_style_override(SNAME("tab_disabled"), theme_cache.tab_disabled_style);
198
199 tab_bar->add_theme_icon_override(SNAME("increment"), theme_cache.increment_icon);
200 tab_bar->add_theme_icon_override(SNAME("increment_highlight"), theme_cache.increment_hl_icon);
201 tab_bar->add_theme_icon_override(SNAME("decrement"), theme_cache.decrement_icon);
202 tab_bar->add_theme_icon_override(SNAME("decrement_highlight"), theme_cache.decrement_hl_icon);
203 tab_bar->add_theme_icon_override(SNAME("drop_mark"), theme_cache.drop_mark_icon);
204 tab_bar->add_theme_color_override(SNAME("drop_mark_color"), theme_cache.drop_mark_color);
205
206 tab_bar->add_theme_color_override(SNAME("font_selected_color"), theme_cache.font_selected_color);
207 tab_bar->add_theme_color_override(SNAME("font_hovered_color"), theme_cache.font_hovered_color);
208 tab_bar->add_theme_color_override(SNAME("font_unselected_color"), theme_cache.font_unselected_color);
209 tab_bar->add_theme_color_override(SNAME("font_disabled_color"), theme_cache.font_disabled_color);
210 tab_bar->add_theme_color_override(SNAME("font_outline_color"), theme_cache.font_outline_color);
211
212 tab_bar->add_theme_font_override(SNAME("font"), theme_cache.tab_font);
213 tab_bar->add_theme_font_size_override(SNAME("font_size"), theme_cache.tab_font_size);
214
215 tab_bar->add_theme_constant_override(SNAME("h_separation"), theme_cache.icon_separation);
216 tab_bar->add_theme_constant_override(SNAME("icon_max_width"), theme_cache.icon_max_width);
217 tab_bar->add_theme_constant_override(SNAME("outline_size"), theme_cache.outline_size);
218
219 _update_margins();
220 if (get_tab_count() > 0) {
221 _repaint();
222 } else {
223 update_minimum_size();
224 }
225 queue_redraw();
226
227 theme_changing = false;
228}
229
230void TabContainer::_repaint() {
231 Vector<Control *> controls = _get_tab_controls();
232 int current = get_current_tab();
233
234 for (int i = 0; i < controls.size(); i++) {
235 Control *c = controls[i];
236
237 if (i == current) {
238 c->show();
239 c->set_anchors_and_offsets_preset(Control::PRESET_FULL_RECT);
240
241 if (tabs_visible) {
242 c->set_offset(SIDE_TOP, _get_top_margin());
243 }
244
245 c->set_offset(SIDE_TOP, c->get_offset(SIDE_TOP) + theme_cache.panel_style->get_margin(SIDE_TOP));
246 c->set_offset(SIDE_LEFT, c->get_offset(SIDE_LEFT) + theme_cache.panel_style->get_margin(SIDE_LEFT));
247 c->set_offset(SIDE_RIGHT, c->get_offset(SIDE_RIGHT) - theme_cache.panel_style->get_margin(SIDE_RIGHT));
248 c->set_offset(SIDE_BOTTOM, c->get_offset(SIDE_BOTTOM) - theme_cache.panel_style->get_margin(SIDE_BOTTOM));
249 } else {
250 c->hide();
251 }
252 }
253
254 update_minimum_size();
255}
256
257void TabContainer::_update_margins() {
258 int menu_width = theme_cache.menu_icon->get_width();
259
260 // Directly check for validity, to avoid errors when quitting.
261 bool has_popup = popup_obj_id.is_valid();
262
263 if (get_tab_count() == 0) {
264 tab_bar->set_offset(SIDE_LEFT, 0);
265 tab_bar->set_offset(SIDE_RIGHT, has_popup ? -menu_width : 0);
266
267 return;
268 }
269
270 switch (get_tab_alignment()) {
271 case TabBar::ALIGNMENT_LEFT: {
272 tab_bar->set_offset(SIDE_LEFT, theme_cache.side_margin);
273 tab_bar->set_offset(SIDE_RIGHT, has_popup ? -menu_width : 0);
274 } break;
275
276 case TabBar::ALIGNMENT_CENTER: {
277 tab_bar->set_offset(SIDE_LEFT, 0);
278 tab_bar->set_offset(SIDE_RIGHT, has_popup ? -menu_width : 0);
279 } break;
280
281 case TabBar::ALIGNMENT_RIGHT: {
282 tab_bar->set_offset(SIDE_LEFT, 0);
283
284 if (has_popup) {
285 tab_bar->set_offset(SIDE_RIGHT, -menu_width);
286 return;
287 }
288
289 int first_tab_pos = tab_bar->get_tab_rect(0).position.x;
290 Rect2 last_tab_rect = tab_bar->get_tab_rect(get_tab_count() - 1);
291 int total_tabs_width = last_tab_rect.position.x - first_tab_pos + last_tab_rect.size.width;
292
293 // Calculate if all the tabs would still fit if the margin was present.
294 if (get_clip_tabs() && (tab_bar->get_offset_buttons_visible() || (get_tab_count() > 1 && (total_tabs_width + theme_cache.side_margin) > get_size().width))) {
295 tab_bar->set_offset(SIDE_RIGHT, has_popup ? -menu_width : 0);
296 } else {
297 tab_bar->set_offset(SIDE_RIGHT, -theme_cache.side_margin);
298 }
299 } break;
300
301 case TabBar::ALIGNMENT_MAX:
302 break; // Can't happen, but silences warning.
303 }
304}
305
306void TabContainer::_on_mouse_exited() {
307 if (menu_hovered) {
308 menu_hovered = false;
309 queue_redraw();
310 }
311}
312
313Vector<Control *> TabContainer::_get_tab_controls() const {
314 Vector<Control *> controls;
315 for (int i = 0; i < get_child_count(); i++) {
316 Control *control = Object::cast_to<Control>(get_child(i));
317 if (!control || control->is_set_as_top_level() || control == tab_bar || children_removing.has(control)) {
318 continue;
319 }
320
321 controls.push_back(control);
322 }
323
324 return controls;
325}
326
327Variant TabContainer::_get_drag_data_fw(const Point2 &p_point, Control *p_from_control) {
328 if (!drag_to_rearrange_enabled) {
329 return Variant();
330 }
331
332 int tab_over = get_tab_idx_at_point(p_point);
333 if (tab_over < 0) {
334 return Variant();
335 }
336
337 HBoxContainer *drag_preview = memnew(HBoxContainer);
338
339 Ref<Texture2D> icon = get_tab_icon(tab_over);
340 if (!icon.is_null()) {
341 TextureRect *tf = memnew(TextureRect);
342 tf->set_texture(icon);
343 drag_preview->add_child(tf);
344 }
345
346 Label *label = memnew(Label(get_tab_title(tab_over)));
347 set_drag_preview(drag_preview);
348 drag_preview->add_child(label);
349
350 Dictionary drag_data;
351 drag_data["type"] = "tabc_element";
352 drag_data["tabc_element"] = tab_over;
353 drag_data["from_path"] = get_path();
354
355 return drag_data;
356}
357
358bool TabContainer::_can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from_control) const {
359 if (!drag_to_rearrange_enabled) {
360 return false;
361 }
362
363 Dictionary d = p_data;
364 if (!d.has("type")) {
365 return false;
366 }
367
368 if (String(d["type"]) == "tabc_element") {
369 NodePath from_path = d["from_path"];
370 NodePath to_path = get_path();
371 if (from_path == to_path) {
372 return true;
373 } else if (get_tabs_rearrange_group() != -1) {
374 // Drag and drop between other TabContainers.
375 Node *from_node = get_node(from_path);
376 TabContainer *from_tabc = Object::cast_to<TabContainer>(from_node);
377 if (from_tabc && from_tabc->get_tabs_rearrange_group() == get_tabs_rearrange_group()) {
378 return true;
379 }
380 }
381 }
382
383 return false;
384}
385
386void TabContainer::_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from_control) {
387 if (!drag_to_rearrange_enabled) {
388 return;
389 }
390
391 Dictionary d = p_data;
392 if (!d.has("type")) {
393 return;
394 }
395
396 if (String(d["type"]) == "tabc_element") {
397 int tab_from_id = d["tabc_element"];
398 int hover_now = get_tab_idx_at_point(p_point);
399 NodePath from_path = d["from_path"];
400 NodePath to_path = get_path();
401
402 if (from_path == to_path) {
403 if (tab_from_id == hover_now) {
404 return;
405 }
406
407 // Drop the new tab to the left or right depending on where the target tab is being hovered.
408 if (hover_now != -1) {
409 Rect2 tab_rect = tab_bar->get_tab_rect(hover_now);
410 if (is_layout_rtl() ^ (p_point.x <= tab_rect.position.x + tab_rect.size.width / 2)) {
411 if (hover_now > tab_from_id) {
412 hover_now -= 1;
413 }
414 } else if (tab_from_id > hover_now) {
415 hover_now += 1;
416 }
417 } else {
418 hover_now = is_layout_rtl() ^ (p_point.x < tab_bar->get_tab_rect(0).position.x) ? 0 : get_tab_count() - 1;
419 }
420
421 move_child(get_tab_control(tab_from_id), get_tab_control(hover_now)->get_index(false));
422 if (!is_tab_disabled(hover_now)) {
423 emit_signal(SNAME("active_tab_rearranged"), hover_now);
424 set_current_tab(hover_now);
425 }
426
427 } else if (get_tabs_rearrange_group() != -1) {
428 // Drag and drop between TabContainers.
429
430 Node *from_node = get_node(from_path);
431 TabContainer *from_tabc = Object::cast_to<TabContainer>(from_node);
432
433 if (from_tabc && from_tabc->get_tabs_rearrange_group() == get_tabs_rearrange_group()) {
434 // Get the tab properties before they get erased by the child removal.
435 String tab_title = from_tabc->get_tab_title(tab_from_id);
436 Ref<Texture2D> tab_icon = from_tabc->get_tab_icon(tab_from_id);
437 bool tab_disabled = from_tabc->is_tab_disabled(tab_from_id);
438 Variant tab_metadata = from_tabc->get_tab_metadata(tab_from_id);
439
440 // Drop the new tab to the left or right depending on where the target tab is being hovered.
441 if (hover_now != -1) {
442 Rect2 tab_rect = tab_bar->get_tab_rect(hover_now);
443 if (is_layout_rtl() ^ (p_point.x > tab_rect.position.x + tab_rect.size.width / 2)) {
444 hover_now += 1;
445 }
446 } else {
447 hover_now = is_layout_rtl() ^ (p_point.x < tab_bar->get_tab_rect(0).position.x) ? 0 : get_tab_count();
448 }
449
450 Control *moving_tabc = from_tabc->get_tab_control(tab_from_id);
451 from_tabc->remove_child(moving_tabc);
452 add_child(moving_tabc, true);
453
454 set_tab_title(get_tab_count() - 1, tab_title);
455 set_tab_icon(get_tab_count() - 1, tab_icon);
456 set_tab_disabled(get_tab_count() - 1, tab_disabled);
457 set_tab_metadata(get_tab_count() - 1, tab_metadata);
458
459 move_child(moving_tabc, get_tab_control(hover_now)->get_index(false));
460 if (!is_tab_disabled(hover_now)) {
461 set_current_tab(hover_now);
462 }
463 }
464 }
465 }
466}
467
468void TabContainer::_on_tab_clicked(int p_tab) {
469 emit_signal(SNAME("tab_clicked"), p_tab);
470}
471
472void TabContainer::_on_tab_hovered(int p_tab) {
473 emit_signal(SNAME("tab_hovered"), p_tab);
474}
475
476void TabContainer::_on_tab_changed(int p_tab) {
477 callable_mp(this, &TabContainer::_repaint).call_deferred();
478
479 emit_signal(SNAME("tab_changed"), p_tab);
480}
481
482void TabContainer::_on_tab_selected(int p_tab) {
483 if (p_tab != get_previous_tab()) {
484 callable_mp(this, &TabContainer::_repaint).call_deferred();
485 }
486
487 emit_signal(SNAME("tab_selected"), p_tab);
488}
489
490void TabContainer::_on_tab_button_pressed(int p_tab) {
491 emit_signal(SNAME("tab_button_pressed"), p_tab);
492}
493
494void TabContainer::_refresh_tab_names() {
495 Vector<Control *> controls = _get_tab_controls();
496 for (int i = 0; i < controls.size(); i++) {
497 if (!controls[i]->has_meta("_tab_name") && String(controls[i]->get_name()) != get_tab_title(i)) {
498 tab_bar->set_tab_title(i, controls[i]->get_name());
499 }
500 }
501}
502
503void TabContainer::add_child_notify(Node *p_child) {
504 Container::add_child_notify(p_child);
505
506 if (p_child == tab_bar) {
507 return;
508 }
509
510 Control *c = Object::cast_to<Control>(p_child);
511 if (!c || c->is_set_as_top_level()) {
512 return;
513 }
514 c->hide();
515
516 tab_bar->add_tab(p_child->get_name());
517
518 _update_margins();
519 if (get_tab_count() == 1) {
520 queue_redraw();
521 }
522
523 p_child->connect("renamed", callable_mp(this, &TabContainer::_refresh_tab_names));
524
525 // TabBar won't emit the "tab_changed" signal when not inside the tree.
526 if (!is_inside_tree()) {
527 callable_mp(this, &TabContainer::_repaint).call_deferred();
528 }
529}
530
531void TabContainer::move_child_notify(Node *p_child) {
532 Container::move_child_notify(p_child);
533
534 if (p_child == tab_bar) {
535 return;
536 }
537
538 Control *c = Object::cast_to<Control>(p_child);
539 if (c && !c->is_set_as_top_level()) {
540 int old_idx = -1;
541 String tab_name = String(c->get_meta("_tab_name", c->get_name()));
542
543 // Find the previous tab index of the control.
544 for (int i = 0; i < get_tab_count(); i++) {
545 if (get_tab_title(i) == tab_name) {
546 old_idx = i;
547 break;
548 }
549 }
550
551 tab_bar->move_tab(old_idx, get_tab_idx_from_control(c));
552 }
553}
554
555void TabContainer::remove_child_notify(Node *p_child) {
556 Container::remove_child_notify(p_child);
557
558 if (p_child == tab_bar) {
559 return;
560 }
561
562 Control *c = Object::cast_to<Control>(p_child);
563 if (!c || c->is_set_as_top_level()) {
564 return;
565 }
566
567 int idx = get_tab_idx_from_control(c);
568
569 // As the child hasn't been removed yet, keep track of it so when the "tab_changed" signal is fired it can be ignored.
570 children_removing.push_back(c);
571 tab_bar->remove_tab(idx);
572 children_removing.erase(c);
573
574 _update_margins();
575 if (get_tab_count() == 0) {
576 queue_redraw();
577 }
578
579 p_child->remove_meta("_tab_name");
580 p_child->disconnect("renamed", callable_mp(this, &TabContainer::_refresh_tab_names));
581
582 // TabBar won't emit the "tab_changed" signal when not inside the tree.
583 if (!is_inside_tree()) {
584 callable_mp(this, &TabContainer::_repaint).call_deferred();
585 }
586}
587
588int TabContainer::get_tab_count() const {
589 return tab_bar->get_tab_count();
590}
591
592void TabContainer::set_current_tab(int p_current) {
593 tab_bar->set_current_tab(p_current);
594}
595
596int TabContainer::get_current_tab() const {
597 return tab_bar->get_current_tab();
598}
599
600int TabContainer::get_previous_tab() const {
601 return tab_bar->get_previous_tab();
602}
603
604Control *TabContainer::get_tab_control(int p_idx) const {
605 Vector<Control *> controls = _get_tab_controls();
606 if (p_idx >= 0 && p_idx < controls.size()) {
607 return controls[p_idx];
608 } else {
609 return nullptr;
610 }
611}
612
613Control *TabContainer::get_current_tab_control() const {
614 return get_tab_control(tab_bar->get_current_tab());
615}
616
617int TabContainer::get_tab_idx_at_point(const Point2 &p_point) const {
618 return tab_bar->get_tab_idx_at_point(p_point);
619}
620
621int TabContainer::get_tab_idx_from_control(Control *p_child) const {
622 ERR_FAIL_NULL_V(p_child, -1);
623 ERR_FAIL_COND_V(p_child->get_parent() != this, -1);
624
625 Vector<Control *> controls = _get_tab_controls();
626 for (int i = 0; i < controls.size(); i++) {
627 if (controls[i] == p_child) {
628 return i;
629 }
630 }
631
632 return -1;
633}
634
635void TabContainer::set_tab_alignment(TabBar::AlignmentMode p_alignment) {
636 if (tab_bar->get_tab_alignment() == p_alignment) {
637 return;
638 }
639
640 tab_bar->set_tab_alignment(p_alignment);
641 _update_margins();
642}
643
644TabBar::AlignmentMode TabContainer::get_tab_alignment() const {
645 return tab_bar->get_tab_alignment();
646}
647
648void TabContainer::set_clip_tabs(bool p_clip_tabs) {
649 tab_bar->set_clip_tabs(p_clip_tabs);
650}
651
652bool TabContainer::get_clip_tabs() const {
653 return tab_bar->get_clip_tabs();
654}
655
656void TabContainer::set_tabs_visible(bool p_visible) {
657 if (p_visible == tabs_visible) {
658 return;
659 }
660
661 tabs_visible = p_visible;
662 tab_bar->set_visible(tabs_visible);
663
664 Vector<Control *> controls = _get_tab_controls();
665 for (int i = 0; i < controls.size(); i++) {
666 Control *c = controls[i];
667 if (tabs_visible) {
668 c->set_offset(SIDE_TOP, _get_top_margin());
669 } else {
670 c->set_offset(SIDE_TOP, 0);
671 }
672 }
673
674 queue_redraw();
675 update_minimum_size();
676}
677
678bool TabContainer::are_tabs_visible() const {
679 return tabs_visible;
680}
681
682void TabContainer::set_all_tabs_in_front(bool p_in_front) {
683 if (p_in_front == all_tabs_in_front) {
684 return;
685 }
686
687 all_tabs_in_front = p_in_front;
688
689 remove_child(tab_bar);
690 add_child(tab_bar, false, all_tabs_in_front ? INTERNAL_MODE_FRONT : INTERNAL_MODE_BACK);
691}
692
693bool TabContainer::is_all_tabs_in_front() const {
694 return all_tabs_in_front;
695}
696
697void TabContainer::set_tab_title(int p_tab, const String &p_title) {
698 Control *child = get_tab_control(p_tab);
699 ERR_FAIL_NULL(child);
700
701 if (tab_bar->get_tab_title(p_tab) == p_title) {
702 return;
703 }
704
705 tab_bar->set_tab_title(p_tab, p_title);
706
707 if (p_title == child->get_name()) {
708 child->remove_meta("_tab_name");
709 } else {
710 child->set_meta("_tab_name", p_title);
711 }
712
713 _update_margins();
714 if (!get_clip_tabs()) {
715 update_minimum_size();
716 }
717}
718
719String TabContainer::get_tab_title(int p_tab) const {
720 return tab_bar->get_tab_title(p_tab);
721}
722
723void TabContainer::set_tab_icon(int p_tab, const Ref<Texture2D> &p_icon) {
724 if (tab_bar->get_tab_icon(p_tab) == p_icon) {
725 return;
726 }
727
728 tab_bar->set_tab_icon(p_tab, p_icon);
729
730 _update_margins();
731 _repaint();
732}
733
734Ref<Texture2D> TabContainer::get_tab_icon(int p_tab) const {
735 return tab_bar->get_tab_icon(p_tab);
736}
737
738void TabContainer::set_tab_disabled(int p_tab, bool p_disabled) {
739 if (tab_bar->is_tab_disabled(p_tab) == p_disabled) {
740 return;
741 }
742
743 tab_bar->set_tab_disabled(p_tab, p_disabled);
744
745 _update_margins();
746 if (!get_clip_tabs()) {
747 update_minimum_size();
748 }
749}
750
751bool TabContainer::is_tab_disabled(int p_tab) const {
752 return tab_bar->is_tab_disabled(p_tab);
753}
754
755void TabContainer::set_tab_hidden(int p_tab, bool p_hidden) {
756 Control *child = get_tab_control(p_tab);
757 ERR_FAIL_NULL(child);
758
759 if (tab_bar->is_tab_hidden(p_tab) == p_hidden) {
760 return;
761 }
762
763 tab_bar->set_tab_hidden(p_tab, p_hidden);
764 child->hide();
765
766 _update_margins();
767 if (!get_clip_tabs()) {
768 update_minimum_size();
769 }
770 callable_mp(this, &TabContainer::_repaint).call_deferred();
771}
772
773bool TabContainer::is_tab_hidden(int p_tab) const {
774 return tab_bar->is_tab_hidden(p_tab);
775}
776
777void TabContainer::set_tab_metadata(int p_tab, const Variant &p_metadata) {
778 tab_bar->set_tab_metadata(p_tab, p_metadata);
779}
780
781Variant TabContainer::get_tab_metadata(int p_tab) const {
782 return tab_bar->get_tab_metadata(p_tab);
783}
784
785void TabContainer::set_tab_button_icon(int p_tab, const Ref<Texture2D> &p_icon) {
786 tab_bar->set_tab_button_icon(p_tab, p_icon);
787
788 _update_margins();
789 _repaint();
790}
791
792Ref<Texture2D> TabContainer::get_tab_button_icon(int p_tab) const {
793 return tab_bar->get_tab_button_icon(p_tab);
794}
795
796Size2 TabContainer::get_minimum_size() const {
797 Size2 ms;
798
799 if (tabs_visible) {
800 ms = tab_bar->get_minimum_size();
801
802 if (!get_clip_tabs()) {
803 if (get_popup()) {
804 ms.x += theme_cache.menu_icon->get_width();
805 }
806
807 if (theme_cache.side_margin > 0 && get_tab_alignment() != TabBar::ALIGNMENT_CENTER &&
808 (get_tab_alignment() != TabBar::ALIGNMENT_RIGHT || !get_popup())) {
809 ms.x += theme_cache.side_margin;
810 }
811 }
812 }
813
814 Vector<Control *> controls = _get_tab_controls();
815 Size2 largest_child_min_size;
816 for (int i = 0; i < controls.size(); i++) {
817 Control *c = controls[i];
818
819 if (!c->is_visible_in_tree() && !use_hidden_tabs_for_min_size) {
820 continue;
821 }
822
823 Size2 cms = c->get_combined_minimum_size();
824 largest_child_min_size.x = MAX(largest_child_min_size.x, cms.x);
825 largest_child_min_size.y = MAX(largest_child_min_size.y, cms.y);
826 }
827 ms.y += largest_child_min_size.y;
828
829 Size2 panel_ms = theme_cache.panel_style->get_minimum_size();
830
831 ms.x = MAX(ms.x, largest_child_min_size.x + panel_ms.x);
832 ms.y += panel_ms.y;
833
834 return ms;
835}
836
837void TabContainer::set_popup(Node *p_popup) {
838 bool had_popup = get_popup();
839
840 Popup *popup = Object::cast_to<Popup>(p_popup);
841 ObjectID popup_id = popup ? popup->get_instance_id() : ObjectID();
842 if (popup_obj_id == popup_id) {
843 return;
844 }
845 popup_obj_id = popup_id;
846
847 if (had_popup != bool(popup)) {
848 queue_redraw();
849 _update_margins();
850 if (!get_clip_tabs()) {
851 update_minimum_size();
852 }
853 }
854}
855
856Popup *TabContainer::get_popup() const {
857 if (popup_obj_id.is_valid()) {
858 Popup *popup = Object::cast_to<Popup>(ObjectDB::get_instance(popup_obj_id));
859 if (popup) {
860 return popup;
861 } else {
862#ifdef DEBUG_ENABLED
863 ERR_PRINT("Popup assigned to TabContainer is gone!");
864#endif
865 popup_obj_id = ObjectID();
866 }
867 }
868
869 return nullptr;
870}
871
872void TabContainer::set_drag_to_rearrange_enabled(bool p_enabled) {
873 drag_to_rearrange_enabled = p_enabled;
874}
875
876bool TabContainer::get_drag_to_rearrange_enabled() const {
877 return drag_to_rearrange_enabled;
878}
879
880void TabContainer::set_tabs_rearrange_group(int p_group_id) {
881 tab_bar->set_tabs_rearrange_group(p_group_id);
882}
883
884int TabContainer::get_tabs_rearrange_group() const {
885 return tab_bar->get_tabs_rearrange_group();
886}
887
888void TabContainer::set_use_hidden_tabs_for_min_size(bool p_use_hidden_tabs) {
889 if (use_hidden_tabs_for_min_size == p_use_hidden_tabs) {
890 return;
891 }
892
893 use_hidden_tabs_for_min_size = p_use_hidden_tabs;
894 update_minimum_size();
895}
896
897bool TabContainer::get_use_hidden_tabs_for_min_size() const {
898 return use_hidden_tabs_for_min_size;
899}
900
901Vector<int> TabContainer::get_allowed_size_flags_horizontal() const {
902 return Vector<int>();
903}
904
905Vector<int> TabContainer::get_allowed_size_flags_vertical() const {
906 return Vector<int>();
907}
908
909void TabContainer::_bind_methods() {
910 ClassDB::bind_method(D_METHOD("get_tab_count"), &TabContainer::get_tab_count);
911 ClassDB::bind_method(D_METHOD("set_current_tab", "tab_idx"), &TabContainer::set_current_tab);
912 ClassDB::bind_method(D_METHOD("get_current_tab"), &TabContainer::get_current_tab);
913 ClassDB::bind_method(D_METHOD("get_previous_tab"), &TabContainer::get_previous_tab);
914 ClassDB::bind_method(D_METHOD("get_current_tab_control"), &TabContainer::get_current_tab_control);
915 ClassDB::bind_method(D_METHOD("get_tab_control", "tab_idx"), &TabContainer::get_tab_control);
916 ClassDB::bind_method(D_METHOD("set_tab_alignment", "alignment"), &TabContainer::set_tab_alignment);
917 ClassDB::bind_method(D_METHOD("get_tab_alignment"), &TabContainer::get_tab_alignment);
918 ClassDB::bind_method(D_METHOD("set_clip_tabs", "clip_tabs"), &TabContainer::set_clip_tabs);
919 ClassDB::bind_method(D_METHOD("get_clip_tabs"), &TabContainer::get_clip_tabs);
920 ClassDB::bind_method(D_METHOD("set_tabs_visible", "visible"), &TabContainer::set_tabs_visible);
921 ClassDB::bind_method(D_METHOD("are_tabs_visible"), &TabContainer::are_tabs_visible);
922 ClassDB::bind_method(D_METHOD("set_all_tabs_in_front", "is_front"), &TabContainer::set_all_tabs_in_front);
923 ClassDB::bind_method(D_METHOD("is_all_tabs_in_front"), &TabContainer::is_all_tabs_in_front);
924 ClassDB::bind_method(D_METHOD("set_tab_title", "tab_idx", "title"), &TabContainer::set_tab_title);
925 ClassDB::bind_method(D_METHOD("get_tab_title", "tab_idx"), &TabContainer::get_tab_title);
926 ClassDB::bind_method(D_METHOD("set_tab_icon", "tab_idx", "icon"), &TabContainer::set_tab_icon);
927 ClassDB::bind_method(D_METHOD("get_tab_icon", "tab_idx"), &TabContainer::get_tab_icon);
928 ClassDB::bind_method(D_METHOD("set_tab_disabled", "tab_idx", "disabled"), &TabContainer::set_tab_disabled);
929 ClassDB::bind_method(D_METHOD("is_tab_disabled", "tab_idx"), &TabContainer::is_tab_disabled);
930 ClassDB::bind_method(D_METHOD("set_tab_hidden", "tab_idx", "hidden"), &TabContainer::set_tab_hidden);
931 ClassDB::bind_method(D_METHOD("is_tab_hidden", "tab_idx"), &TabContainer::is_tab_hidden);
932 ClassDB::bind_method(D_METHOD("set_tab_metadata", "tab_idx", "metadata"), &TabContainer::set_tab_metadata);
933 ClassDB::bind_method(D_METHOD("get_tab_metadata", "tab_idx"), &TabContainer::get_tab_metadata);
934 ClassDB::bind_method(D_METHOD("set_tab_button_icon", "tab_idx", "icon"), &TabContainer::set_tab_button_icon);
935 ClassDB::bind_method(D_METHOD("get_tab_button_icon", "tab_idx"), &TabContainer::get_tab_button_icon);
936 ClassDB::bind_method(D_METHOD("get_tab_idx_at_point", "point"), &TabContainer::get_tab_idx_at_point);
937 ClassDB::bind_method(D_METHOD("get_tab_idx_from_control", "control"), &TabContainer::get_tab_idx_from_control);
938 ClassDB::bind_method(D_METHOD("set_popup", "popup"), &TabContainer::set_popup);
939 ClassDB::bind_method(D_METHOD("get_popup"), &TabContainer::get_popup);
940 ClassDB::bind_method(D_METHOD("set_drag_to_rearrange_enabled", "enabled"), &TabContainer::set_drag_to_rearrange_enabled);
941 ClassDB::bind_method(D_METHOD("get_drag_to_rearrange_enabled"), &TabContainer::get_drag_to_rearrange_enabled);
942 ClassDB::bind_method(D_METHOD("set_tabs_rearrange_group", "group_id"), &TabContainer::set_tabs_rearrange_group);
943 ClassDB::bind_method(D_METHOD("get_tabs_rearrange_group"), &TabContainer::get_tabs_rearrange_group);
944 ClassDB::bind_method(D_METHOD("set_use_hidden_tabs_for_min_size", "enabled"), &TabContainer::set_use_hidden_tabs_for_min_size);
945 ClassDB::bind_method(D_METHOD("get_use_hidden_tabs_for_min_size"), &TabContainer::get_use_hidden_tabs_for_min_size);
946
947 ADD_SIGNAL(MethodInfo("active_tab_rearranged", PropertyInfo(Variant::INT, "idx_to")));
948 ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab")));
949 ADD_SIGNAL(MethodInfo("tab_clicked", PropertyInfo(Variant::INT, "tab")));
950 ADD_SIGNAL(MethodInfo("tab_hovered", PropertyInfo(Variant::INT, "tab")));
951 ADD_SIGNAL(MethodInfo("tab_selected", PropertyInfo(Variant::INT, "tab")));
952 ADD_SIGNAL(MethodInfo("tab_button_pressed", PropertyInfo(Variant::INT, "tab")));
953 ADD_SIGNAL(MethodInfo("pre_popup_pressed"));
954
955 ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_alignment", PROPERTY_HINT_ENUM, "Left,Center,Right"), "set_tab_alignment", "get_tab_alignment");
956 ADD_PROPERTY(PropertyInfo(Variant::INT, "current_tab", PROPERTY_HINT_RANGE, "-1,4096,1", PROPERTY_USAGE_EDITOR), "set_current_tab", "get_current_tab");
957 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_tabs"), "set_clip_tabs", "get_clip_tabs");
958 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "tabs_visible"), "set_tabs_visible", "are_tabs_visible");
959 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "all_tabs_in_front"), "set_all_tabs_in_front", "is_all_tabs_in_front");
960 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled");
961 ADD_PROPERTY(PropertyInfo(Variant::INT, "tabs_rearrange_group"), "set_tabs_rearrange_group", "get_tabs_rearrange_group");
962 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_hidden_tabs_for_min_size"), "set_use_hidden_tabs_for_min_size", "get_use_hidden_tabs_for_min_size");
963
964 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, TabContainer, side_margin);
965
966 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, panel_style, "panel");
967 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tabbar_style, "tabbar_background");
968
969 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, menu_icon, "menu");
970 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, menu_hl_icon, "menu_highlight");
971
972 // TabBar overrides.
973 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, TabContainer, icon_separation);
974 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, TabContainer, icon_max_width);
975
976 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tab_unselected_style, "tab_unselected");
977 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tab_hovered_style, "tab_hovered");
978 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tab_selected_style, "tab_selected");
979 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tab_disabled_style, "tab_disabled");
980
981 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, increment_icon, "increment");
982 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, increment_hl_icon, "increment_highlight");
983 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, decrement_icon, "decrement");
984 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, decrement_hl_icon, "decrement_highlight");
985 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, drop_mark_icon, "drop_mark");
986 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabContainer, drop_mark_color);
987
988 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabContainer, font_selected_color);
989 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabContainer, font_hovered_color);
990 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabContainer, font_unselected_color);
991 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabContainer, font_disabled_color);
992 BIND_THEME_ITEM(Theme::DATA_TYPE_COLOR, TabContainer, font_outline_color);
993
994 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_FONT, TabContainer, tab_font, "font");
995 BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_FONT_SIZE, TabContainer, tab_font_size, "font_size");
996 BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, TabContainer, outline_size);
997}
998
999TabContainer::TabContainer() {
1000 tab_bar = memnew(TabBar);
1001 SET_DRAG_FORWARDING_GCDU(tab_bar, TabContainer);
1002 add_child(tab_bar, false, INTERNAL_MODE_FRONT);
1003 tab_bar->set_anchors_and_offsets_preset(Control::PRESET_TOP_WIDE);
1004 tab_bar->connect("tab_changed", callable_mp(this, &TabContainer::_on_tab_changed));
1005 tab_bar->connect("tab_clicked", callable_mp(this, &TabContainer::_on_tab_clicked));
1006 tab_bar->connect("tab_hovered", callable_mp(this, &TabContainer::_on_tab_hovered));
1007 tab_bar->connect("tab_selected", callable_mp(this, &TabContainer::_on_tab_selected));
1008 tab_bar->connect("tab_button_pressed", callable_mp(this, &TabContainer::_on_tab_button_pressed));
1009
1010 connect("mouse_exited", callable_mp(this, &TabContainer::_on_mouse_exited));
1011}
1012