From 6e965f6c8337eb9946a5e3da7183d89aa53aac38 Mon Sep 17 00:00:00 2001 From: kit Date: Sun, 14 Jan 2024 18:39:47 -0500 Subject: [PATCH] Allow tab deselection --- doc/classes/TabBar.xml | 7 ++- doc/classes/TabContainer.xml | 7 ++- scene/gui/tab_bar.cpp | 110 +++++++++++++++++++++++++---------- scene/gui/tab_bar.h | 9 ++- scene/gui/tab_container.cpp | 57 +++++++++++++++++- scene/gui/tab_container.h | 8 ++- 6 files changed, 159 insertions(+), 39 deletions(-) diff --git a/doc/classes/TabBar.xml b/doc/classes/TabBar.xml index 165b187710e3..c59a336bc282 100644 --- a/doc/classes/TabBar.xml +++ b/doc/classes/TabBar.xml @@ -229,8 +229,11 @@ If [code]true[/code], tabs overflowing this node's width will be hidden, displaying two navigation buttons instead. Otherwise, this node's minimum size is updated so that all tabs are visible. - - Select tab at index [code]tab_idx[/code]. + + The index of the current selected tab. A value of [code]-1[/code] means that no tab is selected and can only be set when [member deselect_enabled] is [code]true[/code] or if all tabs are hidden or disabled. + + + If [code]true[/code], all tabs can be deselected so that no tab is selected. Click on the current tab to deselect it. If [code]true[/code], tabs can be rearranged with mouse drag. diff --git a/doc/classes/TabContainer.xml b/doc/classes/TabContainer.xml index 1f3c3531c622..0c1feae6e0e4 100644 --- a/doc/classes/TabContainer.xml +++ b/doc/classes/TabContainer.xml @@ -181,8 +181,13 @@ If [code]true[/code], tabs overflowing this node's width will be hidden, displaying two navigation buttons instead. Otherwise, this node's minimum size is updated so that all tabs are visible. - + The current tab index. When set, this index's [Control] node's [code]visible[/code] property is set to [code]true[/code] and all others are set to [code]false[/code]. + A value of [code]-1[/code] means that no tab is selected. + + + If [code]true[/code], all tabs can be deselected so that no tab is selected. Click on the [member current_tab] to deselect it. + Only the tab header will be shown if no tabs are selected. If [code]true[/code], tabs can be rearranged with mouse drag. diff --git a/scene/gui/tab_bar.cpp b/scene/gui/tab_bar.cpp index c153f8bd7d3c..3b5428d72bae 100644 --- a/scene/gui/tab_bar.cpp +++ b/scene/gui/tab_bar.cpp @@ -278,7 +278,11 @@ void TabBar::gui_input(const Ref &p_event) { } if (found != -1) { - set_current_tab(found); + if (deselect_enabled && get_current_tab() == found) { + set_current_tab(-1); + } else { + set_current_tab(found); + } if (mb->get_button_index() == MouseButton::RIGHT) { // Right mouse button clicked. @@ -616,8 +620,8 @@ void TabBar::set_tab_count(int p_count) { if (p_count == 0) { offset = 0; max_drawn_tab = 0; - current = 0; - previous = 0; + current = -1; + previous = -1; } else { offset = MIN(offset, p_count - 1); max_drawn_tab = MIN(max_drawn_tab, p_count - 1); @@ -640,7 +644,12 @@ int TabBar::get_tab_count() const { } void TabBar::set_current_tab(int p_current) { - ERR_FAIL_INDEX(p_current, get_tab_count()); + if (p_current == -1) { + // An index of -1 is only valid if deselecting is enabled or there are no valid tabs. + ERR_FAIL_COND_MSG(!_can_deselect(), "Cannot deselect tabs, deselection is not enabled."); + } else { + ERR_FAIL_INDEX(p_current, get_tab_count()); + } previous = current; current = p_current; @@ -1069,8 +1078,13 @@ void TabBar::add_tab(const String &p_str, const Ref &p_icon) { queue_redraw(); update_minimum_size(); - if (tabs.size() == 1 && is_inside_tree()) { - emit_signal(SNAME("tab_changed"), 0); + if (tabs.size() == 1) { + if (is_inside_tree()) { + set_current_tab(0); + } else { + current = 0; + previous = -1; + } } } @@ -1082,8 +1096,8 @@ void TabBar::clear_tabs() { tabs.clear(); offset = 0; max_drawn_tab = 0; - current = 0; - previous = 0; + current = -1; + previous = -1; queue_redraw(); update_minimum_size(); @@ -1094,34 +1108,43 @@ void TabBar::remove_tab(int p_idx) { ERR_FAIL_INDEX(p_idx, tabs.size()); tabs.remove_at(p_idx); - bool is_tab_changing = current == p_idx && !tabs.is_empty(); + bool is_tab_changing = current == p_idx; if (current >= p_idx && current > 0) { current--; } + if (previous >= p_idx && previous > 0) { + previous--; + } if (tabs.is_empty()) { offset = 0; max_drawn_tab = 0; - previous = 0; + current = -1; + previous = -1; } else { - // Try to change to a valid tab if possible (without firing the `tab_selected` signal). - for (int i = current; i < tabs.size(); i++) { - if (!is_tab_disabled(i) && !is_tab_hidden(i)) { - current = i; - break; - } - } - // If nothing, try backwards. - if (is_tab_disabled(current) || is_tab_hidden(current)) { - for (int i = current - 1; i >= 0; i--) { + if (current != -1) { + // Try to change to a valid tab if possible (without firing the `tab_selected` signal). + for (int i = current; i < tabs.size(); i++) { if (!is_tab_disabled(i) && !is_tab_hidden(i)) { current = i; break; } } + // If nothing, try backwards. + if (is_tab_disabled(current) || is_tab_hidden(current)) { + for (int i = current - 1; i >= 0; i--) { + if (!is_tab_disabled(i) && !is_tab_hidden(i)) { + current = i; + break; + } + } + } + // If still no valid tab, deselect. + if (is_tab_disabled(current) || is_tab_hidden(current)) { + current = -1; + } } - offset = MIN(offset, tabs.size() - 1); max_drawn_tab = MIN(max_drawn_tab, tabs.size() - 1); @@ -1301,17 +1324,9 @@ void TabBar::_move_tab_from(TabBar *p_from_tabbar, int p_from_index, int p_to_in if (!is_tab_disabled(p_to_index)) { set_current_tab(p_to_index); - if (tabs.size() == 1) { - _update_cache(); - queue_redraw(); - emit_signal(SNAME("tab_changed"), 0); - } } else { _update_cache(); queue_redraw(); - if (tabs.size() == 1) { - emit_signal(SNAME("tab_changed"), 0); - } } update_minimum_size(); @@ -1398,9 +1413,9 @@ void TabBar::move_tab(int p_from, int p_to) { if (previous == p_from) { previous = p_to; - } else if (previous > p_from && previous >= p_to) { + } else if (previous > p_from && previous <= p_to) { previous--; - } else if (previous < p_from && previous <= p_to) { + } else if (previous < p_from && previous >= p_to) { previous++; } @@ -1516,10 +1531,26 @@ void TabBar::_ensure_no_over_offset() { } } +bool TabBar::_can_deselect() const { + if (deselect_enabled) { + return true; + } + // All tabs must be disabled or hidden. + for (const Tab &tab : tabs) { + if (!tab.disabled && !tab.hidden) { + return false; + } + } + return true; +} + void TabBar::ensure_tab_visible(int p_idx) { if (!is_inside_tree() || !buttons_visible) { return; } + if (p_idx == -1 && _can_deselect()) { + return; + } ERR_FAIL_INDEX(p_idx, tabs.size()); if (tabs[p_idx].hidden || (p_idx >= offset && p_idx <= max_drawn_tab)) { @@ -1662,6 +1693,20 @@ bool TabBar::get_select_with_rmb() const { return select_with_rmb; } +void TabBar::set_deselect_enabled(bool p_enabled) { + if (deselect_enabled == p_enabled) { + return; + } + deselect_enabled = p_enabled; + if (!deselect_enabled && current == -1 && !tabs.is_empty()) { + select_next_available(); + } +} + +bool TabBar::get_deselect_enabled() const { + return deselect_enabled; +} + bool TabBar::_set(const StringName &p_name, const Variant &p_value) { Vector components = String(p_name).split("/", true, 2); if (components.size() >= 2 && components[0].begins_with("tab_") && components[0].trim_prefix("tab_").is_valid_int()) { @@ -1766,6 +1811,8 @@ void TabBar::_bind_methods() { ClassDB::bind_method(D_METHOD("get_scroll_to_selected"), &TabBar::get_scroll_to_selected); ClassDB::bind_method(D_METHOD("set_select_with_rmb", "enabled"), &TabBar::set_select_with_rmb); ClassDB::bind_method(D_METHOD("get_select_with_rmb"), &TabBar::get_select_with_rmb); + ClassDB::bind_method(D_METHOD("set_deselect_enabled", "enabled"), &TabBar::set_deselect_enabled); + ClassDB::bind_method(D_METHOD("get_deselect_enabled"), &TabBar::get_deselect_enabled); ClassDB::bind_method(D_METHOD("clear_tabs"), &TabBar::clear_tabs); ADD_SIGNAL(MethodInfo("tab_selected", PropertyInfo(Variant::INT, "tab"))); @@ -1790,6 +1837,7 @@ void TabBar::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "tabs_rearrange_group"), "set_tabs_rearrange_group", "get_tabs_rearrange_group"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scroll_to_selected"), "set_scroll_to_selected", "get_scroll_to_selected"); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "select_with_rmb"), "set_select_with_rmb", "get_select_with_rmb"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "deselect_enabled"), "set_deselect_enabled", "get_deselect_enabled"); BIND_ENUM_CONSTANT(ALIGNMENT_LEFT); BIND_ENUM_CONSTANT(ALIGNMENT_CENTER); diff --git a/scene/gui/tab_bar.h b/scene/gui/tab_bar.h index 9674187fb3e5..444c73722047 100644 --- a/scene/gui/tab_bar.h +++ b/scene/gui/tab_bar.h @@ -85,8 +85,8 @@ class TabBar : public Control { bool buttons_visible = false; bool missing_right = false; Vector tabs; - int current = 0; - int previous = 0; + int current = -1; + int previous = -1; AlignmentMode tab_alignment = ALIGNMENT_LEFT; bool clip_tabs = true; int rb_hover = -1; @@ -94,6 +94,7 @@ class TabBar : public Control { bool tab_style_v_flip = false; bool select_with_rmb = false; + bool deselect_enabled = false; int cb_hover = -1; bool cb_pressing = false; @@ -146,6 +147,7 @@ class TabBar : public Control { int get_tab_width(int p_idx) const; Size2 _get_tab_icon_size(int p_idx) const; void _ensure_no_over_offset(); + bool _can_deselect() const; void _update_hover(); void _update_cache(bool p_update_hover = true); @@ -250,6 +252,9 @@ class TabBar : public Control { void set_select_with_rmb(bool p_enabled); bool get_select_with_rmb() const; + void set_deselect_enabled(bool p_enabled); + bool get_deselect_enabled() const; + void ensure_tab_visible(int p_idx); void set_max_tab_width(int p_width); diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp index ef01d9ec5da2..741b38a35ae0 100644 --- a/scene/gui/tab_container.cpp +++ b/scene/gui/tab_container.cpp @@ -153,9 +153,9 @@ void TabContainer::_notification(int p_what) { } break; case NOTIFICATION_POST_ENTER_TREE: { - if (setup_current_tab >= 0) { + if (setup_current_tab >= -1) { set_current_tab(setup_current_tab); - setup_current_tab = -1; + setup_current_tab = -2; } } break; @@ -259,6 +259,7 @@ void TabContainer::_repaint() { tab_bar->set_anchors_and_offsets_preset(PRESET_TOP_WIDE); } + updating_visibility = true; for (int i = 0; i < controls.size(); i++) { Control *c = controls[i]; @@ -282,6 +283,7 @@ void TabContainer::_repaint() { c->hide(); } } + updating_visibility = false; _update_margins(); update_minimum_size(); @@ -440,6 +442,7 @@ void TabContainer::_on_tab_hovered(int p_tab) { void TabContainer::_on_tab_changed(int p_tab) { callable_mp(this, &TabContainer::_repaint).call_deferred(); + queue_redraw(); emit_signal(SNAME("tab_changed"), p_tab); } @@ -460,6 +463,43 @@ void TabContainer::_on_active_tab_rearranged(int p_tab) { emit_signal(SNAME("active_tab_rearranged"), p_tab); } +void TabContainer::_on_tab_visibility_changed(Control *p_child) { + if (updating_visibility) { + return; + } + int tab_index = get_tab_idx_from_control(p_child); + if (tab_index == -1) { + return; + } + // Only allow one tab to be visible. + bool made_visible = p_child->is_visible(); + updating_visibility = true; + + if (!made_visible && get_current_tab() == tab_index) { + if (get_deselect_enabled() || get_tab_count() == 0) { + // Deselect. + set_current_tab(-1); + } else if (get_tab_count() == 1) { + // Only tab, cannot deselect. + p_child->show(); + } else { + // Set a different tab to be the current tab. + bool selected = select_next_available(); + if (!selected) { + selected = select_previous_available(); + } + if (!selected) { + // No available tabs, deselect. + set_current_tab(-1); + } + } + } else if (made_visible && get_current_tab() != tab_index) { + set_current_tab(tab_index); + } + + updating_visibility = false; +} + void TabContainer::_refresh_tab_names() { Vector controls = _get_tab_controls(); for (int i = 0; i < controls.size(); i++) { @@ -490,6 +530,7 @@ void TabContainer::add_child_notify(Node *p_child) { } p_child->connect("renamed", callable_mp(this, &TabContainer::_refresh_tab_names)); + p_child->connect(SNAME("visibility_changed"), callable_mp(this, &TabContainer::_on_tab_visibility_changed).bind(c)); // TabBar won't emit the "tab_changed" signal when not inside the tree. if (!is_inside_tree()) { @@ -547,6 +588,7 @@ void TabContainer::remove_child_notify(Node *p_child) { p_child->remove_meta("_tab_name"); p_child->disconnect("renamed", callable_mp(this, &TabContainer::_refresh_tab_names)); + p_child->disconnect(SNAME("visibility_changed"), callable_mp(this, &TabContainer::_on_tab_visibility_changed)); // TabBar won't emit the "tab_changed" signal when not inside the tree. if (!is_inside_tree()) { @@ -586,6 +628,14 @@ bool TabContainer::select_next_available() { return tab_bar->select_next_available(); } +void TabContainer::set_deselect_enabled(bool p_enabled) { + tab_bar->set_deselect_enabled(p_enabled); +} + +bool TabContainer::get_deselect_enabled() const { + return tab_bar->get_deselect_enabled(); +} + Control *TabContainer::get_tab_control(int p_idx) const { Vector controls = _get_tab_controls(); if (p_idx >= 0 && p_idx < controls.size()) { @@ -950,6 +1000,8 @@ void TabContainer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_use_hidden_tabs_for_min_size"), &TabContainer::get_use_hidden_tabs_for_min_size); ClassDB::bind_method(D_METHOD("set_tab_focus_mode", "focus_mode"), &TabContainer::set_tab_focus_mode); ClassDB::bind_method(D_METHOD("get_tab_focus_mode"), &TabContainer::get_tab_focus_mode); + ClassDB::bind_method(D_METHOD("set_deselect_enabled", "enabled"), &TabContainer::set_deselect_enabled); + ClassDB::bind_method(D_METHOD("get_deselect_enabled"), &TabContainer::get_deselect_enabled); ADD_SIGNAL(MethodInfo("active_tab_rearranged", PropertyInfo(Variant::INT, "idx_to"))); ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab"))); @@ -969,6 +1021,7 @@ void TabContainer::_bind_methods() { ADD_PROPERTY(PropertyInfo(Variant::INT, "tabs_rearrange_group"), "set_tabs_rearrange_group", "get_tabs_rearrange_group"); 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"); ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_focus_mode", PROPERTY_HINT_ENUM, "None,Click,All"), "set_tab_focus_mode", "get_tab_focus_mode"); + ADD_PROPERTY(PropertyInfo(Variant::BOOL, "deselect_enabled"), "set_deselect_enabled", "get_deselect_enabled"); BIND_ENUM_CONSTANT(POSITION_TOP); BIND_ENUM_CONSTANT(POSITION_BOTTOM); diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h index 0c645b4598a7..03eff5d9444a 100644 --- a/scene/gui/tab_container.h +++ b/scene/gui/tab_container.h @@ -56,7 +56,9 @@ class TabContainer : public Container { bool theme_changing = false; Vector children_removing; bool drag_to_rearrange_enabled = false; - int setup_current_tab = -1; + // Set the default setup current tab to be an invalid index. + int setup_current_tab = -2; + bool updating_visibility = false; struct ThemeCache { int side_margin = 0; @@ -108,6 +110,7 @@ class TabContainer : public Container { void _on_tab_selected(int p_tab); void _on_tab_button_pressed(int p_tab); void _on_active_tab_rearranged(int p_tab); + void _on_tab_visibility_changed(Control *p_child); Variant _get_drag_data_fw(const Point2 &p_point, Control *p_from_control); bool _can_drop_data_fw(const Point2 &p_point, const Variant &p_data, Control *p_from_control) const; @@ -174,6 +177,9 @@ class TabContainer : public Container { bool select_previous_available(); bool select_next_available(); + void set_deselect_enabled(bool p_enabled); + bool get_deselect_enabled() const; + Control *get_tab_control(int p_idx) const; Control *get_current_tab_control() const;