From 18811ac8141f2b8dfb45413400643af11d09f2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arkadiusz=20Marcin=20Ko=C5=82ek?= Date: Sun, 25 Jun 2023 17:56:52 +0200 Subject: [PATCH] Allow to focus individual tabs in TabBar/TabContainer --- doc/classes/TabBar.xml | 16 ++++++ doc/classes/TabContainer.xml | 18 +++++++ editor/editor_themes.cpp | 4 ++ scene/gui/tab_bar.cpp | 91 +++++++++++++++++++++++++++++++++-- scene/gui/tab_bar.h | 10 +++- scene/gui/tab_container.cpp | 23 +++++++++ scene/gui/tab_container.h | 7 +++ scene/theme/default_theme.cpp | 3 ++ 8 files changed, 168 insertions(+), 4 deletions(-) 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 940eb89dab8..91fc3621660 100644 --- a/doc/classes/TabContainer.xml +++ b/doc/classes/TabContainer.xml @@ -99,6 +99,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. + + @@ -171,6 +183,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. @@ -291,6 +306,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 98f0f70101b..86ca843d03d 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 14bc87ad402..31a0f7ee165 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); @@ -601,6 +602,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()) { @@ -645,6 +654,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); } @@ -911,6 +928,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_control", "tab_idx"), &TabContainer::get_tab_control); ClassDB::bind_method(D_METHOD("set_tab_alignment", "alignment"), &TabContainer::set_tab_alignment); @@ -943,6 +962,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"))); @@ -960,6 +981,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); @@ -977,6 +999,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 2bcc640d056..8b7e464d454 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; @@ -117,6 +118,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; @@ -149,6 +153,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);