diff --git a/draw.h b/draw.h index 224a9c5e..0a414752 100644 --- a/draw.h +++ b/draw.h @@ -27,6 +27,7 @@ #include #include /* for GError */ +#include "common/array.h" #include "common/util.h" typedef struct area_t area_t; @@ -69,6 +70,13 @@ a_iso2utf8(const char *str, ssize_t len, char **dest, ssize_t *dlen) return false; } +static inline void +cairo_surface_array_destroy_surface(cairo_surface_t **s) +{ + cairo_surface_destroy(*s); +} +DO_ARRAY(cairo_surface_t *, cairo_surface, cairo_surface_array_destroy_surface) + cairo_surface_t *draw_surface_from_data(int width, int height, uint32_t *data); cairo_surface_t *draw_dup_image_surface(cairo_surface_t *surface); cairo_surface_t *draw_load_image(lua_State *L, const char *path, GError **error); diff --git a/ewmh.c b/ewmh.c index 60827647..c2811bb1 100644 --- a/ewmh.c +++ b/ewmh.c @@ -676,63 +676,62 @@ ewmh_window_icon_get_unchecked(xcb_window_t w) } static cairo_surface_t * -ewmh_window_icon_from_reply(xcb_get_property_reply_t *r, uint32_t preferred_size) +ewmh_window_icon_from_reply_next(uint32_t **data, uint32_t *data_end) { - uint32_t *data, *end, *found_data = 0; - uint32_t found_size = 0; + uint32_t width, height; + uint64_t data_len; + uint32_t *icon_data; - if(!r || r->type != XCB_ATOM_CARDINAL || r->format != 32 || r->length < 2) - return 0; + if(data_end - *data <= 2) + return NULL; - data = (uint32_t *) xcb_get_property_value(r); - if (!data) return 0; + width = (*data)[0]; + height = (*data)[1]; - end = data + r->length; + /* Check that we have enough data, handling overflow */ + data_len = width * (uint64_t) height; + if (width < 1 || height < 1 || data_len > (uint64_t) (data_end - *data) - 2) + return NULL; - /* Goes over the icon data and picks the icon that best matches the size preference. - * In case the size match is not exact, picks the closest bigger size if present, - * closest smaller size otherwise. - */ - while (data + 1 < end) { - /* check whether the data size specified by width and height fits into the array we got */ - uint64_t data_size = (uint64_t) data[0] * data[1]; - if (data_size > (uint64_t) (end - data - 2)) break; + icon_data = *data + 2; + *data += 2 + data_len; + return draw_surface_from_data(width, height, icon_data); +} - /* use the greater of the two dimensions to match against the preferred size */ - uint32_t size = MAX(data[0], data[1]); +static cairo_surface_array_t +ewmh_window_icon_from_reply(xcb_get_property_reply_t *r) +{ + uint32_t *data, *data_end; + cairo_surface_array_t result; + cairo_surface_t *s; - /* pick the icon if it's a better match than the one we already have */ - bool found_icon_too_small = found_size < preferred_size; - bool found_icon_too_large = found_size > preferred_size; - bool icon_empty = data[0] == 0 || data[1] == 0; - bool better_because_bigger = found_icon_too_small && size > found_size; - bool better_because_smaller = found_icon_too_large && - size >= preferred_size && size < found_size; - if (!icon_empty && (better_because_bigger || better_because_smaller || found_size == 0)) - { - found_data = data; - found_size = size; - } + cairo_surface_array_init(&result); + if(!r || r->type != XCB_ATOM_CARDINAL || r->format != 32) + return result; - data += data_size + 2; + data = (uint32_t*) xcb_get_property_value(r); + data_end = &data[r->length]; + if(!data) + return result; + + while ((s = ewmh_window_icon_from_reply_next(&data, data_end)) != NULL) { + cairo_surface_array_push(&result, s); } - if (!found_data) return 0; - - return draw_surface_from_data(found_data[0], found_data[1], found_data + 2); + return result; } /** Get NET_WM_ICON. * \param cookie The cookie. - * \return The number of elements on stack. + * \return An array of icons. */ -cairo_surface_t * -ewmh_window_icon_get_reply(xcb_get_property_cookie_t cookie, uint32_t preferred_size) +cairo_surface_array_t +ewmh_window_icon_get_reply(xcb_get_property_cookie_t cookie) { xcb_get_property_reply_t *r = xcb_get_property_reply(globalconf.connection, cookie, NULL); - cairo_surface_t *surface = ewmh_window_icon_from_reply(r, preferred_size); + cairo_surface_array_t result = ewmh_window_icon_from_reply(r); p_delete(&r); - return surface; + return result; } // vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/ewmh.h b/ewmh.h index 67e3f3ae..f7d72b91 100644 --- a/ewmh.h +++ b/ewmh.h @@ -28,6 +28,7 @@ #include "strut.h" typedef struct client_t client_t; +typedef struct cairo_surface_array_t cairo_surface_array_t; void ewmh_init(void); void ewmh_init_lua(void); @@ -42,7 +43,7 @@ void ewmh_process_client_strut(client_t *); void ewmh_update_strut(xcb_window_t, strut_t *); void ewmh_update_window_type(xcb_window_t window, uint32_t type); xcb_get_property_cookie_t ewmh_window_icon_get_unchecked(xcb_window_t); -cairo_surface_t *ewmh_window_icon_get_reply(xcb_get_property_cookie_t, uint32_t preferred_size); +cairo_surface_array_t ewmh_window_icon_get_reply(xcb_get_property_cookie_t); #endif // vim: filetype=c:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/titlebar.lua b/lib/awful/titlebar.lua index ca9ce240..a7407aaf 100644 --- a/lib/awful/titlebar.lua +++ b/lib/awful/titlebar.lua @@ -12,6 +12,7 @@ local gmath = require("gears.math") local abutton = require("awful.button") local aclient = require("awful.client") local atooltip = require("awful.tooltip") +local clienticon = require("awful.widget.clienticon") local beautiful = require("beautiful") local drawable = require("wibox.drawable") local imagebox = require("wibox.widget.imagebox") @@ -572,14 +573,7 @@ end -- @param c The client for which an icon widget should be created. -- @return The icon widget. function titlebar.widget.iconwidget(c) - local ret = imagebox() - local function update() - ret:set_image(c.icon) - end - c:connect_signal("property::icon", update) - update() - - return ret + return clienticon(c) end --- Create a new button widget. A button widget displays an image and reacts to diff --git a/lib/awful/widget/clienticon.lua b/lib/awful/widget/clienticon.lua new file mode 100644 index 00000000..b5eb9810 --- /dev/null +++ b/lib/awful/widget/clienticon.lua @@ -0,0 +1,118 @@ +--- Container showing the icon of a client. +-- @author Uli Schlachter +-- @copyright 2017 Uli Schlachter +-- @classmod awful.widget.clienticon + +local base = require("wibox.widget.base") +local surface = require("gears.surface") +local util = require("awful.util") + +local clienticon = {} +local instances = setmetatable({}, { __mode = "k" }) + +local function find_best_icon(sizes, width, height) + local best, best_size + for k, size in ipairs(sizes) do + if not best then + best, best_size = k, size + else + local best_too_small = best_size[1] < width or best_size[2] < height + local best_too_large = best_size[1] > width or best_size[2] > height + local better_because_bigger = best_too_small and size[1] > best_size[1] and size[2] > best_size[2] + local better_because_smaller = best_too_large and size[1] < best_size[1] and size[2] < best_size[2] + and size[1] >= width and size[2] >= height + if better_because_bigger or better_because_smaller then + best, best_size = k, size + end + end + end + return best, best_size +end + +function clienticon:draw(_, cr, width, height) + local c = self._private.client + if not c.valid then + return + end + + local index, size = find_best_icon(c.icon_sizes, width, height) + if not index then + return + end + + local aspect_w = width / size[1] + local aspect_h = height / size[2] + local aspect = math.min(aspect_w, aspect_h) + cr:scale(aspect, aspect) + + local s = surface(c:get_icon(index)) + cr:set_source_surface(s, 0, 0) + cr:paint() +end + +function clienticon:fit(_, width, height) + local c = self._private.client + if not c.valid then + return 0, 0 + end + + local index, size = find_best_icon(c.icon_sizes, width, height) + if not index then + return 0, 0 + end + + local w, h = size[1], size[2] + + if w > width then + h = h * width / w + w = width + end + if h > height then + w = w * height / h + h = height + end + + if h == 0 or w == 0 then + return 0, 0 + end + + local aspect = math.min(width / w, height / h) + return w * aspect, h * aspect +end + +--- Returns a new clienticon. +-- @tparam client c The client whose icon should be displayed. +-- @treturn widget A new `widget` +-- @function awful.widget.clienticon +local function new(c) + local ret = base.make_widget(nil, nil, {enable_properties = true}) + + util.table.crush(ret, clienticon, true) + + ret._private.client = c + + instances[ret] = true + + return ret +end + +client.connect_signal("property::icon", function(c) + for obj in pairs(instances) do + if obj._private.client.valid and obj._private.client == c then + obj:emit_signal("widget::layout_changed") + obj:emit_signal("widget::redraw_needed") + end + end +end) + +--@DOC_widget_COMMON@ + +--@DOC_object_COMMON@ + +return setmetatable(clienticon, { + __call = function(_, ...) + return new(...) + end +}) + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/lib/awful/widget/init.lua b/lib/awful/widget/init.lua index b10f21c7..ca0ed617 100644 --- a/lib/awful/widget/init.lua +++ b/lib/awful/widget/init.lua @@ -20,6 +20,7 @@ return keyboardlayout = require("awful.widget.keyboardlayout"); watch = require("awful.widget.watch"); only_on_screen = require("awful.widget.only_on_screen"); + clienticon = require("awful.widget.clienticon"); } -- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 diff --git a/objects/client.c b/objects/client.c index 7683aa4c..3adfe3ac 100644 --- a/objects/client.c +++ b/objects/client.c @@ -381,6 +381,19 @@ * @param surface */ +/** + * The available sizes of client icons. This is a table where each entry + * contains the width and height of an icon. + * + * **Signal:** + * + * * *property::icon* + * + * @property icon_sizes + * @tparam table sizes + * @see `get_icon` + */ + /** * Client screen. * @@ -826,6 +839,7 @@ client_wipe(client_t *c) { key_array_wipe(&c->keys); xcb_icccm_get_wm_protocols_reply_wipe(&c->protocols); + cairo_surface_array_wipe(&c->icons); p_delete(&c->machine); p_delete(&c->class); p_delete(&c->instance); @@ -834,8 +848,6 @@ client_wipe(client_t *c) p_delete(&c->name); p_delete(&c->alt_name); p_delete(&c->startup_id); - if(c->icon) - cairo_surface_destroy(c->icon); } /** Change the clients urgency flag. @@ -2319,27 +2331,38 @@ luaA_client_isvisible(lua_State *L) return 1; } +/** Set client icons. + * \param L The Lua VM state. + * \param array Array of icons to set. + */ +void +client_set_icons(client_t *c, cairo_surface_array_t array) +{ + cairo_surface_array_wipe(&c->icons); + c->icons = array; + + lua_State *L = globalconf_get_lua_State(); + luaA_object_push(L, c); + luaA_object_emit_signal(L, -1, "property::icon", 0); + lua_pop(L, 1); +} + /** Set a client icon. * \param L The Lua VM state. * \param cidx The client index on the stack. * \param iidx The image index on the stack. */ -void +static void client_set_icon(client_t *c, cairo_surface_t *s) { - lua_State *L = globalconf_get_lua_State(); - - if (s) - s = draw_dup_image_surface(s); - if(c->icon) - cairo_surface_destroy(c->icon); - c->icon = s; - - luaA_object_push(L, c); - luaA_object_emit_signal(L, -1, "property::icon", 0); - lua_pop(L, 1); + cairo_surface_array_t array; + cairo_surface_array_init(&array); + if (s && cairo_surface_status(s) == CAIRO_STATUS_SUCCESS) + cairo_surface_array_push(&array, draw_dup_image_surface(s)); + client_set_icons(c, array); } + /** Set a client icon. * \param c The client to change. * \param icon A bitmap containing the icon. @@ -3079,10 +3102,38 @@ luaA_client_get_content(lua_State *L, client_t *c) static int luaA_client_get_icon(lua_State *L, client_t *c) { - if(!c->icon) + if(c->icons.len == 0) return 0; + + /* Pick the closest available size, only picking a smaller icon if no bigger + * one is available. + */ + cairo_surface_t *found = NULL; + int found_size = 0; + int preferred_size = globalconf.preferred_icon_size; + + foreach(surf, c->icons) + { + int width = cairo_image_surface_get_width(*surf); + int height = cairo_image_surface_get_height(*surf); + int size = MAX(width, height); + + /* pick the icon if it's a better match than the one we already have */ + bool found_icon_too_small = found_size < preferred_size; + bool found_icon_too_large = found_size > preferred_size; + bool icon_empty = width == 0 || height == 0; + bool better_because_bigger = found_icon_too_small && size > found_size; + bool better_because_smaller = found_icon_too_large && + size >= preferred_size && size < found_size; + if (!icon_empty && (better_because_bigger || better_because_smaller || found_size == 0)) + { + found = *surf; + found_size = size; + } + } + /* lua gets its own reference which it will have to destroy */ - lua_pushlightuserdata(L, cairo_surface_reference(c->icon)); + lua_pushlightuserdata(L, cairo_surface_reference(found)); return 1; } @@ -3387,6 +3438,50 @@ luaA_client_keys(lua_State *L) return luaA_key_array_get(L, 1, keys); } +static int +luaA_client_get_icon_sizes(lua_State *L) +{ + int index = 1; + client_t *c = luaA_checkudata(L, 1, &client_class); + + lua_newtable(L); + foreach (s, c->icons) { + /* Create a table { width, height } and append it to the table */ + lua_createtable(L, 2, 0); + + lua_pushinteger(L, cairo_image_surface_get_width(*s)); + lua_rawseti(L, -2, 1); + + lua_pushinteger(L, cairo_image_surface_get_height(*s)); + lua_rawseti(L, -2, 2); + + lua_rawseti(L, -2, index++); + } + return 1; +} + +/** Get the client's n-th icon. + * + * **Signal:** + * + * * *property::icon* + * + * @tparam interger index The index in the list of icons to get. + * @treturn surface A lightuserdata for a cairo surface. This reference must be + * destroyed! + * @function get_icon + */ +static int +luaA_client_get_some_icon(lua_State *L) +{ + client_t *c = luaA_checkudata(L, 1, &client_class); + int index = luaL_checkinteger(L, 2); + luaL_argcheck(L, (index >= 1 && index <= c->icons.len), 2, + "invalid icon index"); + lua_pushlightuserdata(L, cairo_surface_reference(c->icons.tab[index-1])); + return 1; +} + static int client_tostring(lua_State *L, client_t *c) { @@ -3472,6 +3567,7 @@ client_class_setup(lua_State *L) { "titlebar_right", luaA_client_titlebar_right }, { "titlebar_bottom", luaA_client_titlebar_bottom }, { "titlebar_left", luaA_client_titlebar_left }, + { "get_icon", luaA_client_get_some_icon }, { NULL, NULL } }; @@ -3570,6 +3666,10 @@ client_class_setup(lua_State *L) (lua_class_propfunc_t) luaA_client_set_icon, (lua_class_propfunc_t) luaA_client_get_icon, (lua_class_propfunc_t) luaA_client_set_icon); + luaA_class_add_property(&client_class, "icon_sizes", + NULL, + (lua_class_propfunc_t) luaA_client_get_icon_sizes, + NULL); luaA_class_add_property(&client_class, "ontop", (lua_class_propfunc_t) luaA_client_set_ontop, (lua_class_propfunc_t) luaA_client_get_ontop, diff --git a/objects/client.h b/objects/client.h index ad94e133..ee738498 100644 --- a/objects/client.h +++ b/objects/client.h @@ -116,8 +116,8 @@ struct client_t xcb_icccm_get_wm_protocols_reply_t protocols; /** Key bindings */ key_array_t keys; - /** Icon */ - cairo_surface_t *icon; + /** Icons */ + cairo_surface_array_t icons; /** True if we ever got an icon from _NET_WM_ICON */ bool have_ewmh_icon; /** Size hints */ @@ -186,7 +186,7 @@ void client_set_transient_for(lua_State *L, int, client_t *); void client_set_name(lua_State *L, int, char *); void client_set_alt_name(lua_State *L, int, char *); void client_set_group_window(lua_State *, int, xcb_window_t); -void client_set_icon(client_t *, cairo_surface_t *); +void client_set_icons(client_t *, cairo_surface_array_t); void client_set_icon_from_pixmaps(client_t *, xcb_pixmap_t, xcb_pixmap_t); void client_set_skip_taskbar(lua_State *, int, bool); void client_focus(client_t *); diff --git a/property.c b/property.c index de000d77..e57effb4 100644 --- a/property.c +++ b/property.c @@ -206,8 +206,6 @@ property_update_wm_hints(client_t *c, xcb_get_property_cookie_t cookie) else client_set_icon_from_pixmaps(c, wmh.icon_pixmap, XCB_NONE); } - else - client_set_icon(c, NULL); } lua_pop(L, 1); @@ -262,14 +260,14 @@ property_get_net_wm_icon(client_t *c) void property_update_net_wm_icon(client_t *c, xcb_get_property_cookie_t cookie) { - cairo_surface_t *surface = ewmh_window_icon_get_reply(cookie, globalconf.preferred_icon_size); - - if(!surface) + cairo_surface_array_t array = ewmh_window_icon_get_reply(cookie); + if (array.len == 0) + { + cairo_surface_array_wipe(&array); return; - + } c->have_ewmh_icon = true; - client_set_icon(c, surface); - cairo_surface_destroy(surface); + client_set_icons(c, array); } xcb_get_property_cookie_t