Refactor tree_next

- Makes `tree_next` not recursive.
- Adds `focus next|prev [sibling]` command. See (1.) and (2.) in
https://github.com/i3/i3/issues/2587#issuecomment-378505551 (Issue also
requests move command, not implemented here).
- Directional focus command now supports command criteria.

Wrapping is not implemented inside a floating container. This was also
true before the refactor so I am not changing it here.
This commit is contained in:
Orestis Floros 2018-09-14 18:34:43 +03:00 committed by Orestis Floros
parent f402f45702
commit bbc4c99c72
No known key found for this signature in database
GPG Key ID: A09DBD7D3222C1C3
9 changed files with 295 additions and 162 deletions

View File

@ -2053,6 +2053,12 @@ parent::
child:: child::
The opposite of +focus parent+, sets the focus to the last focused The opposite of +focus parent+, sets the focus to the last focused
child container. child container.
next|prev::
Automatically sets focus to the adjacent container. If +sibling+ is
specified, the command will focus the exact sibling container,
including non-leaf containers like split containers. Otherwise, it is
an automatic version of +focus left|right|up|down+ in the orientation
of the parent container.
floating:: floating::
Sets focus to the last focused floating container. Sets focus to the last focused floating container.
tiling:: tiling::
@ -2068,6 +2074,7 @@ output::
<criteria> focus <criteria> focus
focus left|right|down|up focus left|right|down|up
focus parent|child|floating|tiling|mode_toggle focus parent|child|floating|tiling|mode_toggle
focus next|prev [sibling]
focus output left|right|up|down|primary|<output> focus output left|right|up|down|primary|<output>
---------------------------------------------- ----------------------------------------------

View File

@ -182,6 +182,12 @@ void cmd_exec(I3_CMD, const char *nosn, const char *command);
*/ */
void cmd_focus_direction(I3_CMD, const char *direction); void cmd_focus_direction(I3_CMD, const char *direction);
/**
* Implementation of 'focus next|prev sibling'
*
*/
void cmd_focus_sibling(I3_CMD, const char *direction);
/** /**
* Implementation of 'focus tiling|floating|mode_toggle'. * Implementation of 'focus tiling|floating|mode_toggle'.
* *

View File

@ -59,11 +59,16 @@ bool level_down(void);
void tree_render(void); void tree_render(void);
/** /**
* Changes focus in the given way (next/previous) and given orientation * Changes focus in the given direction
* (horizontal/vertical).
* *
*/ */
void tree_next(char way, orientation_t orientation); void tree_next(Con *con, direction_t direction);
/**
* Get the previous / next sibling
*
*/
Con *get_tree_next_sibling(Con *con, position_t direction);
/** /**
* Closes the given container including all children. * Closes the given container including all children.

View File

@ -146,6 +146,8 @@ state WORKSPACE_NUMBER:
state FOCUS: state FOCUS:
direction = 'left', 'right', 'up', 'down' direction = 'left', 'right', 'up', 'down'
-> call cmd_focus_direction($direction) -> call cmd_focus_direction($direction)
direction = 'prev', 'next'
-> FOCUS_AUTO
'output' 'output'
-> FOCUS_OUTPUT -> FOCUS_OUTPUT
window_mode = 'tiling', 'floating', 'mode_toggle' window_mode = 'tiling', 'floating', 'mode_toggle'
@ -155,6 +157,12 @@ state FOCUS:
end end
-> call cmd_focus() -> call cmd_focus()
state FOCUS_AUTO:
'sibling'
-> call cmd_focus_sibling($direction)
end
-> call cmd_focus_direction($direction)
state FOCUS_OUTPUT: state FOCUS_OUTPUT:
output = string output = string
-> call cmd_focus_output($output) -> call cmd_focus_output($output)

View File

@ -212,21 +212,13 @@ static int route_click(Con *con, xcb_button_press_event_t *event, const bool mod
event->detail == XCB_BUTTON_SCROLL_LEFT || event->detail == XCB_BUTTON_SCROLL_LEFT ||
event->detail == XCB_BUTTON_SCROLL_RIGHT)) { event->detail == XCB_BUTTON_SCROLL_RIGHT)) {
DLOG("Scrolling on a window decoration\n"); DLOG("Scrolling on a window decoration\n");
orientation_t orientation = con_orientation(con->parent);
/* Use the focused child of the tabbed / stacked container, not the /* Use the focused child of the tabbed / stacked container, not the
* container the user scrolled on. */ * container the user scrolled on. */
Con *focused = con->parent; Con *current = TAILQ_FIRST(&(con->parent->focus_head));
focused = TAILQ_FIRST(&(focused->focus_head)); const position_t direction =
con_activate(con_descend_focused(focused)); (event->detail == XCB_BUTTON_SCROLL_UP || event->detail == XCB_BUTTON_SCROLL_LEFT) ? BEFORE : AFTER;
/* To prevent scrolling from going outside the container (see ticket Con *next = get_tree_next_sibling(current, direction);
* #557), we first check if scrolling is possible at all. */ con_activate(con_descend_focused(next ? next : current));
bool scroll_prev_possible = (TAILQ_PREV(focused, nodes_head, nodes) != NULL);
bool scroll_next_possible = (TAILQ_NEXT(focused, nodes) != NULL);
if ((event->detail == XCB_BUTTON_SCROLL_UP || event->detail == XCB_BUTTON_SCROLL_LEFT) && scroll_prev_possible) {
tree_next('p', orientation);
} else if ((event->detail == XCB_BUTTON_SCROLL_DOWN || event->detail == XCB_BUTTON_SCROLL_RIGHT) && scroll_next_possible) {
tree_next('n', orientation);
}
goto done; goto done;
} }

