Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Focusable Tabs and TabContainer, tabs can be changed using keyboard and gamepad #49928

Closed
wants to merge 10 commits into from
7 changes: 7 additions & 0 deletions doc/classes/TabContainer.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@
<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" override="true" enum="Control.FocusMode" default="2" />
<member name="rollover" type="bool" setter="set_rollover" getter="get_rollover" default="false">
If [code]true[/code], pressing [code]ui_right[/code] on the last tab will select the first tab and pressing [code]ui_left[/code] on the first tab will select the last one.
</member>
<member name="tab_align" type="int" setter="set_tab_align" getter="get_tab_align" enum="TabContainer.TabAlign" default="1">
The alignment of all tabs in the tab container. See the [enum TabAlign] constants for details.
</member>
Expand Down Expand Up @@ -195,6 +199,9 @@
<theme_item name="decrement_highlight" type="Texture2D">
Icon for the left arrow button that appears when there are too many tabs to fit in the container width. Used when the button is being hovered with the cursor.
</theme_item>
<theme_item name="focus" type="StyleBox">
[StyleBox] used on the selected tab when the [TabContainer] is focused. It is displayed over the current [StyleBox], so using [StyleBoxEmpty] will just disable the focus visual effect.
</theme_item>
<theme_item name="font" type="Font">
The font used to draw tab names.
</theme_item>
Expand Down
7 changes: 7 additions & 0 deletions doc/classes/Tabs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@
<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" override="true" enum="Control.FocusMode" default="2" />
<member name="rollover" type="bool" setter="set_rollover" getter="get_rollover" default="false">
If [code]true[/code], pressing [code]ui_right[/code] on the last tab will select the first tab and pressing [code]ui_left[/code] on the first tab will select the last one.
</member>
<member name="scrolling_enabled" type="bool" setter="set_scrolling_enabled" getter="get_scrolling_enabled" default="true">
if [code]true[/code], the mouse's scroll wheel can be used to navigate the scroll view.
</member>
Expand Down Expand Up @@ -359,6 +363,9 @@
<theme_item name="decrement_highlight" type="Texture2D">
Icon for the left arrow button that appears when there are too many tabs to fit in the container width. Used when the button is being hovered with the cursor.
</theme_item>
<theme_item name="focus" type="StyleBox">
[StyleBox] used on the selected tab when the [Tabs] node is focused. It is displayed over the current [StyleBox], so using [StyleBoxEmpty] will just disable the focus visual effect.
</theme_item>
<theme_item name="font" type="Font">
The font used to draw tab names.
</theme_item>
Expand Down
74 changes: 74 additions & 0 deletions scene/gui/tab_container.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,63 @@ void TabContainer::_gui_input(const Ref<InputEvent> &p_event) {
}
}
}
if (!mm.is_valid() && !mb.is_valid()) {
if (p_event->is_action_pressed("ui_right")) {
int next_tab = get_current_tab() + 1;
bool valid = true;
while (valid) {
if (next_tab == get_current_tab()) {
valid = false;
break;
}

if (next_tab < get_tab_count()) {
if (get_tab_disabled(next_tab)) {
++next_tab;
} else {
break;
}
} else {
if (rollover) {
next_tab = 0;
} else {
valid = false;
}
}
}
if (valid) {
set_current_tab(next_tab);
accept_event();
}
} else if (p_event->is_action_pressed("ui_left")) {
int prev_tab = get_current_tab() - 1;
bool valid = true;
while (valid) {
if (prev_tab == get_current_tab()) {
valid = false;
break;
}

if (prev_tab >= 0) {
if (get_tab_disabled(prev_tab)) {
--prev_tab;
} else {
break;
}
} else {
if (rollover) {
prev_tab = get_tab_count() - 1;
} else {
valid = false;
}
}
}
if (valid) {
set_current_tab(prev_tab);
accept_event();
}
}
}
}

void TabContainer::_notification(int p_what) {
Expand Down Expand Up @@ -548,6 +605,10 @@ void TabContainer::_draw_tab(Ref<StyleBox> &p_tab_style, Color &p_font_color, in
Rect2 tab_rect(p_x, 0, tab_width, header_height);
p_tab_style->draw(canvas, tab_rect);

if (p_index == current && has_focus()) {
Ref<StyleBox> style2 = get_theme_stylebox("focus");
style2->draw(canvas, tab_rect);
}
// Draw the tab contents.
Control *control = Object::cast_to<Control>(tabs[p_index]);
String text = control->has_meta("_tab_name") ? String(tr(String(control->get_meta("_tab_name")))) : String(tr(control->get_name()));
Expand Down Expand Up @@ -1198,6 +1259,14 @@ bool TabContainer::get_use_hidden_tabs_for_min_size() const {
return use_hidden_tabs_for_min_size;
}

void TabContainer::set_rollover(bool p_enabled) {
rollover = p_enabled;
}

bool TabContainer::get_rollover() const {
return rollover;
}

void TabContainer::_bind_methods() {
ClassDB::bind_method(D_METHOD("_gui_input"), &TabContainer::_gui_input);
ClassDB::bind_method(D_METHOD("get_tab_count"), &TabContainer::get_tab_count);
Expand Down Expand Up @@ -1228,6 +1297,9 @@ void TabContainer::_bind_methods() {
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_rollover", "enabled"), &TabContainer::set_rollover);
ClassDB::bind_method(D_METHOD("get_rollover"), &TabContainer::get_rollover);

ClassDB::bind_method(D_METHOD("_on_theme_changed"), &TabContainer::_on_theme_changed);
ClassDB::bind_method(D_METHOD("_update_current_tab"), &TabContainer::_update_current_tab);

Expand All @@ -1241,12 +1313,14 @@ void TabContainer::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "all_tabs_in_front"), "set_all_tabs_in_front", "is_all_tabs_in_front");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled");
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::BOOL, "rollover"), "set_rollover", "get_rollover");

