Skip to content

Commit

Permalink
Merge pull request #79104 from DrRevert/focusable_tabs_refactor
Browse files Browse the repository at this point in the history
Allow to focus individual tabs in `TabBar`/`TabContainer`
  • Loading branch information
akien-mga committed Sep 25, 2023
2 parents 1a0e653 + 18811ac commit c57d9f3
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 4 deletions.
16 changes: 16 additions & 0 deletions doc/classes/TabBar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,18 @@
Removes the tab at index [param tab_idx].
</description>
</method>
<method name="select_next_available">
<return type="bool" />
<description>
Selects the first available tab with greater index than the currently selected. Returns [code]true[/code] if tab selection changed.
</description>
</method>
<method name="select_previous_available">
<return type="bool" />
<description>
Selects the first available tab with lower index than the currently selected. Returns [code]true[/code] if tab selection changed.
</description>
</method>
<method name="set_tab_button_icon">
<return type="void" />
<param index="0" name="tab_idx" type="int" />
Expand Down Expand Up @@ -223,6 +235,7 @@
<member name="drag_to_rearrange_enabled" type="bool" setter="set_drag_to_rearrange_enabled" getter="get_drag_to_rearrange_enabled" default="false">
If [code]true[/code], tabs can be rearranged with mouse drag.
</member>
<member name="focus_mode" type="int" setter="set_focus_mode" getter="get_focus_mode" overrides="Control" enum="Control.FocusMode" default="2" />
<member name="max_tab_width" type="int" setter="set_max_tab_width" getter="get_max_tab_width" default="0">
Sets the maximum width which all tabs should be limited to. Unlimited if set to [code]0[/code].
</member>
Expand Down Expand Up @@ -396,6 +409,9 @@
<theme_item name="tab_disabled" data_type="style" type="StyleBox">
The style of disabled tabs.
</theme_item>
<theme_item name="tab_focus" data_type="style" type="StyleBox">
[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.
</theme_item>
<theme_item name="tab_hovered" data_type="style" type="StyleBox">
The style of the currently hovered tab. Does not apply to the selected tab.
</theme_item>
Expand Down
18 changes: 18 additions & 0 deletions doc/classes/TabContainer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@
Returns [code]true[/code] if the tab at index [param tab_idx] is hidden.
</description>
</method>
<method name="select_next_available">
<return type="bool" />
<description>
Selects the first available tab with greater index than the currently selected. Returns [code]true[/code] if tab selection changed.
</description>
</method>
<method name="select_previous_available">
<return type="bool" />
<description>
Selects the first available tab with lower index than the currently selected. Returns [code]true[/code] if tab selection changed.
</description>
</method>
<method name="set_popup">
<return type="void" />
<param index="0" name="popup" type="Node" />
Expand Down Expand Up @@ -178,6 +190,9 @@
<member name="tab_alignment" type="int" setter="set_tab_alignment" getter="get_tab_alignment" enum="TabBar.AlignmentMode" default="0">
Sets the position at which tabs will be placed. See [enum TabBar.AlignmentMode] for details.
</member>
<member name="tab_focus_mode" type="int" setter="set_tab_focus_mode" getter="get_tab_focus_mode" enum="Control.FocusMode" default="2">
The focus access mode for the internal [TabBar] node.
</member>
<member name="tabs_rearrange_group" type="int" setter="set_tabs_rearrange_group" getter="get_tabs_rearrange_group" default="-1">
[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.
Expand Down Expand Up @@ -298,6 +313,9 @@
<theme_item name="tab_disabled" data_type="style" type="StyleBox">
The style of disabled tabs.
</theme_item>
<theme_item name="tab_focus" data_type="style" type="StyleBox">
[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.
</theme_item>
<theme_item name="tab_hovered" data_type="style" type="StyleBox">
The style of the currently hovered tab.
</theme_item>
Expand Down
4 changes: 4 additions & 0 deletions editor/editor_themes.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,8 @@ Ref<Theme> create_editor_theme(const Ref<Theme> p_theme) {
style_tab_disabled->set_bg_color(disabled_bg_color);
style_tab_disabled->set_border_color(disabled_bg_color);

Ref<StyleBoxFlat> style_tab_focus = style_widget_focus->duplicate();

// Editor background
Color background_color_opaque = background_color;
background_color_opaque.a = 1.0;
Expand Down Expand Up @@ -1536,10 +1538,12 @@ Ref<Theme> create_editor_theme(const Ref<Theme> 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);
Expand Down
91 changes: 88 additions & 3 deletions scene/gui/tab_bar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,34 @@ void TabBar::gui_input(const Ref<InputEvent> &p_event) {
}
}
}

if (p_event->is_pressed()) {
Input *input = Input::get_singleton();
Ref<InputEventJoypadMotion> joypadmotion_event = p_event;
Ref<InputEventJoypadButton> 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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -390,7 +440,7 @@ void TabBar::_notification(int p_what) {
Ref<StyleBox> 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) {
Expand Down Expand Up @@ -456,12 +506,16 @@ void TabBar::_notification(int p_what) {
}
}

void TabBar::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x) {
void TabBar::_draw_tab(Ref<StyleBox> &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<StyleBox> 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);

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand All @@ -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));
}
10 changes: 9 additions & 1 deletion scene/gui/tab_bar.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ class TabBar : public Control {
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;
Expand All @@ -115,6 +119,7 @@ class TabBar : public Control {
Ref<StyleBox> tab_hovered_style;
Ref<StyleBox> tab_selected_style;
Ref<StyleBox> tab_disabled_style;
Ref<StyleBox> tab_focus_style;

Ref<Texture2D> increment_icon;
Ref<Texture2D> increment_hl_icon;
Expand Down Expand Up @@ -148,7 +153,7 @@ class TabBar : public Control {
void _on_mouse_exited();

void _shape(int p_tab);
void _draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x);
void _draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, int p_index, float p_x, bool p_focus);

protected:
virtual void gui_input(const Ref<InputEvent> &p_event) override;
Expand Down Expand Up @@ -214,6 +219,9 @@ class TabBar : public Control {
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;

Expand Down
23 changes: 23 additions & 0 deletions scene/gui/tab_container.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<Control *> controls = _get_tab_controls();
if (p_idx >= 0 && p_idx < controls.size()) {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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")));
Expand All @@ -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);

Expand All @@ -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");
Expand Down
7 changes: 7 additions & 0 deletions scene/gui/tab_container.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class TabContainer : public Container {
Ref<StyleBox> tab_hovered_style;
Ref<StyleBox> tab_selected_style;
Ref<StyleBox> tab_disabled_style;
Ref<StyleBox> tab_focus_style;

Ref<Texture2D> increment_icon;
Ref<Texture2D> increment_hl_icon;
Expand Down Expand Up @@ -119,6 +120,9 @@ class TabContainer : public Container {
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;

Expand Down Expand Up @@ -151,6 +155,9 @@ class TabContainer : public Container {
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;

Expand Down
Loading

0 comments on commit c57d9f3

Please sign in to comment.