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

Add support for multiple focus layers for Controls #62421

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions doc/classes/Control.xml
Original file line number Diff line number Diff line change
Expand Up @@ -565,10 +565,12 @@
</description>
</method>
<method name="grab_focus">
<return type="void" />
<return type="bool" />
KoBeWi marked this conversation as resolved.
Show resolved Hide resolved
<param index="0" name="auto_set_viewport_active_focus_layer" type="bool" default="false" />
KoBeWi marked this conversation as resolved.
Show resolved Hide resolved
<description>
Steal the focus from another control and become the focused control (see [member focus_mode]).
[b]Note:[/b] Using this method together with [method Callable.call_deferred] makes it more reliable, especially when called inside [method Node._ready].
Steal the focus from another control and become the focused control (see [member focus_mode]). If [param auto_set_viewport_active_focus_layer] is [code]true[/code], also sets the focused layer to the layer this control belongs to.
Returns [code]true[/code] if it was able to grab focus. Returns [code]false[/code] if the control doesn't get focused (see [member focus_mode]).
[b]Note[/b]: Using this method together with [method Callable.call_deferred] makes it more reliable, especially when called inside [method Node._ready].
Comment on lines +571 to +573
Copy link
Contributor

@Mickeon Mickeon Feb 25, 2024

Choose a reason for hiding this comment

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

This is a lovely amount of detail and it's a shame it's locked behind this contested PR. Should be a separate PR outright. Aside from a few tweaks I'd accept it in a heartbeat.