BIND_ENUM_CONSTANT(ALIGN_LEFT);
BIND_ENUM_CONSTANT(ALIGN_CENTER);
BIND_ENUM_CONSTANT(ALIGN_RIGHT);
}

TabContainer::TabContainer() {
set_focus_mode(FOCUS_ALL);
connect("mouse_exited", callable_mp(this, &TabContainer::_on_mouse_exited));
}
3 changes: 3 additions & 0 deletions scene/gui/tab_container.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class TabContainer : public Container {
bool drag_to_rearrange_enabled = false;
bool use_hidden_tabs_for_min_size = false;
int tabs_rearrange_group = -1;
bool rollover = false;

Vector<Ref<TextLine>> text_buf;
Vector<Control *> _get_tabs() const;
Expand Down Expand Up @@ -134,6 +135,8 @@ class TabContainer : public Container {
void set_use_hidden_tabs_for_min_size(bool p_use_hidden_tabs);
bool get_use_hidden_tabs_for_min_size() const;

void set_rollover(bool p_enabled);
bool get_rollover() const;
TabContainer();
Comment on lines +138 to 140
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't mind, please try to cluster likewise functions together with a space between them. E.G:

Suggested change
void set_rollover(bool p_enabled);
bool get_rollover() const;
TabContainer();
void set_rollover(bool p_enabled);
bool get_rollover() const;
TabContainer();

};

Expand Down
75 changes: 75 additions & 0 deletions scene/gui/tabs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,63 @@ void Tabs::_gui_input(const Ref<InputEvent> &p_event) {
}
}
}
if (!mm.is_valid() && !mb.is_valid()) {
if (p_event->is_action_pressed("ui_right")) {
int next_tab = get_current_tab() + 1;
bool valid = true;
while (valid) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why this needs to be a while loop?

It seems to me like a for loop which acts as an offset from an origin is more appropriate and easier to read.

int origin_tab = get_current_tab();
for (int offset = 0; offset < get_tab_count(); offset++) {
	int next_tab = (origin_tab + offset) % get_tab_count(); 
	bool valid = get_tab_disabled(next_tab);
	
	if (!rollover) {
		valid &= next_tab > origin_tab;
		
		if (!valid) {
			break;
		}
	}	

	if (valid) {
		set_current_tab(next_tab);
		accept_event();
		break;
	}
}

This might not cover all use cases or compile completely as I'm writing it mostly pseudo code, but something along those lines seems a bit easier to read to me. It also doesn't require checking for self as it will naturally wrap around with the use of modulo.

Copy link
Contributor Author

@DrRevert DrRevert Jun 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really, I just found while loop being easiest way to implement searching for a valid tab and didn't thought of for loop.

if (next_tab == get_current_tab()) {
valid = false;
break;
}

if (next_tab < get_tab_count()) {
if (get_tab_disabled(next_tab)) {
++next_tab;
} else {
break;
}
} else {
if (rollover) {
next_tab = 0;
} else {
valid = false;
}
}
}
if (valid) {
set_current_tab(next_tab);
accept_event();
}
} else if (p_event->is_action_pressed("ui_left")) {
int prev_tab = get_current_tab() - 1;
bool valid = true;
while (valid) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the last comment but inverse direction. Modulo won't work here, so you'll have to appropriately index wrap as you see fit.

if (prev_tab == get_current_tab()) {
valid = false;
break;
}

if (prev_tab >= 0) {
if (get_tab_disabled(prev_tab)) {
--prev_tab;
} else {
break;
}
} else {
if (rollover) {
prev_tab = get_tab_count() - 1;
} else {
valid = false;
}
}
}
if (valid) {
set_current_tab(prev_tab);
accept_event();
}
}
}
}

