From 28104a480ca9a2ab135488ab98545d67e81a9f9d Mon Sep 17 00:00:00 2001 From: Simon Elsbrock Date: Sat, 22 Sep 2012 13:48:22 +0200 Subject: [PATCH] implement delayed urgency hint reset If there is a client with an urgency hint on another workspace and switching to this workspace would cause the urgency to be reset (by moving the focusing to the client), delay the reset by some time. This gives the user the chance to see it. This commit adds the possibility to configure the urgency delay timer duration using the 'force_display_urgency_hint' directive. Also, documentation and a testcase was added to allow for automated checks of the intended behavior. fixes #482 --- docs/userguide | 24 +++++++++ include/config.h | 7 +++ include/data.h | 3 ++ src/cfgparse.l | 2 + src/cfgparse.y | 16 ++++++ src/config.c | 4 ++ src/handlers.c | 8 ++- src/tree.c | 9 ++++ src/workspace.c | 58 ++++++++++++++++++-- testcases/t/198-urgency-timer.t | 95 +++++++++++++++++++++++++++++++++ 10 files changed, 221 insertions(+), 5 deletions(-) create mode 100644 testcases/t/198-urgency-timer.t 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; -- 2.39.5