From bbc4c99c724017c5f2b224f0137715ed43407326 Mon Sep 17 00:00:00 2001 From: Orestis Floros Date: Fri, 14 Sep 2018 18:34:43 +0300 Subject: [PATCH] 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. --- docs/userguide | 7 + include/commands.h | 6 + include/tree.h | 11 +- parser-specs/commands.spec | 8 + src/click.c | 18 +-- src/commands.c | 69 ++++++-- src/tree.c | 254 +++++++++++++++--------------- testcases/t/101-focus.t | 16 +- testcases/t/307-focus-next-prev.t | 72 +++++++++ 9 files changed, 297 insertions(+), 164 deletions(-) create mode 100644 testcases/t/307-focus-next-prev.t diff --git a/docs/userguide b/docs/userguide index ea65bcd1e..a8b0e00d4 100644 --- a/docs/userguide +++ b/docs/userguide @@ -2053,6 +2053,12 @@ parent:: child:: The opposite of +focus parent+, sets the focus to the last focused 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:: Sets focus to the last focused floating container. tiling:: @@ -2068,6 +2074,7 @@ output:: focus focus left|right|down|up focus parent|child|floating|tiling|mode_toggle +focus next|prev [sibling] focus output left|right|up|down|primary| ---------------------------------------------- diff --git a/include/commands.h b/include/commands.h index 0137460fe..27d631a21 100644 --- a/include/commands.h +++ b/include/commands.h @@ -182,6 +182,12 @@ void cmd_exec(I3_CMD, const char *nosn, const char *command); */ 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'. * diff --git a/include/tree.h b/include/tree.h index 12170f94c..0b758d53e 100644 --- a/include/tree.h +++ b/include/tree.h @@ -59,11 +59,16 @@ bool level_down(void); void tree_render(void); /** - * Changes focus in the given way (next/previous) and given orientation - * (horizontal/vertical). + * Changes focus in the given direction * */ -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. diff --git a/parser-specs/commands.spec b/parser-specs/commands.spec index 6b015188e..ed9cf0f2f 100644 --- a/parser-specs/commands.spec +++ b/parser-specs/commands.spec @@ -146,6 +146,8 @@ state WORKSPACE_NUMBER: state FOCUS: direction = 'left', 'right', 'up', 'down' -> call cmd_focus_direction($direction) + direction = 'prev', 'next' + -> FOCUS_AUTO 'output' -> FOCUS_OUTPUT window_mode = 'tiling', 'floating', 'mode_toggle' @@ -155,6 +157,12 @@ state FOCUS: end -> call cmd_focus() +state FOCUS_AUTO: + 'sibling' + -> call cmd_focus_sibling($direction) + end + -> call cmd_focus_direction($direction) + state FOCUS_OUTPUT: output = string -> call cmd_focus_output($output) diff --git a/src/click.c b/src/click.c index 8ab5c5f06..75710a820 100644 --- a/src/click.c +++ b/src/click.c @@ -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_RIGHT)) { 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 * container the user scrolled on. */ - Con *focused = con->parent; - focused = TAILQ_FIRST(&(focused->focus_head)); - con_activate(con_descend_focused(focused)); - /* To prevent scrolling from going outside the container (see ticket - * #557), we first check if scrolling is possible at all. */ - 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); - } + Con *current = TAILQ_FIRST(&(con->parent->focus_head)); + const position_t direction = + (event->detail == XCB_BUTTON_SCROLL_UP || event->detail == XCB_BUTTON_SCROLL_LEFT) ? BEFORE : AFTER; + Con *next = get_tree_next_sibling(current, direction); + con_activate(con_descend_focused(next ? next : current)); goto done; } diff --git a/src/commands.c b/src/commands.c index 24a5bd2f5..5527860ca 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1229,23 +1229,62 @@ void cmd_exec(I3_CMD, const char *nosn, const char *command) { } 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) { - switch (parse_direction(direction)) { - case D_LEFT: - tree_next('p', HORIZ); - break; - case D_RIGHT: - tree_next('n', HORIZ); - break; - case D_UP: - tree_next('p', VERT); - break; - case D_DOWN: - tree_next('n', VERT); - break; +void cmd_focus_direction(I3_CMD, const char *direction_str) { + HANDLE_EMPTY_MATCH; + CMD_FOCUS_WARN_CHILDREN; + + direction_t direction; + position_t position; + bool auto_direction = true; + if (strcmp(direction_str, "prev") == 0) { + position = BEFORE; + } else if (strcmp(direction_str, "next") == 0) { + position = AFTER; + } else { + auto_direction = false; + 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; diff --git a/src/tree.c b/src/tree.c index 4057d177b..039c3a5e3 100644 --- a/src/tree.c +++ b/src/tree.c @@ -462,170 +462,162 @@ void tree_render(void) { 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) { - /* When dealing with fullscreen containers, it's necessary to go up to the - * workspace level, because 'focus $dir' will start at the con's real - * 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 - * workspace if possible. */ - if (con->type == CT_WORKSPACE) { - if (con_get_fullscreen_con(con, CF_GLOBAL)) { - DLOG("Cannot change workspace while in global fullscreen mode.\n"); - return false; +static Con *get_tree_next(Con *con, direction_t direction) { + const bool previous = position_from_direction(direction) == BEFORE; + const orientation_t orientation = orientation_from_direction(direction); + + Con *first_wrap = NULL; + while (con->type != CT_WORKSPACE) { + if (con->fullscreen_mode == CF_OUTPUT) { + /* We've reached a fullscreen container. Directional focus should + * now operate on the workspace level. */ + 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) - return false; - DLOG("Current output is %s\n", output_primary_name(current_output)); - - /* Try to find next output */ - direction_t direction; - if (way == 'n' && orientation == HORIZ) - direction = D_RIGHT; - else if (way == 'p' && orientation == HORIZ) - direction = D_LEFT; - else if (way == 'n' && orientation == VERT) - direction = D_DOWN; - else if (way == 'p' && orientation == VERT) - direction = D_UP; - else - return false; + Con *const parent = con->parent; + if (con->type == CT_FLOATING_CON) { + if (orientation != HORIZ) { + /* up/down does not change floating containers */ + return NULL; + } - next_output = get_output_next(direction, current_output, CLOSEST_OUTPUT); - if (!next_output) - return false; - DLOG("Next output is %s\n", output_primary_name(next_output)); + /* left/right focuses the previous/next floating container */ + Con *next = previous ? TAILQ_PREV(con, floating_head, floating_windows) + : TAILQ_NEXT(con, floating_windows); + /* If there is no next/previous container, wrap */ + if (!next) { + next = previous ? TAILQ_LAST(&(parent->floating_head), floating_head) + : TAILQ_FIRST(&(parent->floating_head)); + } + /* Our parent does not list us in floating heads? */ + assert(next); - /* Find visible workspace on next output */ - Con *workspace = NULL; - GREP_FIRST(workspace, output_get_content(next_output->con), workspace_is_visible(child)); + return next; + } - /* Show next workspace and focus appropriate container if possible. */ - if (!workspace) - return false; + if (con_num_children(parent) > 1 && con_orientation(parent) == orientation) { + Con *const next = previous ? TAILQ_PREV(con, nodes_head, nodes) + : 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. */ /* Use descend_focused first to give higher priority to floating or * tiling fullscreen containers. */ - Con *focus = con_descend_focused(workspace); + Con *focus = con_descend_focused(next); 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 * floating container or the same workspace. */ - if (focus_tiling != workspace) { + if (focus_tiling != next) { focus = focus_tiling; } } - workspace_show(workspace); + workspace_show(next); con_activate(focus); x_set_warp_to(&(focus->rect)); - return true; - } - - Con *parent = con->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 */ + return; + } else if (next->type == CT_FLOATING_CON) { + /* Raise the floating window on top of other windows preserving relative + * stack order */ + Con *parent = next->parent; while (TAILQ_LAST(&(parent->floating_head), floating_head) != next) { Con *last = TAILQ_LAST(&(parent->floating_head), floating_head); TAILQ_REMOVE(&(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 - * 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 */ + workspace_show(con_get_workspace(next)); con_activate(con_descend_focused(next)); - return true; } /* - * Changes focus in the given way (next/previous) and given orientation - * (horizontal/vertical). + * Get the previous / next sibling * */ -void tree_next(char way, orientation_t orientation) { - _tree_next(focused, way, orientation, - config.focus_wrapping != FOCUS_WRAPPING_OFF); +Con *get_tree_next_sibling(Con *con, position_t direction) { + Con *to_focus = (direction == BEFORE ? TAILQ_PREV(con, nodes_head, nodes) + : TAILQ_NEXT(con, nodes)); + if (to_focus && con_fullscreen_permits_focusing(to_focus)) { + return to_focus; + } + return NULL; } /* diff --git a/testcases/t/101-focus.t b/testcases/t/101-focus.t index 9d42345da..1e87a5445 100644 --- a/testcases/t/101-focus.t +++ b/testcases/t/101-focus.t @@ -35,9 +35,13 @@ my $bottom = open_window; # end sleeping for half a second to make sure i3 reacted # 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; } @@ -50,6 +54,14 @@ is($focus, $mid->id, "Middle window focused"); $focus = focus_after('focus up'); 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 ##################################################################### diff --git a/testcases/t/307-focus-next-prev.t b/testcases/t/307-focus-next-prev.t new file mode 100644 index 000000000..c7f06589f --- /dev/null +++ b/testcases/t/307-focus-next-prev.t @@ -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;