diff --git a/docs/userguide b/docs/userguide index 525cfd77..bc208652 100644 --- a/docs/userguide +++ b/docs/userguide @@ -874,6 +874,30 @@ workspace_auto_back_and_forth workspace_auto_back_and_forth yes --------------------------------- +=== Delaying urgency hint reset on workspace change + +If an application on another workspace sets an urgency hint, switching to this +workspace may lead to immediate focus of the application, which also means the +window decoration color would be immediately resetted to +client.focused+. This +may make it unnecessarily hard to tell which window originally raised the +event. + +In order to prevent this, you can tell i3 to delay resetting the urgency state +by a certain time using the +force_display_urgency_hint+ directive. Setting the +value to 0 disables this feature. + +The default is 500ms. + +*Syntax*: +--------------------------------------- +force_display_urgency_hint ms +--------------------------------------- + +*Example*: +--------------------------------- +force_display_urgency_hint 500 ms +--------------------------------- + == Configuring i3bar The bar at the bottom of your monitor is drawn by a separate process called diff --git a/include/config.h b/include/config.h index 669cfe44..056aa5ae 100644 --- a/include/config.h +++ b/include/config.h @@ -149,6 +149,13 @@ struct Config { * between two workspaces. */ bool workspace_auto_back_and_forth; + /** By default, urgency is cleared immediately when switching to another + * workspace leads to focusing the con with the urgency hint. When having + * multiple windows on that workspace, the user needs to guess which + * application raised the event. To prevent this, the reset of the urgency + * flag can be delayed using an urgency timer. */ + float workspace_urgency_timer; + /** The default border style for new windows. */ border_style_t default_border; diff --git a/include/data.h b/include/data.h index a2c6859e..e78354f4 100644 --- a/include/data.h +++ b/include/data.h @@ -496,6 +496,9 @@ struct Con { * inside this container (if any) sets the urgency hint, for example. */ bool urgent; + /* timer used for disabling urgency */ + struct ev_timer *urgency_timer; + /* ids/pixmap/graphics context for the frame window */ xcb_window_t frame; xcb_pixmap_t pixmap; diff --git a/src/cfgparse.l b/src/cfgparse.l index 8ee2a1da..b752851b 100644 --- a/src/cfgparse.l +++ b/src/cfgparse.l @@ -212,6 +212,8 @@ force-xinerama { return TOK_FORCE_XINERAMA; } fake_outputs { WS_STRING; return TOK_FAKE_OUTPUTS; } fake-outputs { WS_STRING; return TOK_FAKE_OUTPUTS; } workspace_auto_back_and_forth { return TOK_WORKSPACE_AUTO_BAF; } +force_display_urgency_hint { return TOK_WORKSPACE_URGENCY_TIMER; } +ms { return TOK_TIME_MS; } workspace_bar { return TOKWORKSPACEBAR; } popup_during_fullscreen { return TOK_POPUP_DURING_FULLSCREEN; } ignore { return TOK_IGNORE; } diff --git a/src/cfgparse.y b/src/cfgparse.y index 29c519f0..bcd7d20c 100644 --- a/src/cfgparse.y +++ b/src/cfgparse.y @@ -728,6 +728,7 @@ void parse_file(const char *f) { %token TOKCOLOR %token TOKARROW "→" %token TOKMODE "mode" +%token TOK_TIME_MS "ms" %token TOK_BAR "bar" %token TOK_ORIENTATION "default_orientation" %token TOK_HORIZ "horizontal" @@ -746,6 +747,7 @@ void parse_file(const char *f) { %token TOK_FORCE_XINERAMA "force_xinerama" %token TOK_FAKE_OUTPUTS "fake_outputs" %token TOK_WORKSPACE_AUTO_BAF "workspace_auto_back_and_forth" +%token TOK_WORKSPACE_URGENCY_TIMER "force_display_urgency_hint" %token TOKWORKSPACEBAR "workspace_bar" %token TOK_DEFAULT "default" %token TOK_STACKING "stacking" @@ -819,6 +821,7 @@ void parse_file(const char *f) { %type optional_release %type command %type word_or_number +%type duration %type qstring_or_number %type optional_workspace_name %type workspace_name @@ -848,6 +851,7 @@ line: | force_focus_wrapping | force_xinerama | fake_outputs + | force_display_urgency_hint | workspace_back_and_forth | workspace_bar | workspace @@ -1052,6 +1056,10 @@ word_or_number: } ; +duration: + NUMBER TOK_TIME_MS { sasprintf(&$$, "%d", $1); } + ; + mode: TOKMODE QUOTEDSTRING '{' modelines '}' { @@ -1548,6 +1556,14 @@ workspace_back_and_forth: } ; +force_display_urgency_hint: + TOK_WORKSPACE_URGENCY_TIMER duration + { + DLOG("workspace urgency_timer = %f\n", atoi($2) / 1000.0); + config.workspace_urgency_timer = atoi($2) / 1000.0; + } + ; + workspace_bar: TOKWORKSPACEBAR bool { diff --git a/src/config.c b/src/config.c index 0bd6811a..0cfa8eb6 100644 --- a/src/config.c +++ b/src/config.c @@ -420,6 +420,10 @@ void load_configuration(xcb_connection_t *conn, const char *override_configpath, /* Set default_orientation to NO_ORIENTATION for auto orientation. */ config.default_orientation = NO_ORIENTATION; + /* Set default urgency reset delay to 500ms */ + if (config.workspace_urgency_timer == 0) + config.workspace_urgency_timer = 0.5; + parse_configuration(override_configpath); if (reload) { diff --git a/src/handlers.c b/src/handlers.c index 21a87342..f9099cc1 100644 --- a/src/handlers.c +++ b/src/handlers.c @@ -836,7 +836,13 @@ static bool handle_hints(void *data, xcb_connection_t *conn, uint8_t state, xcb_ } /* Update the flag on the client directly */ - con->urgent = (xcb_icccm_wm_hints_get_urgency(&hints) != 0); + bool hint_urgent = (xcb_icccm_wm_hints_get_urgency(&hints) != 0); + + if (con->urgency_timer == NULL) { + con->urgent = hint_urgent; + } else + DLOG("Discarding urgency WM_HINT because timer is running\n"); + //CLIENT_LOG(con); if (con->window) { if (con->urgent) { diff --git a/src/tree.c b/src/tree.c index 321bc78a..4f34946c 100644 --- a/src/tree.c +++ b/src/tree.c @@ -255,6 +255,15 @@ bool tree_close(Con *con, kill_window_t kill_window, bool dont_kill_parent, bool x_con_kill(con); con_detach(con); + + /* disable urgency timer, if needed */ + if (con->urgency_timer != NULL) { + DLOG("Removing urgency timer of con %p\n", con); + workspace_update_urgent_flag(con_get_workspace(con)); + ev_timer_stop(main_loop, con->urgency_timer); + FREE(con->urgency_timer); + } + if (con->type != CT_FLOATING_CON) { /* If the container is *not* floating, we might need to re-distribute * percentage values for the resized containers. */ diff --git a/src/workspace.c b/src/workspace.c index 6f560ad9..71102e5c 100644 --- a/src/workspace.c +++ b/src/workspace.c @@ -311,6 +311,23 @@ static void workspace_reassign_sticky(Con *con) { workspace_reassign_sticky(current); } +/* + * Callback to reset the urgent flag of the given con to false. May be started by + * _workspace_show to avoid urgency hints being lost by switching to a workspace + * focusing the con. + * + */ +static void workspace_defer_update_urgent_hint_cb(EV_P_ ev_timer *w, int revents) { + Con *con = w->data; + + DLOG("Resetting urgency flag of con %p by timer\n", con); + con->urgent = false; + workspace_update_urgent_flag(con_get_workspace(con)); + tree_render(); + + ev_timer_stop(main_loop, con->urgency_timer); + FREE(con->urgency_timer); +} static void _workspace_show(Con *workspace) { Con *current, *old = NULL; @@ -350,6 +367,43 @@ static void _workspace_show(Con *workspace) { LOG("switching to %p\n", workspace); Con *next = con_descend_focused(workspace); + /* Memorize current output */ + Con *old_output = con_get_output(focused); + + /* Display urgency hint for a while if the newly visible workspace would + * focus and thereby immediately destroy it */ + if (next->urgent && (int)(config.workspace_urgency_timer * 1000) > 0) { + /* focus for now… */ + con_focus(next); + + /* … but immediately reset urgency flags; they will be set to false by + * the timer callback in case the container is focused at the time of + * its expiration */ + focused->urgent = true; + workspace->urgent = true; + + if (focused->urgency_timer == NULL) { + DLOG("Deferring reset of urgency flag of con %p on newly shown workspace %p\n", + focused, workspace); + focused->urgency_timer = scalloc(sizeof(struct ev_timer)); + /* use a repeating timer to allow for easy resets */ + ev_timer_init(focused->urgency_timer, workspace_defer_update_urgent_hint_cb, + config.workspace_urgency_timer, config.workspace_urgency_timer); + focused->urgency_timer->data = focused; + ev_timer_start(main_loop, focused->urgency_timer); + } else { + DLOG("Resetting urgency timer of con %p on workspace %p\n", + focused, workspace); + ev_timer_again(main_loop, focused->urgency_timer); + } + } else + con_focus(next); + + /* Close old workspace if necessary. This must be done *after* doing + * urgency handling, because tree_close() will do a con_focus() on the next + * client, which will clear the urgency flag too early. Also, there is no + * way for con_focus() to know about when to clear urgency immediately and + * when to defer it. */ if (old && TAILQ_EMPTY(&(old->nodes_head)) && TAILQ_EMPTY(&(old->floating_head))) { /* check if this workspace is currently visible */ if (!workspace_is_visible(old)) { @@ -359,10 +413,6 @@ static void _workspace_show(Con *workspace) { } } - /* Memorize current output */ - Con *old_output = con_get_output(focused); - - con_focus(next); workspace->fullscreen_mode = CF_OUTPUT; LOG("focused now = %p / %s\n", focused, focused->name); diff --git a/testcases/t/198-urgency-timer.t b/testcases/t/198-urgency-timer.t new file mode 100644 index 00000000..d3cdb3d9 --- /dev/null +++ b/testcases/t/198-urgency-timer.t @@ -0,0 +1,95 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Tests whether the urgency timer works as expected and does not break +# urgency handling. +# + +use List::Util qw(first); +use i3test i3_autostart => 0; +use Time::HiRes qw(sleep); + +# Ensure the pointer is at (0, 0) so that we really start on the first +# (the left) workspace. +$x->root->warp_pointer(0, 0); + +my $config = <add_hint('urgency'); +sync_with_i3; + +####################################################################### +# Create a window on ws1, then switch to ws2, set urgency, switch back +####################################################################### + +isnt($x->input_focus, $w->id, 'window not focused'); + +my @content = @{get_ws_content($tmp1)}; +my @urgent = grep { $_->{urgent} } @content; +is(@urgent, 1, "window marked as urgent"); + +# switch to ws1 +cmd "workspace $tmp1"; + +# this will start the timer +sleep(0.1); +@content = @{get_ws_content($tmp1)}; +@urgent = grep { $_->{urgent} } @content; +is(@urgent, 1, 'window still marked as urgent'); + +# now check if the timer was triggered +cmd "workspace $tmp2"; + +sleep(0.1); +@content = @{get_ws_content($tmp1)}; +@urgent = grep { $_->{urgent} } @content; +is(@urgent, 0, 'window not marked as urgent anymore'); + +####################################################################### +# Create another window on ws1, focus it, switch to ws2, make the other +# window urgent, and switch back. This should not trigger the timer. +####################################################################### + +cmd "workspace $tmp1"; +my $w2 = open_window; +is($x->input_focus, $w2->id, 'window 2 focused'); + +cmd "workspace $tmp2"; +$w->add_hint('urgency'); +sync_with_i3; + +@content = @{get_ws_content($tmp1)}; +@urgent = grep { $_->{urgent} } @content; +is(@urgent, 1, 'window 1 marked as urgent'); + +# Switch back to ws1. This should focus w2. +cmd "workspace $tmp1"; +is($x->input_focus, $w2->id, 'window 2 focused'); + +@content = @{get_ws_content($tmp1)}; +@urgent = grep { $_->{urgent} } @content; +is(@urgent, 1, 'window 1 still marked as urgent'); + +# explicitly focusing the window should result in immediate urgency reset +cmd '[id="' . $w->id . '"] focus'; +@content = @{get_ws_content($tmp1)}; +@urgent = grep { $_->{urgent} } @content; +is(@urgent, 0, 'window 1 not marked as urgent anymore'); + +done_testing;