diff --git a/doc/classes/TabBar.xml b/doc/classes/TabBar.xml
index 8b3bac456a1..bfcb1a0e69c 100644
--- a/doc/classes/TabBar.xml
+++ b/doc/classes/TabBar.xml
@@ -140,6 +140,18 @@
Removes the tab at index [param tab_idx].
+
+
+
+ Selects the first available tab with greater index than the currently selected. Returns [code]true[/code] if tab selection changed.
+
+
+
+
+
+ Selects the first available tab with lower index than the currently selected. Returns [code]true[/code] if tab selection changed.
+
+
@@ -223,6 +235,7 @@
If [code]true[/code], tabs can be rearranged with mouse drag.
+
Sets the maximum width which all tabs should be limited to. Unlimited if set to [code]0[/code].
@@ -396,6 +409,9 @@
The style of disabled tabs.
+
+ [StyleBox] used when the [TabBar] is focused. The [theme_item tab_focus] [StyleBox] is displayed [i]over[/i] the base [StyleBox] of the selected tab, so a partially transparent [StyleBox] should be used to ensure the base [StyleBox] remains visible. A [StyleBox] that represents an outline or an underline works well for this purpose. To disable the focus visual effect, assign a [StyleBoxEmpty] resource. Note that disabling the focus visual effect will harm keyboard/controller navigation usability, so this is not recommended for accessibility reasons.
+
The style of the currently hovered tab. Does not apply to the selected tab.
diff --git a/doc/classes/TabContainer.xml b/doc/classes/TabContainer.xml
index 013ab3bd778..07f6dbd8c59 100644
--- a/doc/classes/TabContainer.xml
+++ b/doc/classes/TabContainer.xml
@@ -106,6 +106,18 @@
Returns [code]true[/code] if the tab at index [param tab_idx] is hidden.
+
+
+
+ Selects the first available tab with greater index than the currently selected. Returns [code]true[/code] if tab selection changed.
+
+
+
+
+
+ Selects the first available tab with lower index than the currently selected. Returns [code]true[/code] if tab selection changed.
+
+
@@ -178,6 +190,9 @@
Sets the position at which tabs will be placed. See [enum TabBar.AlignmentMode] for details.
+
+ The focus access mode for the internal [TabBar] node.
+
[TabContainer]s with the same rearrange group ID will allow dragging the tabs between them. Enable drag with [member drag_to_rearrange_enabled].
Setting this to [code]-1[/code] will disable rearranging between [TabContainer]s.
@@ -298,6 +313,9 @@
The style of disabled tabs.
+
+ [StyleBox] used when the [TabBar] is focused. The [theme_item tab_focus] [StyleBox] is displayed [i]over[/i] the base [StyleBox] of the selected tab, so a partially transparent [StyleBox] should be used to ensure the base [StyleBox] remains visible. A [StyleBox] that represents an outline or an underline works well for this purpose. To disable the focus visual effect, assign a [StyleBoxEmpty] resource. Note that disabling the focus visual effect will harm keyboard/controller navigation usability, so this is not recommended for accessibility reasons.
+
The style of the currently hovered tab.
diff --git a/editor/editor_themes.cpp b/editor/editor_themes.cpp
index c5ab3698e20..4b88c7c1032 100644
--- a/editor/editor_themes.cpp
+++ b/editor/editor_themes.cpp
@@ -913,6 +913,8 @@ Ref create_editor_theme(const Ref p_theme) {
style_tab_disabled->set_bg_color(disabled_bg_color);
style_tab_disabled->set_border_color(disabled_bg_color);
+ Ref style_tab_focus = style_widget_focus->duplicate();
+
// Editor background
Color background_color_opaque = background_color;
background_color_opaque.a = 1.0;
@@ -1536,10 +1538,12 @@ Ref create_editor_theme(const Ref p_theme) {
theme->set_stylebox("tab_hovered", "TabContainer", style_tab_hovered);
theme->set_stylebox("tab_unselected", "TabContainer", style_tab_unselected);
theme->set_stylebox("tab_disabled", "TabContainer", style_tab_disabled);
+ theme->set_stylebox("tab_focus", "TabContainer", style_tab_focus);
theme->set_stylebox("tab_selected", "TabBar", style_tab_selected);
theme->set_stylebox("tab_hovered", "TabBar", style_tab_hovered);
theme->set_stylebox("tab_unselected", "TabBar", style_tab_unselected);
theme->set_stylebox("tab_disabled", "TabBar", style_tab_disabled);
+ theme->set_stylebox("tab_focus", "TabBar", style_tab_focus);
theme->set_stylebox("button_pressed", "TabBar", style_menu);
theme->set_stylebox("button_highlight", "TabBar", style_menu);
theme->set_color("font_selected_color", "TabContainer", font_color);
diff --git a/scene/gui/tab_bar.cpp b/scene/gui/tab_bar.cpp
index a9c16a99424..14eed4c7a26 100644
--- a/scene/gui/tab_bar.cpp
+++ b/scene/gui/tab_bar.cpp
@@ -290,6 +290,34 @@ void TabBar::gui_input(const Ref &p_event) {
}
}
}
+
+ if (p_event->is_pressed()) {
+ Input *input = Input::get_singleton();
+ Ref joypadmotion_event = p_event;
+ Ref joypadbutton_event = p_event;
+ bool is_joypad_event = (joypadmotion_event.is_valid() || joypadbutton_event.is_valid());
+ if (p_event->is_action("ui_right", true)) {
+ if (is_joypad_event) {
+ if (!input->is_action_just_pressed("ui_right", true)) {
+ return;
+ }
+ set_process_internal(true);
+ }
+ if (is_layout_rtl() ? select_previous_available() : select_next_available()) {
+ accept_event();
+ }
+ } else if (p_event->is_action("ui_left", true)) {
+ if (is_joypad_event) {
+ if (!input->is_action_just_pressed("ui_left", true)) {
+ return;
+ }
+ set_process_internal(true);
+ }
+ if (is_layout_rtl() ? select_next_available() : select_previous_available()) {
+ accept_event();
+ }
+ }
+ }
}
void TabBar::_shape(int p_tab) {
@@ -307,6 +335,28 @@ void TabBar::_shape(int p_tab) {
void TabBar::_notification(int p_what) {
switch (p_what) {
+ case NOTIFICATION_INTERNAL_PROCESS: {
+ Input *input = Input::get_singleton();
+
+ if (input->is_action_just_released("ui_left") || input->is_action_just_released("ui_right")) {
+ gamepad_event_delay_ms = DEFAULT_GAMEPAD_EVENT_DELAY_MS;
+ set_process_internal(false);
+ return;
+ }
+
+ gamepad_event_delay_ms -= get_process_delta_time();
+ if (gamepad_event_delay_ms <= 0) {
+ gamepad_event_delay_ms = GAMEPAD_EVENT_REPEAT_RATE_MS + gamepad_event_delay_ms;
+ if (input->is_action_pressed("ui_right")) {
+ is_layout_rtl() ? select_previous_available() : select_next_available();
+ }
+
+ if (input->is_action_pressed("ui_left")) {
+ is_layout_rtl() ? select_next_available() : select_previous_available();
+ }
+ }
+ } break;
+
case NOTIFICATION_LAYOUT_DIRECTION_CHANGED: {
queue_redraw();
} break;
@@ -379,7 +429,7 @@ void TabBar::_notification(int p_what) {
col = theme_cache.font_unselected_color;
}
- _draw_tab(sb, col, i, rtl ? size.width - ofs - tabs[i].size_cache : ofs);
+ _draw_tab(sb, col, i, rtl ? size.width - ofs - tabs[i].size_cache : ofs, false);
}
ofs += tabs[i].size_cache;
@@ -390,7 +440,7 @@ void TabBar::_notification(int p_what) {
Ref sb = tabs[current].disabled ? theme_cache.tab_disabled_style : theme_cache.tab_selected_style;
float x = rtl ? size.width - tabs[current].ofs_cache - tabs[current].size_cache : tabs[current].ofs_cache;
- _draw_tab(sb, theme_cache.font_selected_color, current, x);
+ _draw_tab(sb, theme_cache.font_selected_color, current, x, has_focus());
}
if (buttons_visible) {
@@ -456,12 +506,16 @@ void TabBar::_notification(int p_what) {
}
}
-void TabBar::_draw_tab(Ref &p_tab_style, Color &p_font_color, int p_index, float p_x) {
+void TabBar::_draw_tab(Ref &p_tab_style, Color &p_font_color, int p_index, float p_x, bool p_focus) {
RID ci = get_canvas_item();
bool rtl = is_layout_rtl();
Rect2 sb_rect = Rect2(p_x, 0, tabs[p_index].size_cache, get_size().height);
p_tab_style->draw(ci, sb_rect);
+ if (p_focus) {
+ Ref focus_style = theme_cache.tab_focus_style;
+ focus_style->draw(ci, sb_rect);
+ }
p_x += rtl ? tabs[p_index].size_cache - p_tab_style->get_margin(SIDE_LEFT) : p_tab_style->get_margin(SIDE_LEFT);
@@ -607,6 +661,33 @@ int TabBar::get_hovered_tab() const {
return hover;
}
+bool TabBar::select_previous_available() {
+ const int offset_end = (get_current_tab() + 1);
+ for (int i = 1; i < offset_end; i++) {
+ int target_tab = get_current_tab() - i;
+ if (target_tab < 0) {
+ target_tab += get_tab_count();
+ }
+ if (!is_tab_disabled(target_tab)) {
+ set_current_tab(target_tab);
+ return true;
+ }
+ }
+ return false;
+}
+
+bool TabBar::select_next_available() {
+ const int offset_end = (get_tab_count() - get_current_tab());
+ for (int i = 1; i < offset_end; i++) {
+ int target_tab = (get_current_tab() + i) % get_tab_count();
+ if (!is_tab_disabled(target_tab)) {
+ set_current_tab(target_tab);
+ return true;
+ }
+ }
+ return false;
+}
+
int TabBar::get_tab_offset() const {
return offset;
}
@@ -1589,6 +1670,8 @@ void TabBar::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_current_tab", "tab_idx"), &TabBar::set_current_tab);
ClassDB::bind_method(D_METHOD("get_current_tab"), &TabBar::get_current_tab);
ClassDB::bind_method(D_METHOD("get_previous_tab"), &TabBar::get_previous_tab);
+ ClassDB::bind_method(D_METHOD("select_previous_available"), &TabBar::select_previous_available);
+ ClassDB::bind_method(D_METHOD("select_next_available"), &TabBar::select_next_available);
ClassDB::bind_method(D_METHOD("set_tab_title", "tab_idx", "title"), &TabBar::set_tab_title);
ClassDB::bind_method(D_METHOD("get_tab_title", "tab_idx"), &TabBar::get_tab_title);
ClassDB::bind_method(D_METHOD("set_tab_text_direction", "tab_idx", "direction"), &TabBar::set_tab_text_direction);
@@ -1674,6 +1757,7 @@ void TabBar::_bind_methods() {
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, tab_hovered_style, "tab_hovered");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, tab_selected_style, "tab_selected");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, tab_disabled_style, "tab_disabled");
+ BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabBar, tab_focus_style, "tab_focus");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabBar, increment_icon, "increment");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabBar, increment_hl_icon, "increment_highlight");
@@ -1699,5 +1783,6 @@ void TabBar::_bind_methods() {
TabBar::TabBar() {
set_size(Size2(get_size().width, get_minimum_size().height));
+ set_focus_mode(FOCUS_ALL);
connect("mouse_exited", callable_mp(this, &TabBar::_on_mouse_exited));
}
diff --git a/scene/gui/tab_bar.h b/scene/gui/tab_bar.h
index d89b1866bff..b79c170a7b9 100644
--- a/scene/gui/tab_bar.h
+++ b/scene/gui/tab_bar.h
@@ -107,6 +107,10 @@ private:
bool scroll_to_selected = true;
int tabs_rearrange_group = -1;
+ const float DEFAULT_GAMEPAD_EVENT_DELAY_MS = 0.5;
+ const float GAMEPAD_EVENT_REPEAT_RATE_MS = 1.0 / 20;
+ float gamepad_event_delay_ms = DEFAULT_GAMEPAD_EVENT_DELAY_MS;
+
struct ThemeCache {
int h_separation = 0;
int icon_max_width = 0;
@@ -115,6 +119,7 @@ private:
Ref tab_hovered_style;
Ref tab_selected_style;
Ref tab_disabled_style;
+ Ref tab_focus_style;
Ref increment_icon;
Ref increment_hl_icon;
@@ -148,7 +153,7 @@ private:
void _on_mouse_exited();
void _shape(int p_tab);
- void _draw_tab(Ref &p_tab_style, Color &p_font_color, int p_index, float p_x);
+ void _draw_tab(Ref &p_tab_style, Color &p_font_color, int p_index, float p_x, bool p_focus);
protected:
virtual void gui_input(const Ref &p_event) override;
@@ -214,6 +219,9 @@ public:
int get_previous_tab() const;
int get_hovered_tab() const;
+ bool select_previous_available();
+ bool select_next_available();
+
int get_tab_offset() const;
bool get_offset_buttons_visible() const;
diff --git a/scene/gui/tab_container.cpp b/scene/gui/tab_container.cpp
index bad85560d14..b757b516d19 100644
--- a/scene/gui/tab_container.cpp
+++ b/scene/gui/tab_container.cpp
@@ -195,6 +195,7 @@ void TabContainer::_on_theme_changed() {
tab_bar->add_theme_style_override(SNAME("tab_hovered"), theme_cache.tab_hovered_style);
tab_bar->add_theme_style_override(SNAME("tab_selected"), theme_cache.tab_selected_style);
tab_bar->add_theme_style_override(SNAME("tab_disabled"), theme_cache.tab_disabled_style);
+ tab_bar->add_theme_style_override(SNAME("tab_focus"), theme_cache.tab_focus_style);
tab_bar->add_theme_icon_override(SNAME("increment"), theme_cache.increment_icon);
tab_bar->add_theme_icon_override(SNAME("increment_highlight"), theme_cache.increment_hl_icon);
@@ -605,6 +606,14 @@ int TabContainer::get_previous_tab() const {
return tab_bar->get_previous_tab();
}
+bool TabContainer::select_previous_available() {
+ return tab_bar->select_previous_available();
+}
+
+bool TabContainer::select_next_available() {
+ return tab_bar->select_next_available();
+}
+
Control *TabContainer::get_tab_control(int p_idx) const {
Vector controls = _get_tab_controls();
if (p_idx >= 0 && p_idx < controls.size()) {
@@ -649,6 +658,14 @@ TabBar::AlignmentMode TabContainer::get_tab_alignment() const {
return tab_bar->get_tab_alignment();
}
+void TabContainer::set_tab_focus_mode(Control::FocusMode p_focus_mode) {
+ tab_bar->set_focus_mode(p_focus_mode);
+}
+
+Control::FocusMode TabContainer::get_tab_focus_mode() const {
+ return tab_bar->get_focus_mode();
+}
+
void TabContainer::set_clip_tabs(bool p_clip_tabs) {
tab_bar->set_clip_tabs(p_clip_tabs);
}
@@ -915,6 +932,8 @@ void TabContainer::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_current_tab", "tab_idx"), &TabContainer::set_current_tab);
ClassDB::bind_method(D_METHOD("get_current_tab"), &TabContainer::get_current_tab);
ClassDB::bind_method(D_METHOD("get_previous_tab"), &TabContainer::get_previous_tab);
+ ClassDB::bind_method(D_METHOD("select_previous_available"), &TabContainer::select_previous_available);
+ ClassDB::bind_method(D_METHOD("select_next_available"), &TabContainer::select_next_available);
ClassDB::bind_method(D_METHOD("get_current_tab_control"), &TabContainer::get_current_tab_control);
ClassDB::bind_method(D_METHOD("get_tab_bar"), &TabContainer::get_tab_bar);
ClassDB::bind_method(D_METHOD("get_tab_control", "tab_idx"), &TabContainer::get_tab_control);
@@ -948,6 +967,8 @@ void TabContainer::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_tabs_rearrange_group"), &TabContainer::get_tabs_rearrange_group);
ClassDB::bind_method(D_METHOD("set_use_hidden_tabs_for_min_size", "enabled"), &TabContainer::set_use_hidden_tabs_for_min_size);
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);
ADD_SIGNAL(MethodInfo("active_tab_rearranged", PropertyInfo(Variant::INT, "idx_to")));
ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab")));
@@ -965,6 +986,7 @@ void TabContainer::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled");
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");
BIND_THEME_ITEM(Theme::DATA_TYPE_CONSTANT, TabContainer, side_margin);
@@ -982,6 +1004,7 @@ void TabContainer::_bind_methods() {
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tab_hovered_style, "tab_hovered");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tab_selected_style, "tab_selected");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tab_disabled_style, "tab_disabled");
+ BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_STYLEBOX, TabContainer, tab_focus_style, "tab_focus");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, increment_icon, "increment");
BIND_THEME_ITEM_CUSTOM(Theme::DATA_TYPE_ICON, TabContainer, increment_hl_icon, "increment_highlight");
diff --git a/scene/gui/tab_container.h b/scene/gui/tab_container.h
index 9919565f1eb..a831416612d 100644
--- a/scene/gui/tab_container.h
+++ b/scene/gui/tab_container.h
@@ -66,6 +66,7 @@ class TabContainer : public Container {
Ref tab_hovered_style;
Ref tab_selected_style;
Ref tab_disabled_style;
+ Ref tab_focus_style;
Ref increment_icon;
Ref increment_hl_icon;
@@ -119,6 +120,9 @@ public:
void set_tab_alignment(TabBar::AlignmentMode p_alignment);
TabBar::AlignmentMode get_tab_alignment() const;
+ void set_tab_focus_mode(FocusMode p_focus_mode);
+ FocusMode get_tab_focus_mode() const;
+
void set_clip_tabs(bool p_clip_tabs);
bool get_clip_tabs() const;
@@ -151,6 +155,9 @@ public:
int get_current_tab() const;
int get_previous_tab() const;
+ bool select_previous_available();
+ bool select_next_available();
+
Control *get_tab_control(int p_idx) const;
Control *get_current_tab_control() const;
diff --git a/scene/theme/default_theme.cpp b/scene/theme/default_theme.cpp
index 7efbc74bf38..ef4afbb5fa5 100644
--- a/scene/theme/default_theme.cpp
+++ b/scene/theme/default_theme.cpp
@@ -845,11 +845,13 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const
style_tab_disabled->set_bg_color(style_disabled_color);
Ref style_tab_hovered = style_tab_unselected->duplicate();
style_tab_hovered->set_bg_color(Color(0.1, 0.1, 0.1, 0.3));
+ Ref style_tab_focus = focus->duplicate();
theme->set_stylebox("tab_selected", "TabContainer", style_tab_selected);
theme->set_stylebox("tab_hovered", "TabContainer", style_tab_hovered);
theme->set_stylebox("tab_unselected", "TabContainer", style_tab_unselected);
theme->set_stylebox("tab_disabled", "TabContainer", style_tab_disabled);
+ theme->set_stylebox("tab_focus", "TabContainer", style_tab_focus);
theme->set_stylebox("panel", "TabContainer", make_flat_stylebox(style_normal_color, 0, 0, 0, 0));
theme->set_stylebox("tabbar_background", "TabContainer", make_empty_stylebox(0, 0, 0, 0));
@@ -882,6 +884,7 @@ void fill_default_theme(Ref &theme, const Ref &default_font, const
theme->set_stylebox("tab_hovered", "TabBar", style_tab_hovered);
theme->set_stylebox("tab_unselected", "TabBar", style_tab_unselected);
theme->set_stylebox("tab_disabled", "TabBar", style_tab_disabled);
+ theme->set_stylebox("tab_focus", "TabBar", style_tab_focus);
theme->set_stylebox("button_pressed", "TabBar", button_pressed);
theme->set_stylebox("button_highlight", "TabBar", button_normal);