From f44c87685bc1c2755133e6d41867b6ef107d98bf Mon Sep 17 00:00:00 2001 From: =?utf8?q?Ingo=20B=C3=BCrk?= Date: Sun, 30 Aug 2015 10:10:37 +0200 Subject: [PATCH] Support _NET_WM_VISIBLE_NAME. As per specification this is necessary since we can display custom titles with title_format. fixes #1872 --- include/atoms.xmacro | 1 + include/ewmh.h | 6 +++ include/window.h | 7 +++ src/commands.c | 10 +++- src/ewmh.c | 16 +++++- src/window.c | 75 +++++++++++++++++++++++++++++ src/x.c | 65 +------------------------ testcases/t/251-ewmh-visible-name.t | 70 +++++++++++++++++++++++++++ 8 files changed, 183 insertions(+), 67 deletions(-) create mode 100644 testcases/t/251-ewmh-visible-name.t diff --git a/include/atoms.xmacro b/include/atoms.xmacro index 00a346db..80e3bbf0 100644 --- a/include/atoms.xmacro +++ b/include/atoms.xmacro @@ -1,6 +1,7 @@ xmacro(_NET_SUPPORTED) xmacro(_NET_SUPPORTING_WM_CHECK) xmacro(_NET_WM_NAME) +xmacro(_NET_WM_VISIBLE_NAME) xmacro(_NET_WM_MOVERESIZE) xmacro(_NET_WM_STATE_FULLSCREEN) xmacro(_NET_WM_STATE_DEMANDS_ATTENTION) diff --git a/include/ewmh.h b/include/ewmh.h index 8fb7902a..d94b7f3a 100644 --- a/include/ewmh.h +++ b/include/ewmh.h @@ -45,6 +45,12 @@ void ewmh_update_desktop_viewport(void); */ void ewmh_update_active_window(xcb_window_t window); +/** + * Updates _NET_WM_VISIBLE_NAME. + * + */ +void ewmh_update_visible_name(xcb_window_t window, const char *name); + /** * Updates the _NET_CLIENT_LIST hint. Used for window listers. */ diff --git a/include/window.h b/include/window.h index 7a248277..395c9883 100644 --- a/include/window.h +++ b/include/window.h @@ -81,3 +81,10 @@ void window_update_hints(i3Window *win, xcb_get_property_reply_t *prop, bool *ur * */ void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, border_style_t *motif_border_style); + +/** + * Returns the window title considering the current title format. + * If no format is set, this will simply return the window's name. + * + */ +i3String *window_parse_title_format(i3Window *win); diff --git a/src/commands.c b/src/commands.c index 62adcc65..dc440f4a 100644 --- a/src/commands.c +++ b/src/commands.c @@ -1922,9 +1922,17 @@ void cmd_title_format(I3_CMD, char *format) { /* If we only display the title without anything else, we can skip the parsing step, * so we remove the title format altogether. */ - if (strcasecmp(format, "%title") != 0) + if (strcasecmp(format, "%title") != 0) { current->con->window->title_format = sstrdup(format); + i3String *formatted_title = window_parse_title_format(current->con->window); + ewmh_update_visible_name(current->con->window->id, i3string_as_utf8(formatted_title)); + I3STRING_FREE(formatted_title); + } else { + /* We can remove _NET_WM_VISIBLE_NAME since we don't display a custom title. */ + ewmh_update_visible_name(current->con->window->id, NULL); + } + /* Make sure the window title is redrawn immediately. */ current->con->window->name_x_changed = true; } diff --git a/src/ewmh.c b/src/ewmh.c index a1d2489e..70cf7718 100644 --- a/src/ewmh.c +++ b/src/ewmh.c @@ -148,6 +148,18 @@ void ewmh_update_active_window(xcb_window_t window) { A__NET_ACTIVE_WINDOW, XCB_ATOM_WINDOW, 32, 1, &window); } +/* + * Updates _NET_WM_VISIBLE_NAME. + * + */ +void ewmh_update_visible_name(xcb_window_t window, const char *name) { + if (name != NULL) { + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, window, A__NET_WM_VISIBLE_NAME, A_UTF8_STRING, 8, strlen(name), name); + } else { + xcb_delete_property(conn, window, A__NET_WM_VISIBLE_NAME); + } +} + /* * i3 currently does not support _NET_WORKAREA, because it does not correspond * to i3’s concept of workspaces. See also: @@ -234,6 +246,6 @@ void ewmh_setup_hints(void) { /* I’m not entirely sure if we need to keep _NET_WM_NAME on root. */ xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_WM_NAME, A_UTF8_STRING, 8, strlen("i3"), "i3"); - /* only send the first 30 atoms (last one is _NET_CLOSE_WINDOW) increment that number when adding supported atoms */ - xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 32, 30, supported_atoms); + /* only send the first 31 atoms (last one is _NET_CLOSE_WINDOW) increment that number when adding supported atoms */ + xcb_change_property(conn, XCB_PROP_MODE_REPLACE, root, A__NET_SUPPORTED, XCB_ATOM_ATOM, 32, /* number of atoms */ 31, supported_atoms); } diff --git a/src/window.c b/src/window.c index 764cfca5..278918c7 100644 --- a/src/window.c +++ b/src/window.c @@ -66,6 +66,8 @@ void window_update_name(i3Window *win, xcb_get_property_reply_t *prop, bool befo i3string_free(win->name); win->name = i3string_from_utf8_with_length(xcb_get_property_value(prop), xcb_get_property_value_length(prop)); + if (win->title_format != NULL) + ewmh_update_visible_name(win->id, i3string_as_utf8(window_parse_title_format(win))); win->name_x_changed = true; LOG("_NET_WM_NAME changed to \"%s\"\n", i3string_as_utf8(win->name)); @@ -104,6 +106,8 @@ void window_update_name_legacy(i3Window *win, xcb_get_property_reply_t *prop, bo i3string_free(win->name); win->name = i3string_from_utf8_with_length(xcb_get_property_value(prop), xcb_get_property_value_length(prop)); + if (win->title_format != NULL) + ewmh_update_visible_name(win->id, i3string_as_utf8(window_parse_title_format(win))); LOG("WM_NAME changed to \"%s\"\n", i3string_as_utf8(win->name)); LOG("Using legacy window title. Note that in order to get Unicode window " @@ -329,3 +333,74 @@ void window_update_motif_hints(i3Window *win, xcb_get_property_reply_t *prop, bo #undef MWM_DECOR_BORDER #undef MWM_DECOR_TITLE } + +/* + * Returns the window title considering the current title format. + * If no format is set, this will simply return the window's name. + * + */ +i3String *window_parse_title_format(i3Window *win) { + /* We need to ensure that we only escape the window title if pango + * is used by the current font. */ + const bool is_markup = font_is_pango(); + + char *format = win->title_format; + if (format == NULL) + return i3string_copy(win->name); + + /* We initialize these lazily so we only escape them if really necessary. */ + const char *escaped_title = NULL; + const char *escaped_class = NULL; + const char *escaped_instance = NULL; + + /* We have to first iterate over the string to see how much buffer space + * we need to allocate. */ + int buffer_len = strlen(format) + 1; + for (char *walk = format; *walk != '\0'; walk++) { + if (STARTS_WITH(walk, "%title")) { + if (escaped_title == NULL) + escaped_title = i3string_as_utf8(is_markup ? i3string_escape_markup(win->name) : win->name); + + buffer_len = buffer_len - strlen("%title") + strlen(escaped_title); + walk += strlen("%title") - 1; + } else if (STARTS_WITH(walk, "%class")) { + if (escaped_class == NULL) + escaped_class = is_markup ? g_markup_escape_text(win->class_class, -1) : win->class_class; + + buffer_len = buffer_len - strlen("%class") + strlen(escaped_class); + walk += strlen("%class") - 1; + } else if (STARTS_WITH(walk, "%instance")) { + if (escaped_instance == NULL) + escaped_instance = is_markup ? g_markup_escape_text(win->class_instance, -1) : win->class_instance; + + buffer_len = buffer_len - strlen("%instance") + strlen(escaped_instance); + walk += strlen("%instance") - 1; + } + } + + /* Now we can parse the format string. */ + char buffer[buffer_len]; + char *outwalk = buffer; + for (char *walk = format; *walk != '\0'; walk++) { + if (*walk != '%') { + *(outwalk++) = *walk; + continue; + } + + if (STARTS_WITH(walk + 1, "title")) { + outwalk += sprintf(outwalk, "%s", escaped_title); + walk += strlen("title"); + } else if (STARTS_WITH(walk + 1, "class")) { + outwalk += sprintf(outwalk, "%s", escaped_class); + walk += strlen("class"); + } else if (STARTS_WITH(walk + 1, "instance")) { + outwalk += sprintf(outwalk, "%s", escaped_instance); + walk += strlen("instance"); + } + } + *outwalk = '\0'; + + i3String *formatted = i3string_from_utf8(buffer); + i3string_set_markup(formatted, is_markup); + return formatted; +} diff --git a/src/x.c b/src/x.c index 16417ffc..337e268c 100644 --- a/src/x.c +++ b/src/x.c @@ -302,69 +302,6 @@ void x_window_kill(xcb_window_t window, kill_window_t kill_window) { free(event); } -static i3String *parse_title_format(struct Window *win) { - /* We need to ensure that we only escape the window title if pango - * is used by the current font. */ - const bool is_markup = font_is_pango(); - - char *format = win->title_format; - /* We initialize these lazily so we only escape them if really necessary. */ - const char *escaped_title = NULL; - const char *escaped_class = NULL; - const char *escaped_instance = NULL; - - /* We have to first iterate over the string to see how much buffer space - * we need to allocate. */ - int buffer_len = strlen(format) + 1; - for (char *walk = format; *walk != '\0'; walk++) { - if (STARTS_WITH(walk, "%title")) { - if (escaped_title == NULL) - escaped_title = i3string_as_utf8(is_markup ? i3string_escape_markup(win->name) : win->name); - - buffer_len = buffer_len - strlen("%title") + strlen(escaped_title); - walk += strlen("%title") - 1; - } else if (STARTS_WITH(walk, "%class")) { - if (escaped_class == NULL) - escaped_class = is_markup ? g_markup_escape_text(win->class_class, -1) : win->class_class; - - buffer_len = buffer_len - strlen("%class") + strlen(escaped_class); - walk += strlen("%class") - 1; - } else if (STARTS_WITH(walk, "%instance")) { - if (escaped_instance == NULL) - escaped_instance = is_markup ? g_markup_escape_text(win->class_instance, -1) : win->class_instance; - - buffer_len = buffer_len - strlen("%instance") + strlen(escaped_instance); - walk += strlen("%instance") - 1; - } - } - - /* Now we can parse the format string. */ - char buffer[buffer_len]; - char *outwalk = buffer; - for (char *walk = format; *walk != '\0'; walk++) { - if (*walk != '%') { - *(outwalk++) = *walk; - continue; - } - - if (STARTS_WITH(walk + 1, "title")) { - outwalk += sprintf(outwalk, "%s", escaped_title); - walk += strlen("title"); - } else if (STARTS_WITH(walk + 1, "class")) { - outwalk += sprintf(outwalk, "%s", escaped_class); - walk += strlen("class"); - } else if (STARTS_WITH(walk + 1, "instance")) { - outwalk += sprintf(outwalk, "%s", escaped_instance); - walk += strlen("instance"); - } - } - *outwalk = '\0'; - - i3String *formatted = i3string_from_utf8(buffer); - i3string_set_markup(formatted, is_markup); - return formatted; -} - /* * Draws the decoration of the given container onto its parent. * @@ -612,7 +549,7 @@ void x_draw_decoration(Con *con) { I3STRING_FREE(mark); } - i3String *title = win->title_format == NULL ? win->name : parse_title_format(win); + i3String *title = win->title_format == NULL ? win->name : window_parse_title_format(win); draw_text(title, parent->pixmap, parent->pm_gc, con->deco_rect.x + logical_px(2) + indent_px, con->deco_rect.y + text_offset_y, diff --git a/testcases/t/251-ewmh-visible-name.t b/testcases/t/251-ewmh-visible-name.t new file mode 100644 index 00000000..c201b398 --- /dev/null +++ b/testcases/t/251-ewmh-visible-name.t @@ -0,0 +1,70 @@ +#!perl +# vim:ts=4:sw=4:expandtab +# +# Please read the following documents before working on tests: +# • http://build.i3wm.org/docs/testsuite.html +# (or docs/testsuite) +# +# • http://build.i3wm.org/docs/lib-i3test.html +# (alternatively: perldoc ./testcases/lib/i3test.pm) +# +# • http://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) +# +# Tests that _NET_WM_VISIBLE_NAME is set correctly. +# Ticket: #1872 +use i3test; +use X11::XCB qw(:all); + +my ($con); + +sub get_visible_name { + sync_with_i3; + my ($con) = @_; + + my $cookie = $x->get_property( + 0, + $con->{id}, + $x->atom(name => '_NET_WM_VISIBLE_NAME')->id, + $x->atom(name => 'UTF8_STRING')->id, + 0, + 4096 + ); + + my $reply = $x->get_property_reply($cookie->{sequence}); + return undef if $reply->{value_len} == 0; + return $reply->{value}; +} + +############################################################################### +# 1: _NET_WM_VISIBLE_NAME is set when the title format of a window is changed. +############################################################################### + +fresh_workspace; +$con = open_window(name => 'boring title'); +is(get_visible_name($con), undef, 'sanity check: initially no visible name is set'); + +cmd 'title_format custom'; +is(get_visible_name($con), 'custom', 'the visible name is updated'); + +cmd 'title_format "%title"'; +is(get_visible_name($con), 'boring title', 'markup is returned as is'); + +############################################################################### +# 2: _NET_WM_VISIBLE_NAME is removed if not needed. +############################################################################### + +fresh_workspace; +$con = open_window(name => 'boring title'); +cmd 'title_format custom'; +is(get_visible_name($con), 'custom', 'sanity check: a visible name is set'); + +cmd 'title_format %title'; +is(get_visible_name($con), undef, 'the visible name is removed again'); + +############################################################################### + +done_testing; -- 2.39.5