void Tabs::_shape(int p_tab) {
Expand Down Expand Up @@ -352,6 +409,11 @@ void Tabs::_notification(int p_what) {
}
sb->draw(ci, sb_rect);

if (i == current && has_focus()) {
Ref<StyleBox> style2 = get_theme_stylebox("focus");
style2->draw(ci, sb_rect);
}

w += sb->get_margin(SIDE_LEFT);

Size2i sb_ms = sb->get_minimum_size();
Expand Down Expand Up @@ -1106,6 +1168,14 @@ bool Tabs::get_select_with_rmb() const {
return select_with_rmb;
}

void Tabs::set_rollover(bool p_enabled) {
rollover = p_enabled;
}

bool Tabs::get_rollover() const {
return rollover;
}

void Tabs::_bind_methods() {
ClassDB::bind_method(D_METHOD("_gui_input"), &Tabs::_gui_input);
ClassDB::bind_method(D_METHOD("_update_hover"), &Tabs::_update_hover);
Expand Down Expand Up @@ -1149,6 +1219,9 @@ void Tabs::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_select_with_rmb", "enabled"), &Tabs::set_select_with_rmb);
ClassDB::bind_method(D_METHOD("get_select_with_rmb"), &Tabs::get_select_with_rmb);

ClassDB::bind_method(D_METHOD("set_rollover", "enabled"), &Tabs::set_rollover);
ClassDB::bind_method(D_METHOD("get_rollover"), &Tabs::get_rollover);

ADD_SIGNAL(MethodInfo("tab_changed", PropertyInfo(Variant::INT, "tab")));
ADD_SIGNAL(MethodInfo("right_button_pressed", PropertyInfo(Variant::INT, "tab")));
ADD_SIGNAL(MethodInfo("tab_closed", PropertyInfo(Variant::INT, "tab")));
Expand All @@ -1162,6 +1235,7 @@ void Tabs::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::INT, "tab_close_display_policy", PROPERTY_HINT_ENUM, "Show Never,Show Active Only,Show Always"), "set_tab_close_display_policy", "get_tab_close_display_policy");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "scrolling_enabled"), "set_scrolling_enabled", "get_scrolling_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "drag_to_rearrange_enabled"), "set_drag_to_rearrange_enabled", "get_drag_to_rearrange_enabled");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "rollover"), "set_rollover", "get_rollover");

BIND_ENUM_CONSTANT(ALIGN_LEFT);
BIND_ENUM_CONSTANT(ALIGN_CENTER);
Expand All @@ -1175,5 +1249,6 @@ void Tabs::_bind_methods() {
}

Tabs::Tabs() {
set_focus_mode(FOCUS_ALL);
connect("mouse_exited", callable_mp(this, &Tabs::_on_mouse_exited));
}
3 changes: 3 additions & 0 deletions scene/gui/tabs.h
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class Tabs : public Control {
bool scrolling_enabled = true;
bool drag_to_rearrange_enabled = false;
int tabs_rearrange_group = -1;
bool rollover = false;

int get_tab_width(int p_idx) const;
void _ensure_no_over_offset();
Expand Down Expand Up @@ -181,6 +182,8 @@ class Tabs : public Control {
void set_select_with_rmb(bool p_enabled);
bool get_select_with_rmb() const;

void set_rollover(bool p_enabled);
bool get_rollover() const;
void ensure_tab_visible(int p_idx);
void set_min_width(int p_width);

Expand Down
2 changes: 2 additions & 0 deletions scene/resources/default_theme/default_theme.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
theme->set_stylebox("tab_unselected", "TabContainer", sb_expand(make_stylebox(tab_behind_png, 5, 5, 5, 1, 16, 6, 16, 4), 3, 0, 3, 3));
theme->set_stylebox("tab_disabled", "TabContainer", sb_expand(make_stylebox(tab_disabled_png, 5, 5, 5, 1, 16, 6, 16, 4), 3, 0, 3, 3));
theme->set_stylebox("panel", "TabContainer", tc_sb);
theme->set_stylebox("focus", "TabContainer", sb_expand(make_stylebox(button_focus_png, 4, 4, 4, 4, 6, 2, 6, 2), 2, 2, 2, 2));

theme->set_icon("increment", "TabContainer", make_icon(scroll_button_right_png));
theme->set_icon("increment_highlight", "TabContainer", make_icon(scroll_button_right_hl_png));
Expand Down Expand Up @@ -792,6 +793,7 @@ void fill_default_theme(Ref<Theme> &theme, const Ref<Font> &default_font, const
theme->set_stylebox("tab_disabled", "Tabs", sb_expand(make_stylebox(tab_disabled_png, 5, 5, 5, 1, 16, 6, 16, 4), 3, 0, 3, 3));
theme->set_stylebox("button_pressed", "Tabs", make_stylebox(button_pressed_png, 4, 4, 4, 4));
theme->set_stylebox("button", "Tabs", make_stylebox(button_normal_png, 4, 4, 4, 4));
theme->set_stylebox("focus", "Tabs", sb_expand(make_stylebox(button_focus_png, 4, 4, 4, 4, 6, 2, 6, 2), 2, 2, 2, 2));

theme->set_icon("increment", "Tabs", make_icon(scroll_button_right_png));
theme->set_icon("increment_highlight", "Tabs", make_icon(scroll_button_right_hl_png));
Expand Down