From f8c6ac2ed10ed7a3ecb03868a3b0b97deca5c2db Mon Sep 17 00:00:00 2001 From: Přemysl Eric Janouch Date: Sat, 10 Feb 2024 08:46:11 +0100 Subject: Make liberty-xui load PNG program icons X11 applications now have a dependency on libpng. This makes use of a new related liberty-xdg module, which can be used separately. --- liberty-xdg.c | 766 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ liberty-xui.c | 50 +++- 2 files changed, 808 insertions(+), 8 deletions(-) create mode 100644 liberty-xdg.c diff --git a/liberty-xdg.c b/liberty-xdg.c new file mode 100644 index 0000000..2c95668 --- /dev/null +++ b/liberty-xdg.c @@ -0,0 +1,766 @@ +/* + * liberty-xdg.c: the ultimate C unlibrary: freedesktop.org specifications + * + * Copyright (c) 2023 - 2024, Přemysl Eric Janouch + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +// This files assumes you've already included liberty.c. + +#ifdef LIBERTY_XDG_WANT_X11 +#include +#include + +#include +#endif // LIBERTY_XDG_WANT_X11 + +// --- XSettings --------------------------------------------------------------- + +#ifdef LIBERTY_XDG_WANT_X11 + +struct xdg_xsettings_setting +{ + enum xdg_xsettings_type + { + XDG_XSETTINGS_INTEGER, + XDG_XSETTINGS_STRING, + XDG_XSETTINGS_COLOR, + } + type; ///< What's stored in the union + uint32_t serial; ///< Serial of the last change + union + { + int32_t integer; + struct str string; + struct { uint16_t red, green, blue, alpha; } color; + }; +}; + +static void +xdg_xsettings_setting_destroy (struct xdg_xsettings_setting *self) +{ + if (self->type == XDG_XSETTINGS_STRING) + str_free (&self->string); + free (self); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct xdg_xsettings +{ + struct str_map settings; ///< Name -> xdg_xsettings_setting +}; + +static void +xdg_xsettings_free (struct xdg_xsettings *self) +{ + str_map_free (&self->settings); +} + +static struct xdg_xsettings +xdg_xsettings_make (void) +{ + return (struct xdg_xsettings) + { + .settings = + str_map_make ((str_map_free_fn) xdg_xsettings_setting_destroy), + }; +} + +static void +xdg_xsettings_update (struct xdg_xsettings *self, Display *dpy) +{ + // TODO: We're supposed to lock the server. + // TODO: We're supposed to trap X errors. + char *selection = xstrdup_printf ("_XSETTINGS_S%d", DefaultScreen (dpy)); + Window owner + = XGetSelectionOwner (dpy, XInternAtom (dpy, selection, True)); + free (selection); + if (!owner) + return; + + Atom actual_type = None; + int actual_format = 0; + unsigned long nitems = 0, bytes_after = 0; + unsigned char *buffer = NULL; + Atom xsettings = XInternAtom (dpy, "_XSETTINGS_SETTINGS", True); + int status = XGetWindowProperty (dpy, + owner, + xsettings, + 0L, + LONG_MAX, + False, + xsettings, + &actual_type, + &actual_format, + &nitems, + &bytes_after, + &buffer); + if (status != Success || !buffer) + return; + + if (actual_type != xsettings + || actual_format != 8 + || nitems < 12) + goto fail; + + const struct peeker *peeker = NULL; + if (buffer[0] == LSBFirst) + peeker = &peeker_le; + else if (buffer[0] == MSBFirst) + peeker = &peeker_be; + else + goto fail; + + // We're ignoring the serial for now. + uint32_t n_settings = peeker->u32 (buffer + 8); + size_t offset = 12; + struct str name = str_make (); + struct xdg_xsettings_setting *setting = xcalloc (1, sizeof *setting); + while (n_settings--) + { + if (nitems < offset + 4) + goto fail_item; + + setting->type = buffer[offset]; + uint16_t name_len = peeker->u16 (buffer + offset + 2); + offset += 4; + if (nitems < offset + name_len) + goto fail_item; + + str_append_data (&name, buffer + offset, name_len); + offset += ((name_len + 3) & ~3); + if (nitems < offset + 4) + goto fail_item; + + setting->serial = peeker->u32 (buffer + offset); + offset += 4; + switch (setting->type) + { + case XDG_XSETTINGS_INTEGER: + if (nitems < offset + 4) + goto fail_item; + + setting->integer = (int32_t) peeker->u32 (buffer + offset); + offset += 4; + break; + case XDG_XSETTINGS_STRING: + { + if (nitems < offset + 4) + goto fail_item; + + uint32_t value_len = peeker->u32 (buffer + offset); + offset += 4; + if (nitems < offset + value_len) + goto fail_item; + + setting->string = str_make (); + str_append_data (&setting->string, buffer + offset, value_len); + offset += ((value_len + 3) & ~3); + break; + } + case XDG_XSETTINGS_COLOR: + if (nitems < offset + 8) + goto fail_item; + + setting->color.red = peeker->u16 (buffer + offset); + setting->color.green = peeker->u16 (buffer + offset + 2); + setting->color.blue = peeker->u16 (buffer + offset + 4); + setting->color.alpha = peeker->u16 (buffer + offset + 6); + offset += 8; + break; + default: + goto fail_item; + } + + // TODO(p): Change detection, by comparing existence and serials. + str_map_set (&self->settings, name.str, setting); + setting = xcalloc (1, sizeof *setting); + str_reset (&name); + } +fail_item: + xdg_xsettings_setting_destroy (setting); + str_free (&name); +fail: + XFree (buffer); +} + +#endif // LIBERTY_XDG_WANT_X11 + +// --- Desktop file parser ----------------------------------------------------- + +// Useful for parsing desktop-file-spec, icon-theme-spec, trash-spec, +// mime-apps-spec. This code is not designed for making changes to the files. + +struct desktop_file +{ + struct str_map groups; ///< Group name → Key → Value +}; + +static void +desktop_file_free_group (void *value) +{ + str_map_free (value); + free (value); +} + +static void +desktop_file_free (struct desktop_file *self) +{ + str_map_free (&self->groups); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static void +desktop_file_parse_line (struct desktop_file *self, + char **group_name, const char *line, const char *end) +{ + struct str_map *group = NULL; + if (*group_name) + group = str_map_find (&self->groups, *group_name); + + if (*line == '[') + { + bool ok = *--end == ']'; + for (const char *p = ++line; ok && p != end; p++) + ok = (unsigned char) *p >= 32 && (unsigned char) *p <= 127 + && *p != '[' && *p != ']'; + if (!ok) + { + cstr_set (group_name, NULL); + print_debug ("invalid desktop file group header"); + return; + } + + cstr_set (group_name, xstrndup (line, end - line)); + if (str_map_find (&self->groups, *group_name)) + { + print_debug ("duplicate desktop file group: %s", *group_name); + return; + } + + group = xcalloc (1, sizeof *group); + *group = str_map_make (free); + str_map_set (&self->groups, *group_name, group); + return; + } + if (!group) + { + print_debug ("unexpected desktop file entry outside of a group"); + return; + } + + const char *key_end = line; + while (key_end != end && (isalnum_ascii (*key_end) || *key_end == '-')) + key_end++; + + // We could validate these further, but we just search in them anyway. + if (key_end != end && *key_end == '[') + { + while (++key_end != end && *key_end != ']') + ; + if (key_end != end && *key_end == ']') + key_end++; + } + + const char *value = key_end; + while (value != end && *value == ' ') + value++; + if (value == end || *value++ != '=') + { + print_debug ("invalid desktop file entry"); + return; + } + while (value != end && *value == ' ') + value++; + + char *key = xstrndup (line, key_end - line); + if (str_map_find (group, key)) + print_debug ("duplicate desktop file entry for: %s", key); + else + str_map_set (group, key, xstrndup (value, end - value)); + free (key); +} + +static struct desktop_file +desktop_file_make (const char *data, size_t len) +{ + struct desktop_file self = (struct desktop_file) + { .groups = str_map_make (desktop_file_free_group) }; + + char *group_name = NULL; + const char *p = data, *data_end = p + len; + while (p != data_end) + { + const char *line = p, *line_end = line; + while (line_end != data_end && *line_end != '\n') + line_end++; + if ((p = line_end) != data_end && *p == '\n') + p++; + + if (line != line_end && *line != '#') + desktop_file_parse_line (&self, &group_name, line, line_end); + } + free (group_name); + return self; +} + +static const char * +desktop_file_get (struct desktop_file *self, const char *group, const char *key) +{ + // TODO(p): Ideally, also implement localised keys. + struct str_map *group_map = str_map_find (&self->groups, group); + if (!group_map) + return NULL; + + return str_map_find (group_map, key); +} + +static struct strv +desktop_file_unescape (const char *value, bool is_list) +{ + struct strv result = strv_make (); + struct str s = str_make (); + + // XXX: The unescaping behaviour is underspecified. + // It might make sense to warn about unrecognised escape sequences. + bool escape = false; + for (const char *p = value; *p; p++) + { + if (escape) + { + switch (*p) + { + break; case 's': str_append_c (&s, ' '); + break; case 'n': str_append_c (&s, '\n'); + break; case 't': str_append_c (&s, '\t'); + break; case 'r': str_append_c (&s, '\r'); + break; default: str_append_c (&s, *p); + } + } + else if (*p == '\\' && p[1]) + escape = true; + else if (*p == ';' && is_list) + { + strv_append_owned (&result, str_steal (&s)); + s = str_make (); + } + else + str_append_c (&s, *p); + } + + if (!is_list || s.len != 0) + strv_append_owned (&result, str_steal (&s)); + else + str_free (&s); + return result; +} + +static char * +desktop_file_get_string (struct desktop_file *self, + const char *group, const char *key) +{ + const char *value = desktop_file_get (self, group, key); + if (!value) + return NULL; + + struct strv values = desktop_file_unescape (value, false /* is_list */); + char *unescaped = strv_steal (&values, 0); + strv_free (&values); + return unescaped; +} + +static struct strv +desktop_file_get_stringv (struct desktop_file *self, + const char *group, const char *key) +{ + const char *value = desktop_file_get (self, group, key); + if (!value) + return strv_make (); + + return desktop_file_unescape (value, true /* is_list */); +} + +static bool +desktop_file_get_bool (struct desktop_file *self, + const char *group, const char *key) +{ + const char *value = desktop_file_get (self, group, key); + if (!value) + return false; + + // Let's be compatible with pre-1.0 files when it costs us so little. + if (!strcmp (value, "true") + || !strcmp (value, "1")) + return true; + if (!strcmp (value, "false") + || !strcmp (value, "0")) + return false; + + print_debug ("invalid desktop file boolean for '%s': %s", key, value); + return false; +} + +// Nothing uses the "numeric" type. +// "icon-theme-spec" uses "integer" and doesn't say what it is. +static long +desktop_file_get_integer (struct desktop_file *self, + const char *group, const char *key) +{ + const char *value = desktop_file_get (self, group, key); + if (!value) + return 0; + + char *end = NULL; + long parsed = (errno = 0, strtol (value, &end, 10)); + if (errno != 0 || *end) + print_debug ("invalid desktop file integer for '%s': %s", key, value); + return parsed; +} + +// --- Icon themes ------------------------------------------------------------- + +// This implements part of the Icon Theme Specification. + +struct icon_theme_icon +{ + uint32_t width; ///< Width of argb in pixels + uint32_t height; ///< Height of argb in pixels + uint32_t argb[]; ///< ARGB32 data, unassociated alpha +}; + +static void +icon_theme_open_on_error (png_structp pngp, const char *error) +{ + print_debug ("%s: %s", (const char *) png_get_error_ptr (pngp), error); + png_longjmp (pngp, 1); +} + +static void +icon_theme_open_on_warning (png_structp pngp, const char *warning) +{ + (void) pngp; + (void) warning; + // Fuck your "gamma value does not match libpng estimate". +} + +// For simplicity, only support PNG icons, using the most popular library. +static struct icon_theme_icon * +icon_theme_open (const char *path) +{ + volatile png_bytep buffer = NULL; + volatile png_bytepp row_pointers = NULL; + volatile struct icon_theme_icon *result = NULL; + FILE *fp = fopen (path, "rb"); + if (!fp) + { + if (errno != ENOENT) + print_debug ("%s: %s", path, strerror (errno)); + return NULL; + } + + // The simplified and high-level APIs aren't powerful enough. + png_structp pngp = png_create_read_struct (PNG_LIBPNG_VER_STRING, + (png_voidp) path, icon_theme_open_on_error, icon_theme_open_on_warning); + png_infop infop = png_create_info_struct (pngp); + if (!infop) + { + print_debug ("%s: %s", path, strerror (errno)); + goto fail; + } + if (setjmp (png_jmpbuf (pngp))) + goto fail; + + png_init_io (pngp, fp); + png_read_info (pngp, infop); + + // Asking for at least 8-bit channels. This call is a superset of: + // - png_set_palette_to_rgb(), + // - png_set_tRNS_to_alpha(), + // - png_set_expand_gray_1_2_4_to_8(). + png_set_expand (pngp); + + // Reduce the possibilities further to RGB or RGBA... + png_set_gray_to_rgb (pngp); + + // ...and /exactly/ 8-bit channels. + // Alternatively, use png_set_expand_16() above to obtain 16-bit channels. + png_set_scale_16 (pngp); + + // PNG uses RGBA order, let's change that to ARGB (both in memory order). + // This doesn't change a row's `color_type` in png_do_read_filler(), + // and the following transformation thus ignores it. + png_set_add_alpha (pngp, 0xFFFF, PNG_FILLER_BEFORE); + png_set_swap_alpha (pngp); + + (void) png_set_interlace_handling (pngp); + + png_read_update_info (pngp, infop); + if (png_get_bit_depth (pngp, infop) != 8 + || png_get_channels (pngp, infop) != 4 + || png_get_color_type (pngp, infop) != PNG_COLOR_TYPE_RGB_ALPHA) + png_error (pngp, "result not A8R8G8B8"); + + size_t row_bytes = png_get_rowbytes (pngp, infop); + size_t height = png_get_image_height (pngp, infop); + buffer = xcalloc (row_bytes, height); + row_pointers = xcalloc (height, sizeof buffer); + for (size_t y = 0; y < height; y++) + row_pointers[y] = buffer + y * row_bytes; + + png_read_image (pngp, row_pointers); + + result = xcalloc (1, sizeof *result + row_bytes * height); + result->width = png_get_image_width (pngp, infop); + result->height = height; + + uint32_t *dst = (uint32_t *) result->argb, *src = (uint32_t *) buffer; + for (size_t pixels = result->width * result->height; pixels--; ) + *dst++ = ntohl (*src++); + +fail: + free (buffer); + free (row_pointers); + png_destroy_read_struct (&pngp, &infop, NULL); + fclose (fp); + return (struct icon_theme_icon *) result; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +struct icon_theme_find_context +{ + struct strv base; ///< Base directories + struct str_map visited; ///< Cycle prevention + + ARRAY (struct icon_theme_icon *, icons) +}; + +static void +icon_theme_find__fallback (struct icon_theme_find_context *ctx, + const char *name) +{ + for (size_t i = 0; i < ctx->base.len; i++) + { + char *path = xstrdup_printf ("%s/%s.png", ctx->base.vector[i], name); + struct icon_theme_icon *icon = icon_theme_open (path); + free (path); + if (icon) + { + ARRAY_RESERVE (ctx->icons, 1); + ctx->icons[ctx->icons_len++] = icon; + return; + } + } +} + +static struct desktop_file +icon_theme_find__index (struct icon_theme_find_context *ctx, const char *theme) +{ + struct str data = str_make (); + for (size_t i = 0; i < ctx->base.len; i++) + { + struct error *e = NULL; + char *path = xstrdup_printf ("%s/%s/index.theme", + ctx->base.vector[i], theme); + read_file (path, &data, &e); + free (path); + if (!e) + break; + + if (errno != ENOENT) + print_debug ("%s", e->message); + error_free (e); + } + + struct desktop_file index = desktop_file_make (data.str, data.len); + str_free (&data); + return index; +} + +static void +icon_theme_find__named (struct icon_theme_find_context *ctx, + const char *theme, const char *name) +{ + // Either a cycle, or a common ancestor of inherited themes, which is valid. + if (str_map_find (&ctx->visited, theme)) + return; + + str_map_set (&ctx->visited, theme, (void *) (intptr_t) 1); + struct desktop_file index = icon_theme_find__index (ctx, theme); + + char *directories = + desktop_file_get_string (&index, "Icon Theme", "Directories"); + if (!directories) + goto out; + + // NOTE: The sizes are not deduplicated, and priorities are uncertain. + struct strv dirs = strv_make (); + cstr_split (directories, ",", true, &dirs); + free (directories); + for (size_t d = 0; d < dirs.len; d++) + { + // The hicolor icon theme stuffs everything in Directories. + if (desktop_file_get (&index, dirs.vector[d], "Scale") + && desktop_file_get_integer (&index, dirs.vector[d], "Scale") != 1) + continue; + + for (size_t i = 0; i < ctx->base.len; i++) + { + char *path = xstrdup_printf ("%s/%s/%s/%s.png", + ctx->base.vector[i], theme, dirs.vector[d], name); + struct icon_theme_icon *icon = icon_theme_open (path); + free (path); + if (icon) + { + ARRAY_RESERVE (ctx->icons, 1); + ctx->icons[ctx->icons_len++] = icon; + break; + } + } + } + strv_free (&dirs); + if (ctx->icons_len) + goto out; + + char *inherits = + desktop_file_get_string (&index, "Icon Theme", "Inherits"); + if (inherits) + { + struct strv parents = strv_make (); + cstr_split (inherits, ",", true, &parents); + free (inherits); + for (size_t i = 0; i < parents.len; i++) + { + icon_theme_find__named (ctx, parents.vector[i], name); + if (ctx->icons_len) + break; + } + strv_free (&parents); + } + +out: + desktop_file_free (&index); +} + +/// Return all base directories appropriate for icon search. +static struct strv +icon_theme_get_base_directories (void) +{ + struct strv dirs = strv_make (); + struct str icons = str_make (); + (void) str_append_env_path (&icons, "HOME", false); + str_append (&icons, "/.icons"); + strv_append_owned (&dirs, str_steal (&icons)); + + // Note that we use XDG_CONFIG_HOME as well, which might be intended. + struct strv xdg = strv_make (); + get_xdg_data_dirs (&xdg); + for (size_t i = 0; i < xdg.len; i++) + strv_append_owned (&dirs, xstrdup_printf ("%s/icons", xdg.vector[i])); + strv_free (&xdg); + + strv_append (&dirs, "/usr/share/pixmaps"); + return dirs; +} + +static int +icon_theme_find__compare (const void *a, const void *b) +{ + const struct icon_theme_icon **ia = (const struct icon_theme_icon **) a; + const struct icon_theme_icon **ib = (const struct icon_theme_icon **) b; + double pa = (double) (*ia)->width * (*ia)->height; + double pb = (double) (*ib)->width * (*ib)->height; + return (pa > pb) - (pa < pb); +} + +/// Return all sizes of the named icon. When the theme name is not NULL, +/// use it as the preferred theme. Always consult fallbacks locations. +/// Ignore icon scales other than 1. +static struct icon_theme_icon ** +icon_theme_find (const char *theme, const char *name, size_t *len) +{ + struct icon_theme_find_context ctx = {}; + ctx.base = icon_theme_get_base_directories (); + ctx.visited = str_map_make (NULL); + ARRAY_INIT (ctx.icons); + + if (theme) + icon_theme_find__named (&ctx, theme, name); + if (!ctx.icons_len) + icon_theme_find__named (&ctx, "hicolor", name); + if (!ctx.icons_len) + icon_theme_find__fallback (&ctx, name); + + strv_free (&ctx.base); + str_map_free (&ctx.visited); + + ARRAY_RESERVE (ctx.icons, 1); + ctx.icons[ctx.icons_len] = NULL; + if (!ctx.icons_len) + { + free (ctx.icons); + return NULL; + } + + qsort (ctx.icons, + ctx.icons_len, sizeof *ctx.icons, icon_theme_find__compare); + *len = ctx.icons_len; + return ctx.icons; +} + +static void +icon_theme_free (struct icon_theme_icon **icons) +{ + for (struct icon_theme_icon **p = icons; *p; p++) + free (*p); + free (icons); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#ifdef LIBERTY_XDG_WANT_X11 + +static void +icon_theme_set_window_icon (Display *dpy, + Window window, const char *theme, const char *name) +{ + size_t icons_len = 0; + struct icon_theme_icon **icons = icon_theme_find (theme, name, &icons_len); + if (!icons) + return; + + size_t n = 0; + for (size_t i = 0; i < icons_len; i++) + n += 2 + icons[i]->width * icons[i]->height; + + unsigned long *data = xcalloc (n, sizeof *data), *p = data; + for (size_t i = 0; i < icons_len; i++) + { + *p++ = icons[i]->width; + *p++ = icons[i]->height; + + uint32_t *q = icons[i]->argb; + for (size_t k = icons[i]->width * icons[i]->height; k--; ) + *p++ = *q++; + } + + XChangeProperty (dpy, window, XInternAtom (dpy, "_NET_WM_ICON", False), + XA_CARDINAL, 32, PropModeReplace, (const unsigned char *) data, n); + free (data); + icon_theme_free (icons); +} + +#endif // LIBERTY_XDG_WANT_X11 diff --git a/liberty-xui.c b/liberty-xui.c index 45a6d90..761d7c6 100644 --- a/liberty-xui.c +++ b/liberty-xui.c @@ -1,7 +1,7 @@ /* * liberty-xui.c: the ultimate C unlibrary: hybrid terminal/X11 UI * - * Copyright (c) 2016 - 2023, Přemysl Eric Janouch + * Copyright (c) 2016 - 2024, Přemysl Eric Janouch * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. @@ -17,6 +17,7 @@ */ // This file includes some common stuff to build terminal/X11 applications with. +// It assumes you've already included liberty.c, and may include liberty-xdg.c. #include @@ -66,6 +67,9 @@ enum { XUI_KEYMOD_DOUBLE_CLICK = 1 << 15 }; #include #include #include + +#define LIBERTY_XDG_WANT_X11 +#include "liberty-xdg.c" #endif // LIBERTY_XUI_WANT_X11 // The application needs to implement these. @@ -690,7 +694,10 @@ struct xui XftDraw *xft_draw; ///< Xft rendering context struct x11_font *xft_fonts; ///< Font collection char *x11_selection; ///< CLIPBOARD selection + struct xdg_xsettings x11_xsettings; ///< XSETTINGS + int32_t x11_double_click_time; ///< Maximum delay for double clicks + int32_t x11_double_click_distance; ///< Maximum distance for double clicks const char *x11_fontname; ///< Fontconfig font name const char *x11_fontname_monospace; ///< Fontconfig monospace font name XRenderColor *x_fg; ///< Foreground per attribute @@ -1375,6 +1382,7 @@ x11_destroy (void) LIST_FOR_EACH (struct x11_font, font, g_xui.xft_fonts) x11_font_destroy (font); cstr_set (&g_xui.x11_selection, NULL); + xdg_xsettings_free (&g_xui.x11_xsettings); free (g_xui.x_fg); free (g_xui.x_bg); @@ -1623,9 +1631,10 @@ on_x11_input_event (XEvent *ev) unsigned int button = ev->xbutton.button; int modifiers = x11_state_to_modifiers (ev->xbutton.state); if (ev->type == ButtonPress - && ev->xbutton.time - last_press_event.xbutton.time < 500 - && abs (last_press_event.xbutton.x - x) < 5 - && abs (last_press_event.xbutton.y - y) < 5 + && ev->xbutton.time - last_press_event.xbutton.time + < (Time) g_xui.x11_double_click_time + && abs (last_press_event.xbutton.x - x) < g_xui.x11_double_click_distance + && abs (last_press_event.xbutton.y - y) < g_xui.x11_double_click_distance && last_press_event.xbutton.button == button) { modifiers |= XUI_KEYMOD_DOUBLE_CLICK; @@ -1891,6 +1900,13 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len) x11_init_attributes (app_attrs, app_attrs_len); + // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/ + // TODO: Try to use Xft/{Antialias,DPI,HintStyle,Hinting,RGBA} + // from XSETTINGS. Sadly, Gtk/FontName is in the Pango format, + // which is rather difficult to parse. + g_xui.x11_xsettings = xdg_xsettings_make (); + xdg_xsettings_update (&g_xui.x11_xsettings, g_xui.dpy); + if (!FcInit ()) print_warning ("Fontconfig initialization failed"); if (!(g_xui.xft_fonts = x11_font_open (0))) @@ -1942,6 +1958,25 @@ x11_init (struct poller *poller, struct attrs *app_attrs, size_t app_attrs_len) XSetWMName (g_xui.dpy, g_xui.x11_window, &prop); XFree (prop.value); + // It should not be outlandish to expect to find a program icon, + // although it should be possible to use a "DBus well-known name". + const char *icon_theme_name = NULL; + const struct xdg_xsettings_setting *setting = + str_map_find (&g_xui.x11_xsettings.settings, "Net/IconThemeName"); + if (setting != NULL && setting->type == XDG_XSETTINGS_STRING) + icon_theme_name = setting->string.str; + icon_theme_set_window_icon (g_xui.dpy, + g_xui.x11_window, icon_theme_name, name); + + if ((setting = str_map_find (&g_xui.x11_xsettings.settings, + "Net/DoubleClickTime")) + && setting->type == XDG_XSETTINGS_INTEGER && setting->integer >= 0) + g_xui.x11_double_click_time = setting->integer; + if ((setting = str_map_find (&g_xui.x11_xsettings.settings, + "Net/DoubleClickDistance")) + && setting->type == XDG_XSETTINGS_INTEGER && setting->integer >= 0) + g_xui.x11_double_click_distance = setting->integer; + // TODO: It is possible to do, e.g., on-the-spot. XIMStyle im_style = XIMPreeditNothing | XIMStatusNothing; XIMStyles *im_styles = NULL; @@ -2147,11 +2182,10 @@ xui_preinit (void) // Presumably, although not necessarily; unsure if queryable at all. g_xui.focused = true; - // TODO: Try to use Gtk/FontName from the _XSETTINGS_S%d selection, - // as well as Net/DoubleClick*. See the XSETTINGS proposal for details. - // https://www.freedesktop.org/wiki/Specifications/XSettingsRegistry/ - // Note that this needs an X11 connection already. #ifdef LIBERTY_XUI_WANT_X11 + // Note that XSETTINGS overrides some values in the init. + g_xui.x11_double_click_time = 500; + g_xui.x11_double_click_distance = 5; g_xui.x11_fontname = "sans\\-serif-11"; g_xui.x11_fontname_monospace = "monospace-11"; #endif // LIBERTY_XUI_WANT_X11 -- cgit v1.2.3-70-g09d2