View File

@ -1229,23 +1229,62 @@ void cmd_exec(I3_CMD, const char *nosn, const char *command) {
} while (0) } while (0)
/* /*
* Implementation of 'focus left|right|up|down'. * Implementation of 'focus left|right|up|down|next|prev'.
* *
*/ */
void cmd_focus_direction(I3_CMD, const char *direction) { void cmd_focus_direction(I3_CMD, const char *direction_str) {
switch (parse_direction(direction)) { HANDLE_EMPTY_MATCH;
case D_LEFT: CMD_FOCUS_WARN_CHILDREN;
tree_next('p', HORIZ);
break; direction_t direction;
case D_RIGHT: position_t position;
tree_next('n', HORIZ); bool auto_direction = true;
break; if (strcmp(direction_str, "prev") == 0) {
case D_UP: position = BEFORE;
tree_next('p', VERT); } else if (strcmp(direction_str, "next") == 0) {
break; position = AFTER;
case D_DOWN: } else {
tree_next('n', VERT); auto_direction = false;
break; direction = parse_direction(direction_str);
}
owindow *current;
TAILQ_FOREACH(current, &owindows, owindows) {
Con *ws = con_get_workspace(current->con);
if (!ws || con_is_internal(ws)) {
continue;
}
if (auto_direction) {
orientation_t o = con_orientation(current->con->parent);
direction = direction_from_orientation_position(o, position);
}
tree_next(current->con, direction);
}
cmd_output->needs_tree_render = true;
// XXX: default reply for now, make this a better reply
ysuccess(true);
}
/*
* Implementation of 'focus next|prev sibling'
*
*/
void cmd_focus_sibling(I3_CMD, const char *direction_str) {
HANDLE_EMPTY_MATCH;
CMD_FOCUS_WARN_CHILDREN;
const position_t direction = (STARTS_WITH(direction_str, "prev")) ? BEFORE : AFTER;
owindow *current;
TAILQ_FOREACH(current, &owindows, owindows) {
Con *ws = con_get_workspace(current->con);
if (!ws || con_is_internal(ws)) {
continue;
}
Con *next = get_tree_next_sibling(current->con, direction);
if (next) {
con_activate(next);
}
} }
cmd_output->needs_tree_render = true; cmd_output->needs_tree_render = true;

View File

@ -462,170 +462,162 @@ void tree_render(void) {
DLOG("-- END RENDERING --\n"); DLOG("-- END RENDERING --\n");
} }
static Con *get_tree_next_workspace(Con *con, direction_t direction) {
if (con_get_fullscreen_con(con, CF_GLOBAL)) {
DLOG("Cannot change workspace while in global fullscreen mode.\n");
return NULL;
}
Output *current_output = get_output_containing(con->rect.x, con->rect.y);
if (!current_output) {
return NULL;
}
DLOG("Current output is %s\n", output_primary_name(current_output));
Output *next_output = get_output_next(direction, current_output, CLOSEST_OUTPUT);
if (!next_output) {
return NULL;
}
DLOG("Next output is %s\n", output_primary_name(next_output));
/* Find visible workspace on next output */
Con *workspace = NULL;
GREP_FIRST(workspace, output_get_content(next_output->con), workspace_is_visible(child));
return workspace;
}
/* /*
* Recursive function to walk the tree until a con can be found to focus. * Returns the next / previous container to focus in the given direction. Does
* not modify focus and ensures focus restrictions for fullscreen containers
* are respected.
* *
*/ */
static bool _tree_next(Con *con, char way, orientation_t orientation, bool wrap) { static Con *get_tree_next(Con *con, direction_t direction) {
/* When dealing with fullscreen containers, it's necessary to go up to the const bool previous = position_from_direction(direction) == BEFORE;
* workspace level, because 'focus $dir' will start at the con's real const orientation_t orientation = orientation_from_direction(direction);
* position in the tree, and it may not be possible to get to the edge
* normally due to fullscreen focusing restrictions. */
if (con->fullscreen_mode == CF_OUTPUT && con->type != CT_WORKSPACE)
con = con_get_workspace(con);
/* Stop recursing at workspaces after attempting to switch to next Con *first_wrap = NULL;
* workspace if possible. */ while (con->type != CT_WORKSPACE) {
if (con->type == CT_WORKSPACE) { if (con->fullscreen_mode == CF_OUTPUT) {
if (con_get_fullscreen_con(con, CF_GLOBAL)) { /* We've reached a fullscreen container. Directional focus should
DLOG("Cannot change workspace while in global fullscreen mode.\n"); * now operate on the workspace level. */
return false; con = con_get_workspace(con);
break;
} else if (con->fullscreen_mode == CF_GLOBAL) {
/* Focus changes should happen only inside the children of a global
* fullscreen container. */
return first_wrap;
} }
Output *current_output = get_output_containing(con->rect.x, con->rect.y);
Output *next_output;
if (!current_output) Con *const parent = con->parent;
return false; if (con->type == CT_FLOATING_CON) {
DLOG("Current output is %s\n", output_primary_name(current_output)); if (orientation != HORIZ) {
/* up/down does not change floating containers */
return NULL;
}
/* Try to find next output */ /* left/right focuses the previous/next floating container */
direction_t direction; Con *next = previous ? TAILQ_PREV(con, floating_head, floating_windows)
if (way == 'n' && orientation == HORIZ) : TAILQ_NEXT(con, floating_windows);
direction = D_RIGHT; /* If there is no next/previous container, wrap */
else if (way == 'p' && orientation == HORIZ) if (!next) {
direction = D_LEFT; next = previous ? TAILQ_LAST(&(parent->floating_head), floating_head)
else if (way == 'n' && orientation == VERT) : TAILQ_FIRST(&(parent->floating_head));
direction = D_DOWN; }
else if (way == 'p' && orientation == VERT) /* Our parent does not list us in floating heads? */
direction = D_UP; assert(next);
else
return false;
next_output = get_output_next(direction, current_output, CLOSEST_OUTPUT); return next;
if (!next_output) }
return false;
DLOG("Next output is %s\n", output_primary_name(next_output));
/* Find visible workspace on next output */ if (con_num_children(parent) > 1 && con_orientation(parent) == orientation) {
Con *workspace = NULL; Con *const next = previous ? TAILQ_PREV(con, nodes_head, nodes)
GREP_FIRST(workspace, output_get_content(next_output->con), workspace_is_visible(child)); : TAILQ_NEXT(con, nodes);
if (next && con_fullscreen_permits_focusing(next)) {
return next;
}
Con *const wrap = previous ? TAILQ_LAST(&(parent->nodes_head), nodes_head)
: TAILQ_FIRST(&(parent->nodes_head));
switch (config.focus_wrapping) {
case FOCUS_WRAPPING_OFF:
break;
case FOCUS_WRAPPING_ON:
if (!first_wrap && con_fullscreen_permits_focusing(wrap)) {
first_wrap = wrap;
}
break;
case FOCUS_WRAPPING_FORCE:
/* 'force' should always return to ensure focus doesn't
* leave the parent. */
if (next) {
return NULL; /* blocked by fullscreen */
}
return con_fullscreen_permits_focusing(wrap) ? wrap : NULL;
}
}
con = parent;
}
assert(con->type == CT_WORKSPACE);
Con *workspace = get_tree_next_workspace(con, direction);
return workspace ? workspace : first_wrap;
}
/*
* Changes focus in the given direction
*
*/
void tree_next(Con *con, direction_t direction) {
Con *next = get_tree_next(con, direction);
if (!next) {
return;
}
if (next->type == CT_WORKSPACE) {
/* Show next workspace and focus appropriate container if possible. */ /* Show next workspace and focus appropriate container if possible. */
if (!workspace)
return false;
/* Use descend_focused first to give higher priority to floating or /* Use descend_focused first to give higher priority to floating or
* tiling fullscreen containers. */ * tiling fullscreen containers. */
Con *focus = con_descend_focused(workspace); Con *focus = con_descend_focused(next);
if (focus->fullscreen_mode == CF_NONE) { if (focus->fullscreen_mode == CF_NONE) {
Con *focus_tiling = con_descend_tiling_focused(workspace); Con *focus_tiling = con_descend_tiling_focused(next);
/* If descend_tiling returned a workspace then focus is either a /* If descend_tiling returned a workspace then focus is either a
* floating container or the same workspace. */ * floating container or the same workspace. */
if (focus_tiling != workspace) { if (focus_tiling != next) {
focus = focus_tiling; focus = focus_tiling;
} }
} }
workspace_show(workspace); workspace_show(next);
con_activate(focus); con_activate(focus);
x_set_warp_to(&(focus->rect)); x_set_warp_to(&(focus->rect));
return true; return;
} } else if (next->type == CT_FLOATING_CON) {
/* Raise the floating window on top of other windows preserving relative
Con *parent = con->parent; * stack order */
Con *parent = next->parent;
if (con->type == CT_FLOATING_CON) {
if (orientation != HORIZ)
return false;
/* left/right focuses the previous/next floating container */
Con *next;
if (way == 'n')
next = TAILQ_NEXT(con, floating_windows);
else
next = TAILQ_PREV(con, floating_head, floating_windows);
/* If there is no next/previous container, wrap */
if (!next) {
if (way == 'n')
next = TAILQ_FIRST(&(parent->floating_head));
else
next = TAILQ_LAST(&(parent->floating_head), floating_head);
}
/* Still no next/previous container? bail out */
if (!next)
return false;
/* Raise the floating window on top of other windows preserving
* relative stack order */
while (TAILQ_LAST(&(parent->floating_head), floating_head) != next) { while (TAILQ_LAST(&(parent->floating_head), floating_head) != next) {
Con *last = TAILQ_LAST(&(parent->floating_head), floating_head); Con *last = TAILQ_LAST(&(parent->floating_head), floating_head);
TAILQ_REMOVE(&(parent->floating_head), last, floating_windows); TAILQ_REMOVE(&(parent->floating_head), last, floating_windows);
TAILQ_INSERT_HEAD(&(parent->floating_head), last, floating_windows); TAILQ_INSERT_HEAD(&(parent->floating_head), last, floating_windows);
} }
con_activate(con_descend_focused(next));
return true;
} }
/* If the orientation does not match or there is no other con to focus, we workspace_show(con_get_workspace(next));
* need to go higher in the hierarchy */
if (con_orientation(parent) != orientation ||
con_num_children(parent) == 1)
return _tree_next(parent, way, orientation, wrap);
Con *current = TAILQ_FIRST(&(parent->focus_head));
/* TODO: when can the following happen (except for floating windows, which
* are handled above)? */
if (TAILQ_EMPTY(&(parent->nodes_head))) {
DLOG("nothing to focus\n");
return false;
}
Con *next;
if (way == 'n')
next = TAILQ_NEXT(current, nodes);
else
next = TAILQ_PREV(current, nodes_head, nodes);
if (!next) {
if (config.focus_wrapping != FOCUS_WRAPPING_FORCE) {
/* If there is no next/previous container, we check if we can focus one
* when going higher (without wrapping, though). If so, we are done, if
* not, we wrap */
if (_tree_next(parent, way, orientation, false))
return true;
if (!wrap)
return false;
}
if (way == 'n')
next = TAILQ_FIRST(&(parent->nodes_head));
else
next = TAILQ_LAST(&(parent->nodes_head), nodes_head);
}
/* Don't violate fullscreen focus restrictions. */
if (!con_fullscreen_permits_focusing(next))
return false;
/* 3: focus choice comes in here. at the moment we will go down
* until we find a window */
/* TODO: check for window, atm we only go down as far as possible */
con_activate(con_descend_focused(next)); con_activate(con_descend_focused(next));
return true;
} }
/* /*
* Changes focus in the given way (next/previous) and given orientation * Get the previous / next sibling
* (horizontal/vertical).
* *
*/ */
void tree_next(char way, orientation_t orientation) { Con *get_tree_next_sibling(Con *con, position_t direction) {
_tree_next(focused, way, orientation, Con *to_focus = (direction == BEFORE ? TAILQ_PREV(con, nodes_head, nodes)
config.focus_wrapping != FOCUS_WRAPPING_OFF); : TAILQ_NEXT(con, nodes));
if (to_focus && con_fullscreen_permits_focusing(to_focus)) {
return to_focus;
}
return NULL;
} }
/* /*

View File

@ -35,9 +35,13 @@ my $bottom = open_window;
# end sleeping for half a second to make sure i3 reacted # end sleeping for half a second to make sure i3 reacted
# #
sub focus_after { sub focus_after {
my $msg = shift; my ($msg, $win_id) = @_;
cmd $msg; if (defined($win_id)) {
cmd "[id=$win_id] $msg";
} else {
cmd $msg;
}
return $x->input_focus; return $x->input_focus;
} }
@ -50,6 +54,14 @@ is($focus, $mid->id, "Middle window focused");
$focus = focus_after('focus up'); $focus = focus_after('focus up');
is($focus, $top->id, "Top window focused"); is($focus, $top->id, "Top window focused");
# Same using command criteria
$focus = focus_after('focus up', $bottom->id);
is($focus, $mid->id, "Middle window focused");
cmd 'focus down';
$focus = focus_after('focus up', $mid->id);
is($focus, $top->id, "Top window focused");
##################################################################### #####################################################################
# Test focus wrapping # Test focus wrapping
##################################################################### #####################################################################

View File

@ -0,0 +1,72 @@
#!perl
# vim:ts=4:sw=4:expandtab
#
# Please read the following documents before working on tests:
# • https://build.i3wm.org/docs/testsuite.html
# (or docs/testsuite)
#
# • https://build.i3wm.org/docs/lib-i3test.html
# (alternatively: perldoc ./testcases/lib/i3test.pm)
#
# • https://build.i3wm.org/docs/ipc.html
# (or docs/ipc)
#
# • http://onyxneon.com/books/modern_perl/modern_perl_a4.pdf
# (unless you are already familiar with Perl)
#
# Test focus next|prev
# Ticket: #2587
use i3test;
cmp_tree(
msg => "cmd 'prev' selects leaf 1/2",
layout_before => 'S[a b] V[c d* T[e f g]]',
layout_after => 'S[a b] V[c* d T[e f g]]',
cb => sub {
cmd 'focus prev';
});
cmp_tree(
msg => "cmd 'prev' selects leaf 2/2",
layout_before => 'S[a b] V[c* d T[e f g]]',
layout_after => 'S[a b*] V[c d T[e f g]]',
cb => sub {
# c* -> V -> b*
cmd 'focus parent, focus prev';
});
cmp_tree(
msg => "cmd 'prev sibling' selects leaf again",
layout_before => 'S[a b] V[c d* T[e f g]]',
layout_after => 'S[a b] V[c* d T[e f g]]',
cb => sub {
cmd 'focus prev sibling';
});
cmp_tree(
msg => "cmd 'next' selects leaf",
# Notice that g is the last to open before focus moves to d*
layout_before => 'S[a b] V[c d* T[e f g]]',
layout_after => 'S[a b] V[c d T[e f g*]]',
cb => sub {
cmd 'focus next';
});
cmp_tree(
msg => "cmd 'next sibling' selects parent 1/2",
layout_before => 'S[a b] V[c d* T[e f g]]',
layout_after => 'S[a b] V[c d T*[e f g]]',
cb => sub {
cmd 'focus next sibling';
});
cmp_tree(
msg => "cmd 'next sibling' selects parent 2/2",
layout_before => 'S[a b*] V[c d T[e f g]]',
layout_after => 'S[a b] V*[c d T[e f g]]',
cb => sub {
# b* -> S* -> V*
cmd 'focus parent, focus next sibling';
});
done_testing;