</description>
</method>
<method name="has_focus" qualifiers="const">
Expand Down Expand Up @@ -937,8 +939,12 @@
<member name="custom_minimum_size" type="Vector2" setter="set_custom_minimum_size" getter="get_custom_minimum_size" default="Vector2(0, 0)">
The minimum size of the node's bounding rectangle. If you set it to a value greater than (0, 0), the node's bounding rectangle will always have at least this size, even if its content is smaller. If it's set to (0, 0), the node sizes automatically to fit its content, be it a texture or child nodes.
</member>
<member name="focus_layer" type="int" setter="set_focus_layer" getter="get_focus_layer" default="0">
The focus layer for this control. Each layer will only navigate to other [Control]s in the same layer. Each layer can only have one focus at a time.
Use [method Viewport.gui_set_active_focus_layer] to set the active focus layer that will be navigated.
</member>
<member name="focus_mode" type="int" setter="set_focus_mode" getter="get_focus_mode" enum="Control.FocusMode" default="0">
The focus access mode for the control (None, Click or All). Only one Control can be focused at the same time, and it will receive keyboard, gamepad, and mouse signals.
The focus access mode for the control (None, Click or All). Only one [Control] per focus layer can be focused at the same time, and it will receive keyboard, gamepad, and mouse signals.
</member>
<member name="focus_neighbor_bottom" type="NodePath" setter="set_focus_neighbor" getter="get_focus_neighbor" default="NodePath(&quot;&quot;)">
Tells Godot which node it should give focus to if the user presses the down arrow on the keyboard or down on a gamepad by default. You can change the key by editing the [member ProjectSettings.input/ui_down] input action. The node must be a [Control]. If this property is not set, Godot will give focus to the closest [Control] to the bottom of this one.
Expand Down
22 changes: 20 additions & 2 deletions doc/classes/Viewport.xml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@
Returns the visible rectangle in global screen coordinates.
</description>
</method>
<method name="gui_get_active_focus_layer" qualifiers="const">
<return type="int" />
<description>
Returns the active focus layer for this [Viewport]. Only [Control]s using this layer will be navigated to via keyboard and gamepad.
</description>
</method>
<method name="gui_get_drag_data" qualifiers="const">
<return type="Variant" />
<description>
Expand All @@ -117,8 +123,10 @@
</method>
<method name="gui_get_focus_owner" qualifiers="const">
<return type="Control" />
<param index="0" name="focus_layer" type="int" default="0" />
<description>
Returns the [Control] having the focus within this viewport. If no [Control] has the focus, returns null.
Returns the focused [Control] within the provided focus layer in this viewport.
If no [Control] has the focus in the provided focus layer, returns [code]null[/code].
</description>
</method>
<method name="gui_is_drag_successful" qualifiers="const">
Expand All @@ -136,8 +144,18 @@
</method>
<method name="gui_release_focus">
<return type="void" />
<param index="0" name="focus_layer" type="int" default="0" />
<description>
Removes the focus from the currently focused [Control] within this viewport that is also using the provided focus layer.
If no [Control] has the focus in the provided focus layer, does nothing.
</description>
</method>
<method name="gui_set_active_focus_layer">
<return type="void" />
<param index="0" name="focus_layer" type="int" />
<description>
Removes the focus from the currently focused [Control] within this viewport. If no [Control] has the focus, does nothing.
Sets the active focus layer for this [Viewport]. The [Viewport] will only navigate to [Control]s using this focus layer.
Only one layer can be focused at a time.
</description>
</method>
<method name="is_input_handled" qualifiers="const">
Expand Down
2 changes: 1 addition & 1 deletion editor/editor_inspector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ void EditorProperty::gui_input(const Ref<InputEvent> &p_event) {

if (revert_rect.has_point(mpos)) {
accept_event();
get_viewport()->gui_release_focus();
get_viewport()->gui_release_focus(get_viewport()->gui_get_active_focus_layer());
bool is_valid_revert = false;
Variant revert_value = EditorPropertyRevert::get_property_revert_value(object, property, &is_valid_revert);
ERR_FAIL_COND(!is_valid_revert);
Expand Down
61 changes: 47 additions & 14 deletions scene/gui/control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1985,21 +1985,38 @@ Control::FocusMode Control::get_focus_mode() const {
return data.focus_mode;
}

void Control::set_focus_layer(int p_layer_id) {
if (has_focus()) {
release_focus();
}

data.focus_layer = p_layer_id;
}

int Control::get_focus_layer() const {
return data.focus_layer;
}

bool Control::has_focus() const {
ERR_READ_THREAD_GUARD_V(false);
return is_inside_tree() && get_viewport()->_gui_control_has_focus(this);
}

void Control::grab_focus() {
ERR_MAIN_THREAD_GUARD;
ERR_FAIL_COND(!is_inside_tree());
bool Control::grab_focus(bool p_auto_set_viewport_active_focus_layer) {
ERR_READ_THREAD_GUARD_V(false);
ERR_FAIL_COND_V(!is_inside_tree(), false);

if (data.focus_mode == FOCUS_NONE) {
WARN_PRINT("This control can't grab focus. Use set_focus_mode() to allow a control to get focus.");
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if this warning is still necessary if the method returns bool 🤔
(up to discussion, don't change it yet)

Copy link
Author

@redsett redsett Mar 31, 2023

Choose a reason for hiding this comment

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

Yeah, not sure. TBH I'm not sure how is_inside_tree() would fail. Maybe if you dynamically created a Control and didn't parent it to anything?

return;
return false;
}

if (p_auto_set_viewport_active_focus_layer) {
get_viewport()->gui_set_active_focus_layer(get_focus_layer());
}

get_viewport()->_gui_control_grab_focus(this);
return true;
}

void Control::grab_click_focus() {
Expand All @@ -2017,7 +2034,7 @@ void Control::release_focus() {
return;
}

get_viewport()->gui_release_focus();
get_viewport()->gui_release_focus(get_focus_layer());
}

static Control *_next_control(Control *p_from) {
Expand Down Expand Up @@ -2058,7 +2075,7 @@ Control *Control::find_next_valid_focus() const {
ERR_FAIL_NULL_V_MSG(n, nullptr, "Next focus node path is invalid: '" + data.focus_next + "'.");
Control *c = Object::cast_to<Control>(n);
ERR_FAIL_NULL_V_MSG(c, nullptr, "Next focus node is not a control: '" + n->get_name() + "'.");
if (c->is_visible() && c->get_focus_mode() != FOCUS_NONE) {
if (c->is_visible() && c->get_focus_mode() != FOCUS_NONE && c->get_focus_layer() == get_focus_layer()) {
return c;
}
}
Expand All @@ -2069,7 +2086,7 @@ Control *Control::find_next_valid_focus() const {

for (int i = 0; i < from->get_child_count(); i++) {
Control *c = Object::cast_to<Control>(from->get_child(i));
if (!c || !c->is_visible_in_tree() || c->is_set_as_top_level()) {
if (!c || !c->is_visible_in_tree() || c->is_set_as_top_level() || (c->get_focus_mode() == FOCUS_ALL && c->get_focus_layer() != get_focus_layer())) {
continue;
}

Expand Down Expand Up @@ -2113,14 +2130,24 @@ Control *Control::find_next_valid_focus() const {
return nullptr;
}

static Control *_prev_control(Control *p_from) {
static Control *_prev_focusable_control(Control *p_from, int p_focus_layer) {
Control *child = nullptr;
for (int i = p_from->get_child_count() - 1; i >= 0; i--) {
Control *c = Object::cast_to<Control>(p_from->get_child(i));
if (!c || !c->is_visible_in_tree() || c->is_set_as_top_level()) {
continue;
}

// If the control isn't focusable or isn't the correct layer, then let's check under it before we rule it out.
if (c->get_focus_mode() != Control::FocusMode::FOCUS_ALL || c->get_focus_layer() != p_focus_layer) {
// _prev_focusable_control will return p_from if it didn't find anything. So in this case, it's only a valid result if it's NOT c.
Control *nested_control = _prev_focusable_control(c, p_focus_layer);
if (nested_control != c) {
return nested_control;
}
continue;
}

child = c;
break;
}
Expand All @@ -2130,7 +2157,7 @@ static Control *_prev_control(Control *p_from) {
}

// No prev in parent, try the same in parent.
return _prev_control(child);
return _prev_focusable_control(child, p_focus_layer);
}

Control *Control::find_prev_valid_focus() const {
Expand All @@ -2145,7 +2172,7 @@ Control *Control::find_prev_valid_focus() const {
ERR_FAIL_NULL_V_MSG(n, nullptr, "Previous focus node path is invalid: '" + data.focus_prev + "'.");
Control *c = Object::cast_to<Control>(n);
ERR_FAIL_NULL_V_MSG(c, nullptr, "Previous focus node is not a control: '" + n->get_name() + "'.");
if (c->is_visible() && c->get_focus_mode() != FOCUS_NONE) {
if (c->is_visible() && c->get_focus_mode() != FOCUS_NONE && c->get_focus_layer() == get_focus_layer()) {
return c;
}
}
Expand All @@ -2157,7 +2184,7 @@ Control *Control::find_prev_valid_focus() const {
if (from->is_set_as_top_level() || !Object::cast_to<Control>(from->get_parent())) {
// Find last of the children.

prev_child = _prev_control(from);
prev_child = _prev_focusable_control(from, get_focus_layer());

} else {
for (int i = (from->get_index() - 1); i >= 0; i--) {
Expand All @@ -2174,7 +2201,7 @@ Control *Control::find_prev_valid_focus() const {
if (!prev_child) {
prev_child = Object::cast_to<Control>(from->get_parent());
} else {
prev_child = _prev_control(prev_child);
prev_child = _prev_focusable_control(prev_child, get_focus_layer());
}
}

Expand Down Expand Up @@ -2244,6 +2271,9 @@ Control *Control::_get_focus_neighbor(Side p_side, int p_count) {
if (c->get_focus_mode() == FOCUS_NONE) {
valid = false;
}
if (c->get_focus_layer() != get_focus_layer()) {
valid = false;
}
if (valid) {
return c;
}
Expand Down Expand Up @@ -2310,7 +2340,7 @@ void Control::_window_find_focus_neighbor(const Vector2 &p_dir, Node *p_at, cons

Control *c = Object::cast_to<Control>(p_at);

if (c && c != this && c->get_focus_mode() == FOCUS_ALL && c->is_visible_in_tree()) {
if (c && c != this && c->get_focus_mode() == FOCUS_ALL && c->is_visible_in_tree() && c->get_focus_layer() == get_focus_layer()) {
Point2 points[4];

Transform2D xform = c->get_global_transform();
Expand Down Expand Up @@ -3321,8 +3351,10 @@ void Control::_bind_methods() {
ClassDB::bind_method(D_METHOD("get_global_rect"), &Control::get_global_rect);
ClassDB::bind_method(D_METHOD("set_focus_mode", "mode"), &Control::set_focus_mode);
ClassDB::bind_method(D_METHOD("get_focus_mode"), &Control::get_focus_mode);
ClassDB::bind_method(D_METHOD("set_focus_layer", "layer_id"), &Control::set_focus_layer);
ClassDB::bind_method(D_METHOD("get_focus_layer"), &Control::get_focus_layer);
ClassDB::bind_method(D_METHOD("has_focus"), &Control::has_focus);
ClassDB::bind_method(D_METHOD("grab_focus"), &Control::grab_focus);
ClassDB::bind_method(D_METHOD("grab_focus", "auto_set_viewport_active_focus_layer"), &Control::grab_focus, DEFVAL(false));
ClassDB::bind_method(D_METHOD("release_focus"), &Control::release_focus);
ClassDB::bind_method(D_METHOD("find_prev_valid_focus"), &Control::find_prev_valid_focus);
ClassDB::bind_method(D_METHOD("find_next_valid_focus"), &Control::find_next_valid_focus);
Expand Down Expand Up @@ -3503,6 +3535,7 @@ void Control::_bind_methods() {
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "focus_next", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_next", "get_focus_next");
ADD_PROPERTY(PropertyInfo(Variant::NODE_PATH, "focus_previous", PROPERTY_HINT_NODE_PATH_VALID_TYPES, "Control"), "set_focus_previous", "get_focus_previous");
ADD_PROPERTY(PropertyInfo(Variant::INT, "focus_mode", PROPERTY_HINT_ENUM, "None,Click,All"), "set_focus_mode", "get_focus_mode");
ADD_PROPERTY(PropertyInfo(Variant::INT, "focus_layer"), "set_focus_layer", "get_focus_layer");

ADD_GROUP("Mouse", "mouse_");
ADD_PROPERTY(PropertyInfo(Variant::INT, "mouse_filter", PROPERTY_HINT_ENUM, "Stop,Pass,Ignore"), "set_mouse_filter", "get_mouse_filter");
Expand Down
5 changes: 4 additions & 1 deletion scene/gui/control.h
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class Control : public CanvasItem {
real_t offset[4] = { 0.0, 0.0, 0.0, 0.0 };
real_t anchor[4] = { ANCHOR_BEGIN, ANCHOR_BEGIN, ANCHOR_BEGIN, ANCHOR_BEGIN };
FocusMode focus_mode = FOCUS_NONE;
int focus_layer = 0;
GrowDirection h_grow = GROW_DIRECTION_END;
GrowDirection v_grow = GROW_DIRECTION_END;

Expand Down Expand Up @@ -519,8 +520,10 @@ class Control : public CanvasItem {

void set_focus_mode(FocusMode p_focus_mode);
FocusMode get_focus_mode() const;
void set_focus_layer(int p_layer_id);
int get_focus_layer() const;
bool has_focus() const;
void grab_focus();
bool grab_focus(bool p_auto_set_viewport_active_focus_layer = false);
void grab_click_focus();
void release_focus();

Expand Down
Loading