diff --git a/.valgrind.suppressions b/.valgrind.suppressions index de42697..2b694ff 100644 --- a/.valgrind.suppressions +++ b/.valgrind.suppressions @@ -8,3 +8,32 @@ ... fun:main } + +# librsvg leaks some memory, when an invalid svg file is read +# TODO: find the memory leak and fix it upstream +{ + invalid_svgs1 + Memcheck:Leak + ... + fun:gdk_pixbuf__svg_image_load_increment + ... + fun:get_pixbuf_from_file +} + +{ + invalid_svgs2 + Memcheck:Leak + ... + fun:gdk_pixbuf__svg_image_begin_load + ... + fun:get_pixbuf_from_file +} + +{ + invalid_svgs3 + Memcheck:Leak + ... + fun:rsvg_handle_write + ... + fun:get_pixbuf_from_file +} diff --git a/Makefile b/Makefile index 034cf31..3edd1e7 100644 --- a/Makefile +++ b/Makefile @@ -80,6 +80,7 @@ test-valgrind: test/test --leak-check=full \ --show-leak-kinds=definite \ --errors-for-leak-kinds=definite \ + --num-callers=40 \ --error-exitcode=123 \ ./test diff --git a/config.h b/config.h index fc6a42b..8ffa7c9 100644 --- a/config.h +++ b/config.h @@ -16,7 +16,15 @@ settings_t defaults = { .icons = { "dialog-information", "dialog-information", "dialog-warning" }, /* low, normal, critical */ .transparency = 0, /* transparency */ -.geom = "0x0", /* geometry */ +.geometry = { .x = 0, /* geometry */ + .y = 0, + .w = 0, + .h = 0, + .negative_x = 0, + .negative_y = 0, + .negative_width = 0, + .width_set = 0 + }, .title = "Dunst", /* the title of dunst notification windows */ .class = "Dunst", /* the class of dunst notification windows */ .shrink = false, /* shrinking */ @@ -37,7 +45,7 @@ settings_t defaults = { .separator_height = 2, /* height of the separator line between two notifications */ .padding = 0, .h_padding = 0, /* horizontal padding */ -.sep_color = AUTO, /* AUTO, FOREGROUND, FRAME, CUSTOM */ +.sep_color = SEP_AUTO, /* SEP_AUTO, SEP_FOREGROUND, SEP_FRAME, SEP_CUSTOM */ .sep_custom_color_str = NULL,/* custom color if sep_color is set to CUSTOM */ .frame_width = 0, diff --git a/src/draw.c b/src/draw.c new file mode 100644 index 0000000..f6487ae --- /dev/null +++ b/src/draw.c @@ -0,0 +1,586 @@ +#include "draw.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dunst.h" +#include "icon.h" +#include "markup.h" +#include "notification.h" +#include "log.h" +#include "queues.h" +#include "x11/x.h" + +typedef struct { + PangoLayout *l; + color_t fg; + color_t bg; + color_t frame; + char *text; + PangoAttrList *attr; + cairo_surface_t *icon; + notification *n; +} colored_layout; + +window_x11 *win; + +PangoFontDescription *pango_fdesc; + +void draw_setup(void) +{ + x_setup(); + + win = x_win_create(); + pango_fdesc = pango_font_description_from_string(settings.font); +} + +static color_t color_hex_to_double(int hexValue) +{ + color_t color; + color.r = ((hexValue >> 16) & 0xFF) / 255.0; + color.g = ((hexValue >> 8) & 0xFF) / 255.0; + color.b = ((hexValue) & 0xFF) / 255.0; + + return color; +} + +static color_t string_to_color(const char *str) +{ + char *end; + long int val = strtol(str+1, &end, 16); + if (*end != '\0' && *(end+1) != '\0') { + LOG_W("Invalid color string: '%s'", str); + } + + return color_hex_to_double(val); +} + +static double color_apply_delta(double base, double delta) +{ + base += delta; + if (base > 1) + base = 1; + if (base < 0) + base = 0; + + return base; +} + +static color_t calculate_foreground_color(color_t bg) +{ + double c_delta = 0.1; + color_t color = bg; + + /* do we need to darken or brighten the colors? */ + bool darken = (bg.r + bg.g + bg.b) / 3 > 0.5; + + int signedness = darken ? -1 : 1; + + color.r = color_apply_delta(color.r, c_delta * signedness); + color.g = color_apply_delta(color.g, c_delta * signedness); + color.b = color_apply_delta(color.b, c_delta * signedness); + + return color; +} + +static color_t layout_get_sepcolor(colored_layout *cl, colored_layout *cl_next) +{ + switch (settings.sep_color) { + case SEP_FRAME: + if (cl_next->n->urgency > cl->n->urgency) + return cl_next->frame; + else + return cl->frame; + case SEP_CUSTOM: + return string_to_color(settings.sep_custom_color_str); + case SEP_FOREGROUND: + return cl->fg; + case SEP_AUTO: + return calculate_foreground_color(cl->bg); + default: + LOG_E("Invalid %s enum value in %s:%d", "sep_color", __FILE__, __LINE__); + break; + } +} + +static void layout_setup_pango(PangoLayout *layout, int width) +{ + pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); + pango_layout_set_width(layout, width * PANGO_SCALE); + pango_layout_set_font_description(layout, pango_fdesc); + pango_layout_set_spacing(layout, settings.line_height * PANGO_SCALE); + + PangoAlignment align; + switch (settings.align) { + case left: + default: + align = PANGO_ALIGN_LEFT; + break; + case center: + align = PANGO_ALIGN_CENTER; + break; + case right: + align = PANGO_ALIGN_RIGHT; + break; + } + pango_layout_set_alignment(layout, align); + +} + +static void free_colored_layout(void *data) +{ + colored_layout *cl = data; + g_object_unref(cl->l); + pango_attr_list_unref(cl->attr); + g_free(cl->text); + if (cl->icon) cairo_surface_destroy(cl->icon); + g_free(cl); +} + +static bool have_dynamic_width(void) +{ + return (settings.geometry.width_set && settings.geometry.w == 0); +} + +static struct dimensions calculate_dimensions(GSList *layouts) +{ + struct dimensions dim = { 0 }; + + screen_info *scr = get_active_screen(); + if (have_dynamic_width()) { + /* dynamic width */ + dim.w = 0; + } else if (settings.geometry.width_set) { + /* fixed width */ + if (settings.geometry.negative_width) { + dim.w = scr->w - settings.geometry.w; + } else { + dim.w = settings.geometry.w; + } + } else { + /* across the screen */ + dim.w = scr->w; + } + + dim.h += 2 * settings.frame_width; + dim.h += (g_slist_length(layouts) - 1) * settings.separator_height; + + int text_width = 0, total_width = 0; + for (GSList *iter = layouts; iter; iter = iter->next) { + colored_layout *cl = iter->data; + int w=0,h=0; + pango_layout_get_pixel_size(cl->l, &w, &h); + if (cl->icon) { + h = MAX(cairo_image_surface_get_height(cl->icon), h); + w += cairo_image_surface_get_width(cl->icon) + settings.h_padding; + } + h = MAX(settings.notification_height, h + settings.padding * 2); + dim.h += h; + text_width = MAX(w, text_width); + + if (have_dynamic_width() || settings.shrink) { + /* dynamic width */ + total_width = MAX(text_width + 2 * settings.h_padding, total_width); + + /* subtract height from the unwrapped text */ + dim.h -= h; + + if (total_width > scr->w) { + /* set width to screen width */ + dim.w = scr->w - settings.geometry.x * 2; + } else if (have_dynamic_width() || (total_width < settings.geometry.w && settings.shrink)) { + /* set width to text width */ + dim.w = total_width + 2 * settings.frame_width; + } + + /* re-setup the layout */ + w = dim.w; + w -= 2 * settings.h_padding; + w -= 2 * settings.frame_width; + if (cl->icon) w -= cairo_image_surface_get_width(cl->icon) + settings.h_padding; + layout_setup_pango(cl->l, w); + + /* re-read information */ + pango_layout_get_pixel_size(cl->l, &w, &h); + if (cl->icon) { + h = MAX(cairo_image_surface_get_height(cl->icon), h); + w += cairo_image_surface_get_width(cl->icon) + settings.h_padding; + } + h = MAX(settings.notification_height, h + settings.padding * 2); + dim.h += h; + text_width = MAX(w, text_width); + } + } + + if (dim.w <= 0) { + dim.w = text_width + 2 * settings.h_padding; + dim.w += 2 * settings.frame_width; + } + + return dim; +} + +static PangoLayout *layout_create(cairo_t *c) +{ + screen_info *screen = get_active_screen(); + + PangoContext *context = pango_cairo_create_context(c); + pango_cairo_context_set_resolution(context, get_dpi_for_screen(screen)); + + PangoLayout *layout = pango_layout_new(context); + + g_object_unref(context); + + return layout; +} + +static colored_layout *layout_init_shared(cairo_t *c, notification *n) +{ + colored_layout *cl = g_malloc(sizeof(colored_layout)); + cl->l = layout_create(c); + + if (!settings.word_wrap) { + PangoEllipsizeMode ellipsize; + switch (settings.ellipsize) { + case start: + ellipsize = PANGO_ELLIPSIZE_START; + break; + case middle: + ellipsize = PANGO_ELLIPSIZE_MIDDLE; + break; + case end: + ellipsize = PANGO_ELLIPSIZE_END; + break; + default: + LOG_E("Invalid %s enum value in %s:%d", "ellipsize", __FILE__, __LINE__); + break; + } + pango_layout_set_ellipsize(cl->l, ellipsize); + } + + if (settings.icon_position != icons_off) { + cl->icon = icon_get_for_notification(n); + } else { + cl->icon = NULL; + } + + if (cl->icon && cairo_surface_status(cl->icon) != CAIRO_STATUS_SUCCESS) { + cairo_surface_destroy(cl->icon); + cl->icon = NULL; + } + + cl->fg = string_to_color(n->colors[ColFG]); + cl->bg = string_to_color(n->colors[ColBG]); + cl->frame = string_to_color(n->colors[ColFrame]); + + cl->n = n; + + struct dimensions dim = calculate_dimensions(NULL); + int width = dim.w; + + if (have_dynamic_width()) { + layout_setup_pango(cl->l, -1); + } else { + width -= 2 * settings.h_padding; + width -= 2 * settings.frame_width; + if (cl->icon) width -= cairo_image_surface_get_width(cl->icon) + settings.h_padding; + layout_setup_pango(cl->l, width); + } + + return cl; +} + +static colored_layout *layout_derive_xmore(cairo_t *c, notification *n, int qlen) +{ + colored_layout *cl = layout_init_shared(c, n); + cl->text = g_strdup_printf("(%d more)", qlen); + cl->attr = NULL; + pango_layout_set_text(cl->l, cl->text, -1); + return cl; +} + +static colored_layout *layout_from_notification(cairo_t *c, notification *n) +{ + + colored_layout *cl = layout_init_shared(c, n); + + /* markup */ + GError *err = NULL; + pango_parse_markup(n->text_to_render, -1, 0, &(cl->attr), &(cl->text), NULL, &err); + + if (!err) { + pango_layout_set_text(cl->l, cl->text, -1); + pango_layout_set_attributes(cl->l, cl->attr); + } else { + /* remove markup and display plain message instead */ + n->text_to_render = markup_strip(n->text_to_render); + cl->text = NULL; + cl->attr = NULL; + pango_layout_set_text(cl->l, n->text_to_render, -1); + if (n->first_render) { + LOG_W("Unable to parse markup: %s", err->message); + } + g_error_free(err); + } + + + pango_layout_get_pixel_size(cl->l, NULL, &(n->displayed_height)); + if (cl->icon) n->displayed_height = MAX(cairo_image_surface_get_height(cl->icon), n->displayed_height); + n->displayed_height = MAX(settings.notification_height, n->displayed_height + settings.padding * 2); + + n->first_render = false; + return cl; +} + +static GSList *create_layouts(cairo_t *c) +{ + GSList *layouts = NULL; + + int qlen = queues_length_waiting(); + bool xmore_is_needed = qlen > 0 && settings.indicate_hidden; + + notification *last = NULL; + for (const GList *iter = queues_get_displayed(); + iter; iter = iter->next) + { + notification *n = iter->data; + last = n; + + notification_update_text_to_render(n); + + if (!iter->next && xmore_is_needed && settings.geometry.h == 1) { + char *new_ttr = g_strdup_printf("%s (%d more)", n->text_to_render, qlen); + g_free(n->text_to_render); + n->text_to_render = new_ttr; + } + layouts = g_slist_append(layouts, + layout_from_notification(c, n)); + } + + if (xmore_is_needed && settings.geometry.h != 1) { + /* append xmore message as new message */ + layouts = g_slist_append(layouts, + layout_derive_xmore(c, last, qlen)); + } + + return layouts; +} + +static void free_layouts(GSList *layouts) +{ + g_slist_free_full(layouts, free_colored_layout); +} + +static int layout_get_height(colored_layout *cl) +{ + int h; + int h_icon = 0; + pango_layout_get_pixel_size(cl->l, NULL, &h); + if (cl->icon) + h_icon = cairo_image_surface_get_height(cl->icon); + + return MAX(h, h_icon); +} + +static cairo_surface_t *render_background(cairo_surface_t *srf, + colored_layout *cl, + colored_layout *cl_next, + int y, + int width, + int height, + bool first, + bool last, + int *ret_width) +{ + int x = 0; + + cairo_t *c = cairo_create(srf); + + if (first) height += settings.frame_width; + if (last) height += settings.frame_width; + else height += settings.separator_height; + + cairo_set_source_rgb(c, cl->frame.r, cl->frame.g, cl->frame.b); + cairo_rectangle(c, x, y, width, height); + cairo_fill(c); + + /* adding frame */ + x += settings.frame_width; + if (first) { + y += settings.frame_width; + height -= settings.frame_width; + } + + width -= 2 * settings.frame_width; + + if (last) + height -= settings.frame_width; + else + height -= settings.separator_height; + + cairo_set_source_rgb(c, cl->bg.r, cl->bg.g, cl->bg.b); + cairo_rectangle(c, x, y, width, height); + cairo_fill(c); + + if ( settings.sep_color != SEP_FRAME + && settings.separator_height > 0 + && !last) { + color_t sep_color = layout_get_sepcolor(cl, cl_next); + cairo_set_source_rgb(c, sep_color.r, sep_color.g, sep_color.b); + + cairo_rectangle(c, settings.frame_width, y + height, width, settings.separator_height); + + cairo_fill(c); + } + + cairo_destroy(c); + + if (ret_width) + *ret_width = width; + + return cairo_surface_create_for_rectangle(srf, x, y, width, height); +} + +static void render_content(cairo_t *c, colored_layout *cl, int width) +{ + const int h = layout_get_height(cl); + int h_text; + pango_layout_get_pixel_size(cl->l, NULL, &h_text); + + if (cl->icon && settings.icon_position == icons_left) { + cairo_move_to(c, cairo_image_surface_get_width(cl->icon) + 2 * settings.h_padding, + settings.padding + h/2 - h_text/2); + } else if (cl->icon && settings.icon_position == icons_right) { + cairo_move_to(c, settings.h_padding, settings.padding + h/2 - h_text/2); + } else { + cairo_move_to(c, settings.h_padding, settings.padding); + } + + cairo_set_source_rgb(c, cl->fg.r, cl->fg.g, cl->fg.b); + pango_cairo_update_layout(c, cl->l); + pango_cairo_show_layout(c, cl->l); + + + if (cl->icon) { + unsigned int image_width = cairo_image_surface_get_width(cl->icon), + image_height = cairo_image_surface_get_height(cl->icon), + image_x, + image_y = settings.padding + h/2 - image_height/2; + + if (settings.icon_position == icons_left) { + image_x = settings.h_padding; + } else if (settings.icon_position == icons_right){ + image_x = width - settings.h_padding - image_width; + } else { + LOG_E("Tried to draw icon but icon position is not valid. %s:%d", __FILE__, __LINE__); + } + + cairo_set_source_surface(c, cl->icon, image_x, image_y); + cairo_rectangle(c, image_x, image_y, image_width, image_height); + cairo_fill(c); + } + +} + +static struct dimensions layout_render(cairo_surface_t *srf, + colored_layout *cl, + colored_layout *cl_next, + struct dimensions dim, + bool first, + bool last) +{ + const int cl_h = layout_get_height(cl); + + int h_text = 0; + pango_layout_get_pixel_size(cl->l, NULL, &h_text); + + int bg_width = 0; + int bg_height = MAX(settings.notification_height, (2 * settings.padding) + cl_h); + + cairo_surface_t *content = render_background(srf, cl, cl_next, dim.y, dim.w, bg_height, first, last, &bg_width); + cairo_t *c = cairo_create(content); + + render_content(c, cl, bg_width); + + /* adding frame */ + if (first) + dim.y += settings.frame_width; + + if (!last) + dim.y += settings.separator_height; + + + if (settings.notification_height <= (2 * settings.padding) + cl_h) + dim.y += cl_h + 2 * settings.padding; + else + dim.y += settings.notification_height; + + cairo_destroy(c); + cairo_surface_destroy(content); + return dim; +} + +/** + * Calculates the position the window should be placed at given its width and + * height and stores them in \p ret_x and \p ret_y. + */ +static void calc_window_pos(int width, int height, int *ret_x, int *ret_y) +{ + screen_info *scr = get_active_screen(); + + if (ret_x) { + if (settings.geometry.negative_x) { + *ret_x = (scr->x + (scr->w - width)) + settings.geometry.x; + } else { + *ret_x = scr->x + settings.geometry.x; + } + } + + if (ret_y) { + if (settings.geometry.negative_y) { + *ret_y = scr->y + (scr->h + settings.geometry.y) - height; + } else { + *ret_y = scr->y + settings.geometry.y; + } + } +} + +void draw(void) +{ + + GSList *layouts = create_layouts(x_win_get_context(win)); + + struct dimensions dim = calculate_dimensions(layouts); + + cairo_surface_t *image_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, dim.w, dim.h); + + bool first = true; + for (GSList *iter = layouts; iter; iter = iter->next) { + if (iter->next) + dim = layout_render(image_surface, iter->data, iter->next->data, dim, first, iter->next == NULL); + else + dim = layout_render(image_surface, iter->data, NULL, dim, first, iter->next == NULL); + + first = false; + } + + calc_window_pos(dim.w, dim.h, &dim.x, &dim.y); + x_display_surface(image_surface, win, &dim); + + cairo_surface_destroy(image_surface); + free_layouts(layouts); +} + +void draw_deinit(void) +{ + x_win_destroy(win); + x_free(); +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/src/draw.h b/src/draw.h new file mode 100644 index 0000000..3a3ea88 --- /dev/null +++ b/src/draw.h @@ -0,0 +1,14 @@ +#ifndef DUNST_DRAW_H +#define DUNST_DRAW_H + +#include "src/x11/x.h" +extern window_x11 *win; // Temporary + +void draw_setup(void); + +void draw(void); + +void draw_deinit(void); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/src/dunst.c b/src/dunst.c index 2bf6899..8b6ae46 100644 --- a/src/dunst.c +++ b/src/dunst.c @@ -1,7 +1,5 @@ /* copyright 2012 - 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */ -#define XLIB_ILLEGAL_ACCESS - #include "dunst.h" #include @@ -13,6 +11,7 @@ #include #include "dbus.h" +#include "draw.h" #include "log.h" #include "menu.h" #include "notification.h" @@ -27,12 +26,6 @@ #define VERSION "version info needed" #endif -typedef struct _x11_source { - GSource source; - Display *dpy; - Window w; -} x11_source_t; - /* index of colors fit to urgency level */ GMainLoop *mainloop = NULL; @@ -58,19 +51,19 @@ static gboolean run(void *data) static gint64 next_timeout = 0; - if (!xctx.visible && queues_length_displayed() > 0) { - x_win_show(); + if (!x_win_visible(win) && queues_length_displayed() > 0) { + x_win_show(win); } - if (xctx.visible && queues_length_displayed() == 0) { - x_win_hide(); + if (x_win_visible(win) && queues_length_displayed() == 0) { + x_win_hide(win); } - if (xctx.visible) { - x_win_draw(); + if (x_win_visible(win)) { + draw(); } - if (xctx.visible) { + if (x_win_visible(win)) { gint64 now = time_monotonic_now(); gint64 sleep = queues_get_next_datachange(now); gint64 timeout_at = now + sleep; @@ -120,7 +113,7 @@ static void teardown(void) teardown_queues(); - x_free(); + draw_deinit(); } int dunst_main(int argc, char *argv[]) @@ -154,7 +147,17 @@ int dunst_main(int argc, char *argv[]) int owner_id = initdbus(); - x_setup(); + mainloop = g_main_loop_new(NULL, FALSE); + + draw_setup(); + + guint pause_src = g_unix_signal_add(SIGUSR1, pause_signal, NULL); + guint unpause_src = g_unix_signal_add(SIGUSR2, unpause_signal, NULL); + + /* register SIGINT/SIGTERM handler for + * graceful termination */ + guint term_src = g_unix_signal_add(SIGTERM, quit_signal, NULL); + guint int_src = g_unix_signal_add(SIGINT, quit_signal, NULL); if (settings.startup_notification) { notification *n = notification_create(); @@ -171,37 +174,6 @@ int dunst_main(int argc, char *argv[]) // we do not call wakeup now, wake_up does not work here yet } - mainloop = g_main_loop_new(NULL, FALSE); - - GPollFD dpy_pollfd = { xctx.dpy->fd, - G_IO_IN | G_IO_HUP | G_IO_ERR, 0 - }; - - GSourceFuncs x11_source_funcs = { - x_mainloop_fd_prepare, - x_mainloop_fd_check, - x_mainloop_fd_dispatch, - NULL, - NULL, - NULL - }; - - GSource *x11_source = - g_source_new(&x11_source_funcs, sizeof(x11_source_t)); - ((x11_source_t *) x11_source)->dpy = xctx.dpy; - ((x11_source_t *) x11_source)->w = xctx.win; - g_source_add_poll(x11_source, &dpy_pollfd); - - g_source_attach(x11_source, NULL); - - guint pause_src = g_unix_signal_add(SIGUSR1, pause_signal, NULL); - guint unpause_src = g_unix_signal_add(SIGUSR2, unpause_signal, NULL); - - /* register SIGINT/SIGTERM handler for - * graceful termination */ - guint term_src = g_unix_signal_add(SIGTERM, quit_signal, NULL); - guint int_src = g_unix_signal_add(SIGINT, quit_signal, NULL); - run(NULL); g_main_loop_run(mainloop); g_main_loop_unref(mainloop); @@ -212,8 +184,6 @@ int dunst_main(int argc, char *argv[]) g_source_remove(term_src); g_source_remove(int_src); - g_source_destroy(x11_source); - dbus_tear_down(owner_id); teardown(); diff --git a/src/icon.c b/src/icon.c new file mode 100644 index 0000000..164a618 --- /dev/null +++ b/src/icon.c @@ -0,0 +1,183 @@ +#include "icon.h" + +#include +#include +#include +#include + +#include "log.h" +#include "notification.h" +#include "settings.h" + +static bool is_readable_file(const char *filename) +{ + return (access(filename, R_OK) != -1); +} + +const char *get_filename_ext(const char *filename) +{ + const char *dot = strrchr(filename, '.'); + if (!dot || dot == filename) return ""; + return dot + 1; +} + +static cairo_status_t read_from_buf(void *closure, unsigned char *data, unsigned int size) +{ + GByteArray *buf = (GByteArray *)closure; + + unsigned int cpy = MIN(size, buf->len); + memcpy(data, buf->data, cpy); + g_byte_array_remove_range(buf, 0, cpy); + + return CAIRO_STATUS_SUCCESS; +} + + +cairo_surface_t *gdk_pixbuf_to_cairo_surface(GdkPixbuf *pixbuf) +{ + /* + * Export the gdk pixbuf into buffer as a png and import the png buffer + * via cairo again as a cairo_surface_t. + * It looks counterintuitive, as there is gdk_cairo_set_source_pixbuf, + * which does the job faster. But this would require gtk3 as a dependency + * for a single function call. See discussion in #334 and #376. + */ + cairo_surface_t *icon_surface = NULL; + GByteArray *buffer; + char *bufstr; + gsize buflen; + + gdk_pixbuf_save_to_buffer(pixbuf, &bufstr, &buflen, "png", NULL, NULL); + + buffer = g_byte_array_new_take((guint8*)bufstr, buflen); + icon_surface = cairo_image_surface_create_from_png_stream(read_from_buf, buffer); + + g_byte_array_free(buffer, TRUE); + + return icon_surface; +} + +static GdkPixbuf *get_pixbuf_from_file(const char *filename) +{ + GdkPixbuf *pixbuf = NULL; + if (is_readable_file(filename)) { + GError *error = NULL; + pixbuf = gdk_pixbuf_new_from_file(filename, &error); + if (!pixbuf) { + LOG_W("%s", error->message); + g_error_free(error); + } + } + return pixbuf; +} + +GdkPixbuf *get_pixbuf_from_icon(const char *iconname) +{ + if (!iconname || iconname[0] == '\0') + return NULL; + + const char *suffixes[] = { ".svg", ".png", ".xpm", NULL }; + GdkPixbuf *pixbuf = NULL; + gchar *uri_path = NULL; + + if (g_str_has_prefix(iconname, "file://")) { + uri_path = g_filename_from_uri(iconname, NULL, NULL); + if (uri_path) + iconname = uri_path; + } + + /* absolute path? */ + if (iconname[0] == '/' || iconname[0] == '~') { + pixbuf = get_pixbuf_from_file(iconname); + } + + /* search in icon_path */ + if (!pixbuf) { + char *start = settings.icon_path, + *end, *current_folder, *maybe_icon_path; + do { + end = strchr(start, ':'); + if (!end) end = strchr(settings.icon_path, '\0'); /* end = end of string */ + + current_folder = g_strndup(start, end - start); + + for (const char **suf = suffixes; *suf; suf++) { + maybe_icon_path = g_strconcat(current_folder, "/", iconname, *suf, NULL); + pixbuf = get_pixbuf_from_file(maybe_icon_path); + g_free(maybe_icon_path); + + if (pixbuf) + break; + } + + g_free(current_folder); + if (pixbuf) + break; + + start = end + 1; + } while (*(end) != '\0'); + } + if (!pixbuf) + LOG_W("No icon found for: '%s'", iconname); + + g_free(uri_path); + return pixbuf; +} + +GdkPixbuf *get_pixbuf_from_raw_image(const RawImage *raw_image) +{ + GdkPixbuf *pixbuf = NULL; + + pixbuf = gdk_pixbuf_new_from_data(raw_image->data, + GDK_COLORSPACE_RGB, + raw_image->has_alpha, + raw_image->bits_per_sample, + raw_image->width, + raw_image->height, + raw_image->rowstride, + NULL, + NULL); + + return pixbuf; +} + +cairo_surface_t *icon_get_for_notification(const notification *n) +{ + GdkPixbuf *pixbuf; + + if (n->raw_icon) + pixbuf = get_pixbuf_from_raw_image(n->raw_icon); + else if (n->icon) + pixbuf = get_pixbuf_from_icon(n->icon); + else + return NULL; + + if (!pixbuf) + return NULL; + + int w = gdk_pixbuf_get_width(pixbuf); + int h = gdk_pixbuf_get_height(pixbuf); + int larger = w > h ? w : h; + if (settings.max_icon_size && larger > settings.max_icon_size) { + GdkPixbuf *scaled; + if (w >= h) { + scaled = gdk_pixbuf_scale_simple(pixbuf, + settings.max_icon_size, + (settings.max_icon_size * h) / w, + GDK_INTERP_BILINEAR); + } else { + scaled = gdk_pixbuf_scale_simple(pixbuf, + (settings.max_icon_size * w) / h, + settings.max_icon_size, + GDK_INTERP_BILINEAR); + } + g_object_unref(pixbuf); + pixbuf = scaled; + } + + cairo_surface_t *ret = gdk_pixbuf_to_cairo_surface(pixbuf); + g_object_unref(pixbuf); + return ret; +} + +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/src/icon.h b/src/icon.h new file mode 100644 index 0000000..c314465 --- /dev/null +++ b/src/icon.h @@ -0,0 +1,34 @@ +#ifndef DUNST_ICON_H +#define DUNST_ICON_H + +#include +#include + +#include "notification.h" + +cairo_surface_t *gdk_pixbuf_to_cairo_surface(GdkPixbuf *pixbuf); + +/** Retrieve an icon by its name sent via the notification bus + * + * @param iconname A string describing a `file://` URL, an arbitary filename + * or an icon name, which then gets searched for in the + * settings.icon_path + * + * @return an instance of `GdkPixbuf` or `NULL` if not found + */ +GdkPixbuf *get_pixbuf_from_icon(const char *iconname); + +/** Convert a RawImage to a `GdkPixbuf` + */ +GdkPixbuf *get_pixbuf_from_raw_image(const RawImage *raw_image); + +/** + * Get a cairo surface with the appropriate icon for the notification, scaled + * according to the current settings + * + * @return a cairo_surface_t pointer or NULL if no icon could be retrieved. + */ +cairo_surface_t *icon_get_for_notification(const notification *n); + +#endif +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/src/notification.c b/src/notification.c index 183fc63..1cea286 100644 --- a/src/notification.c +++ b/src/notification.c @@ -32,12 +32,13 @@ static void notification_dmenu_string(notification *n); const char *enum_to_string_fullscreen(enum behavior_fullscreen in) { switch (in) { - case FS_SHOW: return "show"; - case FS_DELAY: return "delay"; - case FS_PUSHBACK: return "pushback"; - case FS_NULL: return "(null)"; - default: - LOG_E("Enum behavior_fullscreen has wrong value."); + case FS_SHOW: return "show"; + case FS_DELAY: return "delay"; + case FS_PUSHBACK: return "pushback"; + case FS_NULL: return "(null)"; + default: + LOG_E("Invalid %s enum value in %s:%d", "fullscreen", __FILE__, __LINE__); + break; } } diff --git a/src/settings.c b/src/settings.c index aa0109d..397d45b 100644 --- a/src/settings.c +++ b/src/settings.c @@ -17,6 +17,7 @@ #include "notification.h" #include "option_parser.h" #include "utils.h" +#include "x11/x.h" settings_t settings; @@ -256,11 +257,24 @@ void load_settings(char *cmdline_config_path) "Define the class of windows spawned by dunst." ); - settings.geom = option_get_string( - "global", - "geometry", "-geom/-geometry", defaults.geom, - "Geometry for the window" - ); + { + + char *c = option_get_string( + "global", + "geometry", "-geom/-geometry", NULL, + "Geometry for the window" + ); + + if (c) { + // TODO: Implement own geometry parsing to get rid of + // the include dependency on X11 + settings.geometry = x_parse_geometry(c); + g_free(c); + } else { + settings.geometry = defaults.geometry; + } + + } settings.shrink = option_get_bool( "global", @@ -363,13 +377,13 @@ void load_settings(char *cmdline_config_path) if (strlen(c) > 0) { if (strcmp(c, "auto") == 0) - settings.sep_color = AUTO; + settings.sep_color = SEP_AUTO; else if (strcmp(c, "foreground") == 0) - settings.sep_color = FOREGROUND; + settings.sep_color = SEP_FOREGROUND; else if (strcmp(c, "frame") == 0) - settings.sep_color = FRAME; + settings.sep_color = SEP_FRAME; else { - settings.sep_color = CUSTOM; + settings.sep_color = SEP_CUSTOM; settings.sep_custom_color_str = g_strdup(c); } } diff --git a/src/settings.h b/src/settings.h index 0577f42..ee7e4a1 100644 --- a/src/settings.h +++ b/src/settings.h @@ -9,10 +9,22 @@ enum alignment { left, center, right }; enum ellipsize { start, middle, end }; enum icon_position_t { icons_left, icons_right, icons_off }; -enum separator_color { FOREGROUND, AUTO, FRAME, CUSTOM }; +enum separator_color { SEP_FOREGROUND, SEP_AUTO, SEP_FRAME, SEP_CUSTOM }; enum follow_mode { FOLLOW_NONE, FOLLOW_MOUSE, FOLLOW_KEYBOARD }; enum markup_mode { MARKUP_NULL, MARKUP_NO, MARKUP_STRIP, MARKUP_FULL }; +struct geometry { + int x; + int y; + unsigned int w; + unsigned int h; + bool negative_x; + bool negative_y; + bool negative_width; + bool width_set; + +}; + typedef struct _settings { bool print_notifications; bool per_monitor_dpi; @@ -33,7 +45,7 @@ typedef struct _settings { gint64 timeouts[3]; char *icons[3]; unsigned int transparency; - char *geom; + struct geometry geometry; char *title; char *class; int shrink; diff --git a/src/x11/screen.c b/src/x11/screen.c index 2bd37b8..e18fd51 100644 --- a/src/x11/screen.c +++ b/src/x11/screen.c @@ -126,12 +126,12 @@ void randr_update(void) alloc_screen_ar(n); for (int i = 0; i < n; i++) { - screens[i].scr = i; - screens[i].dim.x = m[i].x; - screens[i].dim.y = m[i].y; - screens[i].dim.w = m[i].width; - screens[i].dim.h = m[i].height; - screens[i].dim.mmh = m[i].mheight; + screens[i].id = i; + screens[i].x = m[i].x; + screens[i].y = m[i].y; + screens[i].w = m[i].width; + screens[i].h = m[i].height; + screens[i].mmh = m[i].mheight; } XRRFreeMonitors(m); @@ -139,7 +139,7 @@ void randr_update(void) static int autodetect_dpi(screen_info *scr) { - return (double)scr->dim.h * 25.4 / (double)scr->dim.mmh; + return (double)scr->h * 25.4 / (double)scr->mmh; } void screen_check_event(XEvent event) @@ -165,11 +165,11 @@ void xinerama_update(void) alloc_screen_ar(n); for (int i = 0; i < n; i++) { - screens[i].scr = i; - screens[i].dim.x = info[i].x_org; - screens[i].dim.y = info[i].y_org; - screens[i].dim.h = info[i].height; - screens[i].dim.w = info[i].width; + screens[i].id = i; + screens[i].x = info[i].x_org; + screens[i].y = info[i].y_org; + screens[i].h = info[i].height; + screens[i].w = info[i].width; } XFree(info); } @@ -184,8 +184,8 @@ void screen_update_fallback(void) else screen = DefaultScreen(xctx.dpy); - screens[0].dim.w = DisplayWidth(xctx.dpy, screen); - screens[0].dim.h = DisplayHeight(xctx.dpy, screen); + screens[0].w = DisplayWidth(xctx.dpy, screen); + screens[0].h = DisplayHeight(xctx.dpy, screen); } /* see screen.h */ @@ -328,8 +328,8 @@ screen_info *get_active_screen(void) } for (int i = 0; i < screens_len; i++) { - if (INRECT(x, y, screens[i].dim.x, screens[i].dim.y, - screens[i].dim.w, screens[i].dim.h)) { + if (INRECT(x, y, screens[i].x, screens[i].y, + screens[i].w, screens[i].h)) { ret = i; } } diff --git a/src/x11/screen.h b/src/x11/screen.h index fcd020d..1188299 100644 --- a/src/x11/screen.h +++ b/src/x11/screen.h @@ -7,19 +7,13 @@ #define INRECT(x,y,rx,ry,rw,rh) ((x) >= (rx) && (x) < (rx)+(rw) && (y) >= (ry) && (y) < (ry)+(rh)) -typedef struct _dimension_t { +typedef struct { + int id; int x; int y; unsigned int h; unsigned int mmh; unsigned int w; - int mask; - int negative_width; -} dimension_t; - -typedef struct _screen_info { - int scr; - dimension_t dim; } screen_info; void init_screens(void); diff --git a/src/x11/x.c b/src/x11/x.c index a21840d..a559fbf 100644 --- a/src/x11/x.c +++ b/src/x11/x.c @@ -9,21 +9,16 @@ #include #include #include -#include #include #include #include -#include -#include -#include -#include -#include #include #include #include #include #include +#include "src/draw.h" #include "src/dbus.h" #include "src/dunst.h" #include "src/log.h" @@ -38,723 +33,74 @@ #define WIDTH 400 #define HEIGHT 400 +struct window_x11 { + Window xwin; + cairo_surface_t *root_surface; + cairo_t *c_ctx; + GSource *esrc; + int cur_screen; + bool visible; + struct dimensions dim; +}; + +struct x11_source { + GSource source; + window_x11 *win; +}; + xctx_t xctx; bool dunst_grab_errored = false; -typedef struct _cairo_ctx { - cairo_status_t status; - cairo_surface_t *surface; - cairo_t *context; - PangoFontDescription *desc; -} cairo_ctx_t; - -typedef struct _colored_layout { - PangoLayout *l; - color_t fg; - color_t bg; - color_t frame; - char *text; - PangoAttrList *attr; - cairo_surface_t *icon; - notification *n; -} colored_layout; - -cairo_ctx_t cairo_ctx; static bool fullscreen_last = false; +static void x_shortcut_init(keyboard_shortcut *ks); +static int x_shortcut_grab(keyboard_shortcut *ks); +static void x_shortcut_ungrab(keyboard_shortcut *ks); /* FIXME refactor setup teardown handlers into one setup and one teardown */ static void x_shortcut_setup_error_handler(void); static int x_shortcut_tear_down_error_handler(void); -static void x_win_move(int width, int height); static void setopacity(Window win, unsigned long opacity); static void x_handle_click(XEvent ev); -static void x_win_setup(void); -static color_t x_color_hex_to_double(int hexValue) +static void x_win_move(window_x11 *win, int x, int y, int width, int height) { - color_t color; - color.r = ((hexValue >> 16) & 0xFF) / 255.0; - color.g = ((hexValue >> 8) & 0xFF) / 255.0; - color.b = ((hexValue) & 0xFF) / 255.0; + /* move and resize */ + if (x != win->dim.x || y != win->dim.y) { + XMoveWindow(xctx.dpy, win->xwin, x, y); - return color; -} - -static color_t x_string_to_color_t(const char *str) -{ - char *end; - long int val = strtol(str+1, &end, 16); - if (*end != '\0' && *(end+1) != '\0') { - LOG_W("Invalid color string: '%s'", str); + win->dim.x = x; + win->dim.y = y; } - return x_color_hex_to_double(val); -} + if (width != win->dim.w || height != win->dim.h) { + XResizeWindow(xctx.dpy, win->xwin, width, height); -static double _apply_delta(double base, double delta) -{ - base += delta; - if (base > 1) - base = 1; - if (base < 0) - base = 0; - - return base; -} - -static color_t calculate_foreground_color(color_t bg) -{ - double c_delta = 0.1; - color_t color = bg; - - /* do we need to darken or brighten the colors? */ - bool darken = (bg.r + bg.g + bg.b) / 3 > 0.5; - - int signedness = darken ? -1 : 1; - - color.r = _apply_delta(color.r, c_delta * signedness); - color.g = _apply_delta(color.g, c_delta * signedness); - color.b = _apply_delta(color.b, c_delta * signedness); - - return color; -} - -static color_t x_get_separator_color(colored_layout *cl, colored_layout *cl_next) -{ - switch (settings.sep_color) { - case FRAME: - if (cl_next->n->urgency > cl->n->urgency) - return cl_next->frame; - else - return cl->frame; - case CUSTOM: - return x_string_to_color_t(settings.sep_custom_color_str); - case FOREGROUND: - return cl->fg; - case AUTO: - return calculate_foreground_color(cl->bg); - default: - LOG_E("Unknown separator color type."); + win->dim.h = height; + win->dim.w = width; } } -static void x_cairo_setup(void) +void x_display_surface(cairo_surface_t *srf, window_x11 *win, const struct dimensions *dim) { - cairo_ctx.surface = cairo_xlib_surface_create(xctx.dpy, - xctx.win, DefaultVisual(xctx.dpy, 0), WIDTH, HEIGHT); + x_win_move(win, dim->x, dim->y, dim->w, dim->h); + cairo_xlib_surface_set_size(win->root_surface, dim->w, dim->h); - cairo_ctx.context = cairo_create(cairo_ctx.surface); - - cairo_ctx.desc = pango_font_description_from_string(settings.font); -} - -static void r_setup_pango_layout(PangoLayout *layout, int width) -{ - pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); - pango_layout_set_width(layout, width * PANGO_SCALE); - pango_layout_set_font_description(layout, cairo_ctx.desc); - pango_layout_set_spacing(layout, settings.line_height * PANGO_SCALE); - - PangoAlignment align; - switch (settings.align) { - case left: - default: - align = PANGO_ALIGN_LEFT; - break; - case center: - align = PANGO_ALIGN_CENTER; - break; - case right: - align = PANGO_ALIGN_RIGHT; - break; - } - pango_layout_set_alignment(layout, align); - -} - -static void free_colored_layout(void *data) -{ - colored_layout *cl = data; - g_object_unref(cl->l); - pango_attr_list_unref(cl->attr); - g_free(cl->text); - if (cl->icon) cairo_surface_destroy(cl->icon); - g_free(cl); -} - -static bool have_dynamic_width(void) -{ - return (xctx.geometry.mask & WidthValue && xctx.geometry.w == 0); -} - -static bool does_file_exist(const char *filename) -{ - return (access(filename, F_OK) != -1); -} - -static bool is_readable_file(const char *filename) -{ - return (access(filename, R_OK) != -1); -} - -const char *get_filename_ext(const char *filename) -{ - const char *dot = strrchr(filename, '.'); - if (!dot || dot == filename) return ""; - return dot + 1; -} - -static dimension_t calculate_dimensions(GSList *layouts) -{ - dimension_t dim; - dim.w = 0; - dim.h = 0; - dim.x = 0; - dim.y = 0; - dim.mask = xctx.geometry.mask; - - screen_info *scr = get_active_screen(); - if (have_dynamic_width()) { - /* dynamic width */ - dim.w = 0; - } else if (xctx.geometry.mask & WidthValue) { - /* fixed width */ - if (xctx.geometry.negative_width) { - dim.w = scr->dim.w - xctx.geometry.w; - } else { - dim.w = xctx.geometry.w; - } - } else { - /* across the screen */ - dim.w = scr->dim.w; - } - - dim.h += 2 * settings.frame_width; - dim.h += (g_slist_length(layouts) - 1) * settings.separator_height; - - int text_width = 0, total_width = 0; - for (GSList *iter = layouts; iter; iter = iter->next) { - colored_layout *cl = iter->data; - int w=0,h=0; - pango_layout_get_pixel_size(cl->l, &w, &h); - if (cl->icon) { - h = MAX(cairo_image_surface_get_height(cl->icon), h); - w += cairo_image_surface_get_width(cl->icon) + settings.h_padding; - } - h = MAX(settings.notification_height, h + settings.padding * 2); - dim.h += h; - text_width = MAX(w, text_width); - - if (have_dynamic_width() || settings.shrink) { - /* dynamic width */ - total_width = MAX(text_width + 2 * settings.h_padding, total_width); - - /* subtract height from the unwrapped text */ - dim.h -= h; - - if (total_width > scr->dim.w) { - /* set width to screen width */ - dim.w = scr->dim.w - xctx.geometry.x * 2; - } else if (have_dynamic_width() || (total_width < xctx.geometry.w && settings.shrink)) { - /* set width to text width */ - dim.w = total_width + 2 * settings.frame_width; - } - - /* re-setup the layout */ - w = dim.w; - w -= 2 * settings.h_padding; - w -= 2 * settings.frame_width; - if (cl->icon) w -= cairo_image_surface_get_width(cl->icon) + settings.h_padding; - r_setup_pango_layout(cl->l, w); - - /* re-read information */ - pango_layout_get_pixel_size(cl->l, &w, &h); - if (cl->icon) { - h = MAX(cairo_image_surface_get_height(cl->icon), h); - w += cairo_image_surface_get_width(cl->icon) + settings.h_padding; - } - h = MAX(settings.notification_height, h + settings.padding * 2); - dim.h += h; - text_width = MAX(w, text_width); - } - } - - if (dim.w <= 0) { - dim.w = text_width + 2 * settings.h_padding; - dim.w += 2 * settings.frame_width; - } - - return dim; -} - -static cairo_status_t read_from_buf(void *closure, unsigned char *data, unsigned int size) -{ - GByteArray *buf = (GByteArray *)closure; - - unsigned int cpy = MIN(size, buf->len); - memcpy(data, buf->data, cpy); - g_byte_array_remove_range(buf, 0, cpy); - - return CAIRO_STATUS_SUCCESS; -} - - -static cairo_surface_t *gdk_pixbuf_to_cairo_surface(GdkPixbuf *pixbuf) -{ - /* - * Export the gdk pixbuf into buffer as a png and import the png buffer - * via cairo again as a cairo_surface_t. - * It looks counterintuitive, as there is gdk_cairo_set_source_pixbuf, - * which does the job faster. But this would require gtk3 as a dependency - * for a single function call. See discussion in #334 and #376. - */ - cairo_surface_t *icon_surface = NULL; - GByteArray *buffer; - char *bufstr; - gsize buflen; - - gdk_pixbuf_save_to_buffer(pixbuf, &bufstr, &buflen, "png", NULL, NULL); - - buffer = g_byte_array_new_take((guint8*)bufstr, buflen); - icon_surface = cairo_image_surface_create_from_png_stream(read_from_buf, buffer); - - g_byte_array_free(buffer, TRUE); - - return icon_surface; -} - -static GdkPixbuf *get_pixbuf_from_file(const char *icon_path) -{ - GdkPixbuf *pixbuf = NULL; - if (is_readable_file(icon_path)) { - GError *error = NULL; - pixbuf = gdk_pixbuf_new_from_file(icon_path, &error); - if (pixbuf == NULL) - g_error_free(error); - } - return pixbuf; -} - -static GdkPixbuf *get_pixbuf_from_path(char *icon_path) -{ - GdkPixbuf *pixbuf = NULL; - gchar *uri_path = NULL; - if (strlen(icon_path) > 0) { - if (g_str_has_prefix(icon_path, "file://")) { - uri_path = g_filename_from_uri(icon_path, NULL, NULL); - if (uri_path != NULL) { - icon_path = uri_path; - } - } - /* absolute path? */ - if (icon_path[0] == '/' || icon_path[0] == '~') { - pixbuf = get_pixbuf_from_file(icon_path); - } - /* search in icon_path */ - if (pixbuf == NULL) { - char *start = settings.icon_path, - *end, *current_folder, *maybe_icon_path; - do { - end = strchr(start, ':'); - if (end == NULL) end = strchr(settings.icon_path, '\0'); /* end = end of string */ - - current_folder = g_strndup(start, end - start); - /* try svg */ - maybe_icon_path = g_strconcat(current_folder, "/", icon_path, ".svg", NULL); - if (!does_file_exist(maybe_icon_path)) { - g_free(maybe_icon_path); - /* fallback to png */ - maybe_icon_path = g_strconcat(current_folder, "/", icon_path, ".png", NULL); - } - g_free(current_folder); - - pixbuf = get_pixbuf_from_file(maybe_icon_path); - g_free(maybe_icon_path); - if (pixbuf != NULL) { - return pixbuf; - } - - start = end + 1; - } while (*(end) != '\0'); - } - if (pixbuf == NULL) { - LOG_W("Could not load icon: '%s'", icon_path); - } - if (uri_path != NULL) { - g_free(uri_path); - } - } - return pixbuf; -} - -static GdkPixbuf *get_pixbuf_from_raw_image(const RawImage *raw_image) -{ - GdkPixbuf *pixbuf = NULL; - - pixbuf = gdk_pixbuf_new_from_data(raw_image->data, - GDK_COLORSPACE_RGB, - raw_image->has_alpha, - raw_image->bits_per_sample, - raw_image->width, - raw_image->height, - raw_image->rowstride, - NULL, - NULL); - - return pixbuf; -} - -static PangoLayout *create_layout(cairo_t *c) -{ - screen_info *screen = get_active_screen(); - - PangoContext *context = pango_cairo_create_context(c); - pango_cairo_context_set_resolution(context, get_dpi_for_screen(screen)); - - PangoLayout *layout = pango_layout_new(context); - - g_object_unref(context); - - return layout; -} - -static colored_layout *r_init_shared(cairo_t *c, notification *n) -{ - colored_layout *cl = g_malloc(sizeof(colored_layout)); - cl->l = create_layout(c); - - if (!settings.word_wrap) { - PangoEllipsizeMode ellipsize; - switch (settings.ellipsize) { - case start: - ellipsize = PANGO_ELLIPSIZE_START; - break; - case middle: - ellipsize = PANGO_ELLIPSIZE_MIDDLE; - break; - case end: - ellipsize = PANGO_ELLIPSIZE_END; - break; - default: - assert(false); - } - pango_layout_set_ellipsize(cl->l, ellipsize); - } - - GdkPixbuf *pixbuf = NULL; - - if (n->raw_icon && - settings.icon_position != icons_off) { - - pixbuf = get_pixbuf_from_raw_image(n->raw_icon); - - } else if (n->icon && settings.icon_position != icons_off) { - pixbuf = get_pixbuf_from_path(n->icon); - } - - if (pixbuf != NULL) { - int w = gdk_pixbuf_get_width(pixbuf); - int h = gdk_pixbuf_get_height(pixbuf); - int larger = w > h ? w : h; - if (settings.max_icon_size && larger > settings.max_icon_size) { - GdkPixbuf *scaled; - if (w >= h) { - scaled = gdk_pixbuf_scale_simple(pixbuf, - settings.max_icon_size, - (int) ((double) settings.max_icon_size / w * h), - GDK_INTERP_BILINEAR); - } else { - scaled = gdk_pixbuf_scale_simple(pixbuf, - (int) ((double) settings.max_icon_size / h * w), - settings.max_icon_size, - GDK_INTERP_BILINEAR); - } - g_object_unref(pixbuf); - pixbuf = scaled; - } - - cl->icon = gdk_pixbuf_to_cairo_surface(pixbuf); - g_object_unref(pixbuf); - } else { - cl->icon = NULL; - } - - if (cl->icon && cairo_surface_status(cl->icon) != CAIRO_STATUS_SUCCESS) { - cairo_surface_destroy(cl->icon); - cl->icon = NULL; - } - - cl->fg = x_string_to_color_t(n->colors[ColFG]); - cl->bg = x_string_to_color_t(n->colors[ColBG]); - cl->frame = x_string_to_color_t(n->colors[ColFrame]); - - cl->n = n; - - dimension_t dim = calculate_dimensions(NULL); - int width = dim.w; - - if (have_dynamic_width()) { - r_setup_pango_layout(cl->l, -1); - } else { - width -= 2 * settings.h_padding; - width -= 2 * settings.frame_width; - if (cl->icon) width -= cairo_image_surface_get_width(cl->icon) + settings.h_padding; - r_setup_pango_layout(cl->l, width); - } - - return cl; -} - -static colored_layout *r_create_layout_for_xmore(cairo_t *c, notification *n, int qlen) -{ - colored_layout *cl = r_init_shared(c, n); - cl->text = g_strdup_printf("(%d more)", qlen); - cl->attr = NULL; - pango_layout_set_text(cl->l, cl->text, -1); - return cl; -} - -static colored_layout *r_create_layout_from_notification(cairo_t *c, notification *n) -{ - - colored_layout *cl = r_init_shared(c, n); - - /* markup */ - GError *err = NULL; - pango_parse_markup(n->text_to_render, -1, 0, &(cl->attr), &(cl->text), NULL, &err); - - if (!err) { - pango_layout_set_text(cl->l, cl->text, -1); - pango_layout_set_attributes(cl->l, cl->attr); - } else { - /* remove markup and display plain message instead */ - n->text_to_render = markup_strip(n->text_to_render); - cl->text = NULL; - cl->attr = NULL; - pango_layout_set_text(cl->l, n->text_to_render, -1); - if (n->first_render) { - LOG_W("Unable to parse markup: %s", err->message); - } - g_error_free(err); - } - - - pango_layout_get_pixel_size(cl->l, NULL, &(n->displayed_height)); - if (cl->icon) n->displayed_height = MAX(cairo_image_surface_get_height(cl->icon), n->displayed_height); - n->displayed_height = MAX(settings.notification_height, n->displayed_height + settings.padding * 2); - - n->first_render = false; - return cl; -} - -static GSList *r_create_layouts(cairo_t *c) -{ - GSList *layouts = NULL; - - int qlen = queues_length_waiting(); - bool xmore_is_needed = qlen > 0 && settings.indicate_hidden; - - notification *last = NULL; - for (const GList *iter = queues_get_displayed(); - iter; iter = iter->next) - { - notification *n = iter->data; - last = n; - - notification_update_text_to_render(n); - - if (!iter->next && xmore_is_needed && xctx.geometry.h == 1) { - char *new_ttr = g_strdup_printf("%s (%d more)", n->text_to_render, qlen); - g_free(n->text_to_render); - n->text_to_render = new_ttr; - } - layouts = g_slist_append(layouts, - r_create_layout_from_notification(c, n)); - } - - if (xmore_is_needed && xctx.geometry.h != 1) { - /* append xmore message as new message */ - layouts = g_slist_append(layouts, - r_create_layout_for_xmore(c, last, qlen)); - } - - return layouts; -} - -static void r_free_layouts(GSList *layouts) -{ - g_slist_free_full(layouts, free_colored_layout); -} - -static dimension_t x_render_layout(cairo_t *c, colored_layout *cl, colored_layout *cl_next, dimension_t dim, bool first, bool last) -{ - int h; - int h_text = 0; - pango_layout_get_pixel_size(cl->l, NULL, &h); - if (cl->icon) { - h_text = h; - h = MAX(cairo_image_surface_get_height(cl->icon), h); - } - - int bg_x = 0; - int bg_y = dim.y; - int bg_width = dim.w; - int bg_height = MAX(settings.notification_height, (2 * settings.padding) + h); - double bg_half_height = settings.notification_height/2.0; - int pango_offset = (int) floor(h/2.0); - - if (first) bg_height += settings.frame_width; - if (last) bg_height += settings.frame_width; - else bg_height += settings.separator_height; - - cairo_set_source_rgb(c, cl->frame.r, cl->frame.g, cl->frame.b); - cairo_rectangle(c, bg_x, bg_y, bg_width, bg_height); - cairo_fill(c); - - /* adding frame */ - bg_x += settings.frame_width; - if (first) { - dim.y += settings.frame_width; - bg_y += settings.frame_width; - bg_height -= settings.frame_width; - if (!last) bg_height -= settings.separator_height; - } - bg_width -= 2 * settings.frame_width; - if (last) - bg_height -= settings.frame_width; - - cairo_set_source_rgb(c, cl->bg.r, cl->bg.g, cl->bg.b); - cairo_rectangle(c, bg_x, bg_y, bg_width, bg_height); - cairo_fill(c); - - bool use_padding = settings.notification_height <= (2 * settings.padding) + h; - if (use_padding) - dim.y += settings.padding; - else - dim.y += (int) (ceil(bg_half_height) - pango_offset); - - if (cl->icon && settings.icon_position == icons_left) { - cairo_move_to(c, settings.frame_width + cairo_image_surface_get_width(cl->icon) + 2 * settings.h_padding, bg_y + settings.padding + h/2 - h_text/2); - } else if (cl->icon && settings.icon_position == icons_right) { - cairo_move_to(c, settings.frame_width + settings.h_padding, bg_y + settings.padding + h/2 - h_text/2); - } else { - cairo_move_to(c, settings.frame_width + settings.h_padding, bg_y + settings.padding); - } - - cairo_set_source_rgb(c, cl->fg.r, cl->fg.g, cl->fg.b); - pango_cairo_update_layout(c, cl->l); - pango_cairo_show_layout(c, cl->l); - if (use_padding) - dim.y += h + settings.padding; - else - dim.y += (int)(floor(bg_half_height) + pango_offset); - - if (settings.separator_height > 0 && !last) { - color_t sep_color = x_get_separator_color(cl, cl_next); - cairo_set_source_rgb(c, sep_color.r, sep_color.g, sep_color.b); - - if (settings.sep_color == FRAME) - // Draw over the borders on both sides to avoid - // the wrong color in the corners. - cairo_rectangle(c, 0, dim.y, dim.w, settings.separator_height); - else - cairo_rectangle(c, settings.frame_width, dim.y + settings.frame_width, dim.w - 2 * settings.frame_width, settings.separator_height); - - cairo_fill(c); - dim.y += settings.separator_height; - } - cairo_move_to(c, settings.h_padding, dim.y); - - if (cl->icon) { - unsigned int image_width = cairo_image_surface_get_width(cl->icon), - image_height = cairo_image_surface_get_height(cl->icon), - image_x, - image_y = bg_y + settings.padding + h/2 - image_height/2; - - if (settings.icon_position == icons_left) { - image_x = settings.frame_width + settings.h_padding; - } else { - image_x = bg_width - settings.h_padding - image_width + settings.frame_width; - } - - cairo_set_source_surface(c, cl->icon, image_x, image_y); - cairo_rectangle(c, image_x, image_y, image_width, image_height); - cairo_fill(c); - } - - return dim; -} - -void x_win_draw(void) -{ - - GSList *layouts = r_create_layouts(cairo_ctx.context); - - dimension_t dim = calculate_dimensions(layouts); - int width = dim.w; - int height = dim.h; - - cairo_t *c; - cairo_surface_t *image_surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); - c = cairo_create(image_surface); - - x_win_move(width, height); - cairo_xlib_surface_set_size(cairo_ctx.surface, width, height); - - cairo_move_to(c, 0, 0); - - bool first = true; - for (GSList *iter = layouts; iter; iter = iter->next) { - if (iter->next) - dim = x_render_layout(c, iter->data, iter->next->data, dim, first, iter->next == NULL); - else - dim = x_render_layout(c, iter->data, NULL, dim, first, iter->next == NULL); - - first = false; - } - - cairo_set_source_surface(cairo_ctx.context, image_surface, 0, 0); - cairo_paint(cairo_ctx.context); - cairo_show_page(cairo_ctx.context); + cairo_set_source_surface(win->c_ctx, srf, 0, 0); + cairo_paint(win->c_ctx); + cairo_show_page(win->c_ctx); XFlush(xctx.dpy); - cairo_destroy(c); - cairo_surface_destroy(image_surface); - r_free_layouts(layouts); } -static void x_win_move(int width, int height) +bool x_win_visible(window_x11 *win) { + return win->visible; +} - int x, y; - screen_info *scr = get_active_screen(); - xctx.cur_screen = scr->scr; - /* calculate window position */ - if (xctx.geometry.mask & XNegative) { - x = (scr->dim.x + (scr->dim.w - width)) + xctx.geometry.x; - } else { - x = scr->dim.x + xctx.geometry.x; - } - - if (xctx.geometry.mask & YNegative) { - y = scr->dim.y + (scr->dim.h + xctx.geometry.y) - height; - } else { - y = scr->dim.y + xctx.geometry.y; - } - - /* move and resize */ - if (x != xctx.window_dim.x || y != xctx.window_dim.y) { - XMoveWindow(xctx.dpy, xctx.win, x, y); - } - if (width != xctx.window_dim.w || height != xctx.window_dim.h) { - XResizeWindow(xctx.dpy, xctx.win, width, height); - } - - xctx.window_dim.x = x; - xctx.window_dim.y = y; - xctx.window_dim.h = height; - xctx.window_dim.w = width; +cairo_t* x_win_get_context(window_x11 *win) +{ + return win->c_ctx; } static void setopacity(Window win, unsigned long opacity) @@ -849,7 +195,10 @@ gboolean x_mainloop_fd_check(GSource *source) */ gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer user_data) { + window_x11 *win = ((struct x11_source*) source)->win; + bool fullscreen_now; + screen_info *scr; XEvent ev; unsigned int state; while (XPending(xctx.dpy) > 0) { @@ -858,15 +207,12 @@ gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer switch (ev.type) { case Expose: - if (ev.xexpose.count == 0 && xctx.visible) { - x_win_draw(); + if (ev.xexpose.count == 0 && win->visible) { + draw(); } break; - case SelectionNotify: - if (ev.xselection.property == xctx.utf8) - break; case ButtonRelease: - if (ev.xbutton.window == xctx.win) { + if (ev.xbutton.window == win->xwin) { x_handle_click(ev); wake_up(); } @@ -911,8 +257,14 @@ gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer case FocusOut: wake_up(); break; + case CreateNotify: + if (win->visible && + ev.xcreatewindow.override_redirect == 0) + XRaiseWindow(xctx.dpy, win->xwin); + break; case PropertyNotify: fullscreen_now = have_fullscreen_window(); + scr = get_active_screen(); if (fullscreen_now != fullscreen_last) { fullscreen_last = fullscreen_now; @@ -922,9 +274,10 @@ gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer * same screen. PropertyNotify is only neccessary * to detect a focus change to another screen */ - && xctx.visible - && get_active_screen()->scr != xctx.cur_screen) { - x_win_draw(); + && win->visible + && scr->id != win->cur_screen) { + draw(); + win->cur_screen = scr->id; } break; default: @@ -932,7 +285,7 @@ gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer break; } } - return true; + return G_SOURCE_CONTINUE; } /* @@ -986,8 +339,8 @@ static void x_handle_click(XEvent ev) void x_free(void) { - cairo_surface_destroy(cairo_ctx.surface); - cairo_destroy(cairo_ctx.context); + if (xctx.screensaver_info) + XFree(xctx.screensaver_info); if (xctx.dpy) XCloseDisplay(xctx.dpy); @@ -1041,37 +394,46 @@ void x_setup(void) else xctx.colors[ColFrame][URG_CRIT] = settings.frame_color; - /* parse and set xctx.geometry and monitor position */ - if (settings.geom[0] == '-') { - xctx.geometry.negative_width = true; - settings.geom++; - } else { - xctx.geometry.negative_width = false; - } - - xctx.geometry.mask = XParseGeometry(settings.geom, - &xctx.geometry.x, &xctx.geometry.y, - &xctx.geometry.w, &xctx.geometry.h); - - /* calculate maximum notification count and push information to queue */ - if (xctx.geometry.h == 0) { - queues_displayed_limit(0); - } else if (xctx.geometry.h == 1) { - queues_displayed_limit(1); - } else if (settings.indicate_hidden) { - queues_displayed_limit(xctx.geometry.h - 1); - } else { - queues_displayed_limit(xctx.geometry.h); - } - xctx.screensaver_info = XScreenSaverAllocInfo(); init_screens(); - x_win_setup(); - x_cairo_setup(); x_shortcut_grab(&settings.history_ks); } +struct geometry x_parse_geometry(const char *geom_str) +{ + assert(geom_str); + + if (geom_str[0] == '-') { + settings.geometry.negative_width = true; + geom_str++; + } else { + settings.geometry.negative_width = false; + } + + struct geometry geometry = { 0 }; + + int mask = XParseGeometry(geom_str, + &geometry.x, &geometry.y, + &geometry.w, &geometry.h); + geometry.width_set = mask & WidthValue; + geometry.negative_x = mask & XNegative; + geometry.negative_y = mask & YNegative; + + /* calculate maximum notification count and push information to queue */ + if (geometry.h == 0) { + queues_displayed_limit(0); + } else if (geometry.h == 1) { + queues_displayed_limit(1); + } else if (settings.indicate_hidden) { + queues_displayed_limit(geometry.h - 1); + } else { + queues_displayed_limit(geometry.h); + } + + return geometry; +} + static void x_set_wm(Window win) { @@ -1124,22 +486,42 @@ static void x_set_wm(Window win) PropModeReplace, (unsigned char *) data, 1L); } +GSource* x_win_reg_source(window_x11 *win) +{ + // Static is necessary here because glib keeps the pointer and we need + // to keep the reference alive. + static GSourceFuncs xsrc_fn = { + x_mainloop_fd_prepare, + x_mainloop_fd_check, + x_mainloop_fd_dispatch, + NULL, + NULL, + NULL + }; + + struct x11_source *xsrc = (struct x11_source*) g_source_new(&xsrc_fn, + sizeof(struct x11_source)); + + xsrc->win = win; + + g_source_add_unix_fd((GSource*) xsrc, xctx.dpy->fd, G_IO_IN | G_IO_HUP | G_IO_ERR); + + g_source_attach((GSource*) xsrc, NULL); + + return (GSource*)xsrc; +} + /* * Setup the window */ -static void x_win_setup(void) +window_x11 *x_win_create(void) { + window_x11 *win = g_malloc0(sizeof(window_x11)); Window root; XSetWindowAttributes wa; - xctx.window_dim.x = 0; - xctx.window_dim.y = 0; - xctx.window_dim.w = 0; - xctx.window_dim.h = 0; - root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)); - xctx.utf8 = XInternAtom(xctx.dpy, "UTF8_STRING", false); wa.override_redirect = true; wa.background_pixmap = ParentRelative; @@ -1148,11 +530,11 @@ static void x_win_setup(void) ButtonReleaseMask | FocusChangeMask| StructureNotifyMask; screen_info *scr = get_active_screen(); - xctx.win = XCreateWindow(xctx.dpy, + win->xwin = XCreateWindow(xctx.dpy, root, - scr->dim.x, - scr->dim.y, - scr->dim.w, + scr->x, + scr->y, + scr->w, 1, 0, DefaultDepth(xctx.dpy, DefaultScreen(xctx.dpy)), @@ -1161,26 +543,48 @@ static void x_win_setup(void) CWOverrideRedirect | CWBackPixmap | CWEventMask, &wa); - x_set_wm(xctx.win); + x_set_wm(win->xwin); settings.transparency = settings.transparency > 100 ? 100 : settings.transparency; - setopacity(xctx.win, + setopacity(win->xwin, (unsigned long)((100 - settings.transparency) * (0xffffffff / 100))); + win->root_surface = cairo_xlib_surface_create(xctx.dpy, win->xwin, + DefaultVisual(xctx.dpy, 0), + WIDTH, HEIGHT); + win->c_ctx = cairo_create(win->root_surface); + + win->esrc = x_win_reg_source(win); + + long root_event_mask = SubstructureNotifyMask; if (settings.f_mode != FOLLOW_NONE) { - long root_event_mask = FocusChangeMask | PropertyChangeMask; - XSelectInput(xctx.dpy, root, root_event_mask); + root_event_mask |= FocusChangeMask | PropertyChangeMask; } + XSelectInput(xctx.dpy, root, root_event_mask); + + return win; +} + +void x_win_destroy(window_x11 *win) +{ + g_source_destroy(win->esrc); + g_source_unref(win->esrc); + + cairo_destroy(win->c_ctx); + cairo_surface_destroy(win->root_surface); + XDestroyWindow(xctx.dpy, win->xwin); + + g_free(win); } /* * Show the window and grab shortcuts. */ -void x_win_show(void) +void x_win_show(window_x11 *win) { /* window is already mapped or there's nothing to show */ - if (xctx.visible || queues_length_displayed() == 0) { + if (win->visible || queues_length_displayed() == 0) { return; } @@ -1192,9 +596,9 @@ void x_win_show(void) XGrabButton(xctx.dpy, AnyButton, AnyModifier, - xctx.win, + win->xwin, false, - BUTTONMASK, + (ButtonPressMask|ButtonReleaseMask), GrabModeAsync, GrabModeSync, None, @@ -1203,23 +607,23 @@ void x_win_show(void) LOG_W("Unable to grab mouse button(s)."); } - XMapRaised(xctx.dpy, xctx.win); - xctx.visible = true; + XMapRaised(xctx.dpy, win->xwin); + win->visible = true; } /* * Hide the window and ungrab unused keyboard_shortcuts */ -void x_win_hide(void) +void x_win_hide(window_x11 *win) { x_shortcut_ungrab(&settings.close_ks); x_shortcut_ungrab(&settings.close_all_ks); x_shortcut_ungrab(&settings.context_ks); - XUngrabButton(xctx.dpy, AnyButton, AnyModifier, xctx.win); - XUnmapWindow(xctx.dpy, xctx.win); + XUngrabButton(xctx.dpy, AnyButton, AnyModifier, win->xwin); + XUnmapWindow(xctx.dpy, win->xwin); XFlush(xctx.dpy); - xctx.visible = false; + win->visible = false; } /* @@ -1288,7 +692,7 @@ static int x_shortcut_tear_down_error_handler(void) /* * Grab the given keyboard shortcut. */ -int x_shortcut_grab(keyboard_shortcut *ks) +static int x_shortcut_grab(keyboard_shortcut *ks) { if (!ks->is_valid) return 1; @@ -1325,7 +729,7 @@ int x_shortcut_grab(keyboard_shortcut *ks) /* * Ungrab the given keyboard shortcut. */ -void x_shortcut_ungrab(keyboard_shortcut *ks) +static void x_shortcut_ungrab(keyboard_shortcut *ks) { Window root; root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy)); @@ -1338,7 +742,7 @@ void x_shortcut_ungrab(keyboard_shortcut *ks) /* * Initialize the keyboard shortcut. */ -void x_shortcut_init(keyboard_shortcut *ks) +static void x_shortcut_init(keyboard_shortcut *ks) { if (ks == NULL || ks->str == NULL) return; diff --git a/src/x11/x.h b/src/x11/x.h index 8ee757a..f32fefa 100644 --- a/src/x11/x.h +++ b/src/x11/x.h @@ -2,6 +2,9 @@ #ifndef DUNST_X_H #define DUNST_X_H +#define XLIB_ILLEGAL_ACCESS + +#include #include #include #include @@ -10,10 +13,6 @@ #include "screen.h" -#define BUTTONMASK (ButtonPressMask|ButtonReleaseMask) -#define FONT_HEIGHT_BORDER 2 -#define DEFFONT "Monospace-11" - typedef struct _keyboard_shortcut { const char *str; KeyCode code; @@ -22,17 +21,22 @@ typedef struct _keyboard_shortcut { bool is_valid; } keyboard_shortcut; +// Cyclical dependency +#include "src/settings.h" + +typedef struct window_x11 window_x11; + +struct dimensions { + int x; + int y; + int w; + int h; +}; + typedef struct _xctx { - Atom utf8; Display *dpy; - int cur_screen; - Window win; - bool visible; - dimension_t geometry; const char *colors[3][3]; XScreenSaverInfo *screensaver_info; - dimension_t window_dim; - unsigned long sep_custom_col; } xctx_t; typedef struct _color_t { @@ -44,25 +48,23 @@ typedef struct _color_t { extern xctx_t xctx; /* window */ -void x_win_draw(void); -void x_win_hide(void); -void x_win_show(void); +window_x11 *x_win_create(void); +void x_win_destroy(window_x11 *win); -/* shortcut */ -void x_shortcut_init(keyboard_shortcut *shortcut); -void x_shortcut_ungrab(keyboard_shortcut *ks); -int x_shortcut_grab(keyboard_shortcut *ks); -KeySym x_shortcut_string_to_mask(const char *str); +void x_win_show(window_x11 *win); +void x_win_hide(window_x11 *win); + +void x_display_surface(cairo_surface_t *srf, window_x11 *win, const struct dimensions *dim); + +bool x_win_visible(window_x11 *win); +cairo_t* x_win_get_context(window_x11 *win); /* X misc */ bool x_is_idle(void); void x_setup(void); void x_free(void); -gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, - gpointer user_data); -gboolean x_mainloop_fd_check(GSource *source); -gboolean x_mainloop_fd_prepare(GSource *source, gint *timeout); +struct geometry x_parse_geometry(const char *geom_str); #endif /* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/test/data/icons/invalid.png b/test/data/icons/invalid.png new file mode 100644 index 0000000..07623af --- /dev/null +++ b/test/data/icons/invalid.png @@ -0,0 +1 @@ +Got'cha! This has to be invalid! diff --git a/test/data/icons/invalid.svg b/test/data/icons/invalid.svg new file mode 100644 index 0000000..07623af --- /dev/null +++ b/test/data/icons/invalid.svg @@ -0,0 +1 @@ +Got'cha! This has to be invalid! diff --git a/test/data/icons/path/invalid/icon1.png b/test/data/icons/path/invalid/icon1.png new file mode 120000 index 0000000..d6b006b --- /dev/null +++ b/test/data/icons/path/invalid/icon1.png @@ -0,0 +1 @@ +../../invalid.png \ No newline at end of file diff --git a/test/data/icons/path/invalid/icon1.svg b/test/data/icons/path/invalid/icon1.svg new file mode 120000 index 0000000..b5f5825 --- /dev/null +++ b/test/data/icons/path/invalid/icon1.svg @@ -0,0 +1 @@ +../../invalid.svg \ No newline at end of file diff --git a/test/data/icons/path/valid/icon1.png b/test/data/icons/path/valid/icon1.png new file mode 120000 index 0000000..28a474c --- /dev/null +++ b/test/data/icons/path/valid/icon1.png @@ -0,0 +1 @@ +../../valid.png \ No newline at end of file diff --git a/test/data/icons/path/valid/icon1.svg b/test/data/icons/path/valid/icon1.svg new file mode 120000 index 0000000..95267e3 --- /dev/null +++ b/test/data/icons/path/valid/icon1.svg @@ -0,0 +1 @@ +../../valid.svg \ No newline at end of file diff --git a/test/data/icons/path/valid/onlypng.png b/test/data/icons/path/valid/onlypng.png new file mode 120000 index 0000000..28a474c --- /dev/null +++ b/test/data/icons/path/valid/onlypng.png @@ -0,0 +1 @@ +../../valid.png \ No newline at end of file diff --git a/test/data/icons/path/valid/onlysvg.svg b/test/data/icons/path/valid/onlysvg.svg new file mode 120000 index 0000000..95267e3 --- /dev/null +++ b/test/data/icons/path/valid/onlysvg.svg @@ -0,0 +1 @@ +../../valid.svg \ No newline at end of file diff --git a/test/data/icons/valid.png b/test/data/icons/valid.png new file mode 100644 index 0000000..014e6a7 Binary files /dev/null and b/test/data/icons/valid.png differ diff --git a/test/data/icons/valid.svg b/test/data/icons/valid.svg new file mode 100644 index 0000000..7a1cb7f --- /dev/null +++ b/test/data/icons/valid.svg @@ -0,0 +1,65 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/test/icon.c b/test/icon.c new file mode 100644 index 0000000..5695a82 --- /dev/null +++ b/test/icon.c @@ -0,0 +1,98 @@ +#include "greatest.h" +#include "../src/icon.h" +#include "../src/utils.h" + +#include +#include + +#define ICONPREFIX "./data/icons/path" + +/* As there are no hints to test if the loaded GdkPixbuf is + * read from a PNG or an SVG file, the sample icons in the + * test structure have different sizes + */ +#define IS_ICON_PNG(pb) 4 == gdk_pixbuf_get_width(pb) +#define IS_ICON_SVG(pb) 16 == gdk_pixbuf_get_width(pb) + +TEST test_get_pixbuf_from_icon_invalid(void) +{ + GdkPixbuf *pixbuf = get_pixbuf_from_icon("invalid"); + ASSERT(pixbuf == NULL); + g_clear_pointer(&pixbuf, g_object_unref); + + PASS(); +} + +TEST test_get_pixbuf_from_icon_both(void) +{ + GdkPixbuf *pixbuf = get_pixbuf_from_icon("icon1"); + ASSERT(pixbuf); + ASSERTm("SVG pixbuf hasn't precedence", IS_ICON_SVG(pixbuf)); + g_clear_pointer(&pixbuf, g_object_unref); + + PASS(); +} + +TEST test_get_pixbuf_from_icon_onlysvg(void) +{ + GdkPixbuf *pixbuf = get_pixbuf_from_icon("onlysvg"); + ASSERT(pixbuf); + ASSERTm("SVG pixbuf isn't loaded", IS_ICON_SVG(pixbuf)); + g_clear_pointer(&pixbuf, g_object_unref); + + PASS(); +} + +TEST test_get_pixbuf_from_icon_onlypng(void) +{ + GdkPixbuf *pixbuf = get_pixbuf_from_icon("onlypng"); + ASSERT(pixbuf); + ASSERTm("PNG pixbuf isn't loaded", IS_ICON_PNG(pixbuf)); + g_clear_pointer(&pixbuf, g_object_unref); + + PASS(); +} + +TEST test_get_pixbuf_from_icon_filename(void) +{ + char *icon = string_append(g_get_current_dir(), "/data/icons/valid.png", NULL); + GdkPixbuf *pixbuf = get_pixbuf_from_icon(icon); + ASSERT(pixbuf); + ASSERTm("PNG pixbuf isn't loaded", IS_ICON_PNG(pixbuf)); + g_clear_pointer(&pixbuf, g_object_unref); + + g_free(icon); + PASS(); +} + +TEST test_get_pixbuf_from_icon_fileuri(void) +{ + char *curdir = g_get_current_dir(); + char *icon = g_strconcat("file://", curdir,"/data/icons/valid.svg", NULL); + GdkPixbuf *pixbuf = get_pixbuf_from_icon(icon); + ASSERT(pixbuf); + ASSERTm("SVG pixbuf isn't loaded", IS_ICON_SVG(pixbuf)); + g_clear_pointer(&pixbuf, g_object_unref); + + g_free(icon); + g_free(curdir); + PASS(); +} + +SUITE(suite_icon) +{ + settings.icon_path = + ICONPREFIX "/invalid" + ":" ICONPREFIX "/valid" + ":" ICONPREFIX "/both"; + + RUN_TEST(test_get_pixbuf_from_icon_invalid); + RUN_TEST(test_get_pixbuf_from_icon_both); + RUN_TEST(test_get_pixbuf_from_icon_onlysvg); + RUN_TEST(test_get_pixbuf_from_icon_onlypng); + RUN_TEST(test_get_pixbuf_from_icon_filename); + RUN_TEST(test_get_pixbuf_from_icon_fileuri); + + settings.icon_path = NULL; +} +/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/test/notification.c b/test/notification.c index c4e3494..a9c02d6 100644 --- a/test/notification.c +++ b/test/notification.c @@ -120,6 +120,8 @@ SUITE(suite_notification) g_free(b); RUN_TEST(test_notification_replace_single_field); + + g_clear_pointer(&settings.icon_path, g_free); } /* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */ diff --git a/test/test.c b/test/test.c index 935bda9..09835fd 100644 --- a/test/test.c +++ b/test/test.c @@ -8,6 +8,7 @@ SUITE_EXTERN(suite_utils); SUITE_EXTERN(suite_option_parser); SUITE_EXTERN(suite_notification); SUITE_EXTERN(suite_markup); +SUITE_EXTERN(suite_icon); GREATEST_MAIN_DEFS(); @@ -20,6 +21,7 @@ int main(int argc, char *argv[]) { RUN_SUITE(suite_option_parser); RUN_SUITE(suite_notification); RUN_SUITE(suite_markup); + RUN_SUITE(suite_icon); GREATEST_MAIN_END(); } /* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */