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
next
Simon Elsbrock 2012-09-22 13:48:22 +02:00 committed by Michael Stapelberg
parent e15e37f922
commit 28104a480c
10 changed files with 221 additions and 5 deletions

View File

@ -874,6 +874,30 @@ workspace_auto_back_and_forth <yes|no>
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 <timeout> 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

View File

@ -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;

View File

@ -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;

View File

@ -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; }

View File

@ -728,6 +728,7 @@ void parse_file(const char *f) {
%token <color> 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 <number> optional_release
%type <string> command
%type <string> word_or_number
%type <string> duration
%type <string> qstring_or_number
%type <string> optional_workspace_name
%type <string> 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
{

View File

@ -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) {

View File

@ -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) {

View File

@ -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. */

View File

@ -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);

View File

@ -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 = <<EOT;
# i3 config file (v4)
font -misc-fixed-medium-r-normal--13-120-75-75-C-70-iso10646-1
force_display_urgency_hint 150ms
EOT
my $pid = launch_with_config($config);
#####################################################################
# Initial setup: one window on ws1, empty ws2
#####################################################################
my $tmp1 = fresh_workspace;
my $w = open_window;
my $tmp2 = fresh_workspace;
cmd "workspace $tmp2";
$w->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;