dunst/src/x11/x.c
Benedikt Heine 1fc96e9691 Call XFlush after redrawing the window
The commit 7f335b7 introduced, that specific notifications did not time
out properly. Although these had been internally closed already, X11
still displayed them.
2017-11-29 12:46:39 +01:00

1359 lines
45 KiB
C

/* copyright 2013 Sascha Kruse and contributors (see LICENSE for licensing information) */
#include "x.h"
#include <X11/X.h>
#include <X11/XKBlib.h>
#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <assert.h>
#include <cairo-xlib.h>
#include <cairo.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gdk/gdk.h>
#include <glib-object.h>
#include <locale.h>
#include <math.h>
#include <pango/pango-attributes.h>
#include <pango/pango-font.h>
#include <pango/pango-layout.h>
#include <pango/pango-types.h>
#include <pango/pangocairo.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "src/dbus.h"
#include "src/dunst.h"
#include "src/markup.h"
#include "src/notification.h"
#include "src/queues.h"
#include "src/settings.h"
#include "src/utils.h"
#include "screen.h"
#define WIDTH 400
#define HEIGHT 400
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;
/* 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)
{
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 x_string_to_color_t(const char *str)
{
char *end;
long int val = strtol(str+1, &end, 16);
if (*end != '\0' && *(end+1) != '\0') {
printf("WARNING: Invalid color string: \"%s\"\n", str);
}
return x_color_hex_to_double(val);
}
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:
printf("Unknown separator color type. Please file a Bugreport.\n");
return cl->fg;
}
}
static void x_cairo_setup(void)
{
cairo_ctx.surface = cairo_xlib_surface_create(xctx.dpy,
xctx.win, DefaultVisual(xctx.dpy, 0), WIDTH, HEIGHT);
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_surface_t *gdk_pixbuf_to_cairo_surface(const GdkPixbuf *pixbuf)
{
cairo_surface_t *icon_surface = NULL;
cairo_t *cr;
cairo_format_t format;
double width, height;
format = gdk_pixbuf_get_has_alpha(pixbuf) ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24;
width = gdk_pixbuf_get_width(pixbuf);
height = gdk_pixbuf_get_height(pixbuf);
icon_surface = cairo_image_surface_create(format, width, height);
cr = cairo_create(icon_surface);
gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
cairo_paint(cr);
cairo_destroy(cr);
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_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) {
fprintf(stderr,
"Could not load icon: '%s'\n", 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->color_strings[ColFG]);
cl->bg = x_string_to_color_t(n->color_strings[ColBG]);
cl->frame = x_string_to_color_t(n->color_strings[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) {
printf("Error parsing markup: %s\n", 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);
XFlush(xctx.dpy);
cairo_destroy(c);
cairo_surface_destroy(image_surface);
r_free_layouts(layouts);
}
static void x_win_move(int width, int height)
{
int x, y;
screen_info *scr = get_active_screen();
/* 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;
}
static void setopacity(Window win, unsigned long opacity)
{
Atom _NET_WM_WINDOW_OPACITY =
XInternAtom(xctx.dpy, "_NET_WM_WINDOW_OPACITY", false);
XChangeProperty(xctx.dpy,
win,
_NET_WM_WINDOW_OPACITY,
XA_CARDINAL,
32,
PropModeReplace,
(unsigned char *)&opacity,
1L);
}
/*
* Returns the modifier which is NumLock.
*/
static KeySym x_numlock_mod()
{
static KeyCode nl = 0;
KeySym sym = 0;
XModifierKeymap *map = XGetModifierMapping(xctx.dpy);
if (!nl)
nl = XKeysymToKeycode(xctx.dpy, XStringToKeysym("Num_Lock"));
for (int mod = 0; mod < 8; mod++) {
for (int j = 0; j < map->max_keypermod; j++) {
if (map->modifiermap[mod*map->max_keypermod+j] == nl) {
/* In theory, one could use `1 << mod`, but this
* could count as 'using implementation details',
* so use this large switch. */
switch (mod) {
case ShiftMapIndex:
sym = ShiftMask;
goto end;
case LockMapIndex:
sym = LockMask;
goto end;
case ControlMapIndex:
sym = ControlMask;
goto end;
case Mod1MapIndex:
sym = Mod1Mask;
goto end;
case Mod2MapIndex:
sym = Mod2Mask;
goto end;
case Mod3MapIndex:
sym = Mod3Mask;
goto end;
case Mod4MapIndex:
sym = Mod4Mask;
goto end;
case Mod5MapIndex:
sym = Mod5Mask;
goto end;
}
}
}
}
end:
XFreeModifiermap(map);
return sym;
}
/*
* Helper function to use glib's mainloop mechanic
* with Xlib
*/
gboolean x_mainloop_fd_prepare(GSource *source, gint *timeout)
{
if (timeout)
*timeout = -1;
else
g_print("BUG: x_mainloop_fd_prepare: timeout == NULL\n");
return false;
}
/*
* Helper function to use glib's mainloop mechanic
* with Xlib
*/
gboolean x_mainloop_fd_check(GSource *source)
{
return XPending(xctx.dpy) > 0;
}
/*
* Main Dispatcher for XEvents
*/
gboolean x_mainloop_fd_dispatch(GSource *source, GSourceFunc callback, gpointer user_data)
{
XEvent ev;
unsigned int state;
while (XPending(xctx.dpy) > 0) {
XNextEvent(xctx.dpy, &ev);
switch (ev.type) {
case Expose:
if (ev.xexpose.count == 0 && xctx.visible) {
x_win_draw();
}
break;
case SelectionNotify:
if (ev.xselection.property == xctx.utf8)
break;
case ButtonRelease:
if (ev.xbutton.window == xctx.win) {
x_handle_click(ev);
wake_up();
}
break;
case KeyPress:
state = ev.xkey.state;
/* NumLock is also encoded in the state. Remove it. */
state &= ~x_numlock_mod();
if (settings.close_ks.str
&& XLookupKeysym(&ev.xkey,
0) == settings.close_ks.sym
&& settings.close_ks.mask == state) {
const GList *displayed = queues_get_displayed();
if (displayed && displayed->data) {
queues_notification_close(displayed->data, REASON_USER);
wake_up();
}
}
if (settings.history_ks.str
&& XLookupKeysym(&ev.xkey,
0) == settings.history_ks.sym
&& settings.history_ks.mask == state) {
queues_history_pop();
wake_up();
}
if (settings.close_all_ks.str
&& XLookupKeysym(&ev.xkey,
0) == settings.close_all_ks.sym
&& settings.close_all_ks.mask == state) {
queues_history_push_all();
wake_up();
}
if (settings.context_ks.str
&& XLookupKeysym(&ev.xkey,
0) == settings.context_ks.sym
&& settings.context_ks.mask == state) {
context_menu();
wake_up();
}
break;
case FocusIn:
case FocusOut:
case PropertyNotify:
wake_up();
break;
default:
screen_check_event(ev);
break;
}
}
return true;
}
/*
* Check whether the user is currently idle.
*/
bool x_is_idle(void)
{
XScreenSaverQueryInfo(xctx.dpy, DefaultRootWindow(xctx.dpy),
xctx.screensaver_info);
if (settings.idle_threshold == 0) {
return false;
}
return xctx.screensaver_info->idle > settings.idle_threshold / 1000;
}
/* TODO move to x_mainloop_* */
/*
* Handle incoming mouse click events
*/
static void x_handle_click(XEvent ev)
{
if (ev.xbutton.button == Button3) {
queues_history_push_all();
return;
}
if (ev.xbutton.button == Button1 || ev.xbutton.button == Button2) {
int y = settings.separator_height;
notification *n = NULL;
int first = true;
for (const GList *iter = queues_get_displayed(); iter;
iter = iter->next) {
n = iter->data;
if (ev.xbutton.y > y && ev.xbutton.y < y + n->displayed_height)
break;
y += n->displayed_height + settings.separator_height;
if (first)
y += settings.frame_width;
}
if (n) {
if (ev.xbutton.button == Button1)
queues_notification_close(n, REASON_USER);
else
notification_do_action(n);
}
}
}
void x_free(void)
{
cairo_surface_destroy(cairo_ctx.surface);
cairo_destroy(cairo_ctx.context);
if (xctx.dpy)
XCloseDisplay(xctx.dpy);
}
/*
* Setup X11 stuff
*/
void x_setup(void)
{
/* initialize xctx.dc, font, keyboard, colors */
if (!setlocale(LC_CTYPE, "") || !XSupportsLocale())
fputs("no locale support\n", stderr);
if (!(xctx.dpy = XOpenDisplay(NULL))) {
die("cannot open display\n", EXIT_FAILURE);
}
x_shortcut_init(&settings.close_ks);
x_shortcut_init(&settings.close_all_ks);
x_shortcut_init(&settings.history_ks);
x_shortcut_init(&settings.context_ks);
x_shortcut_grab(&settings.close_ks);
x_shortcut_ungrab(&settings.close_ks);
x_shortcut_grab(&settings.close_all_ks);
x_shortcut_ungrab(&settings.close_all_ks);
x_shortcut_grab(&settings.history_ks);
x_shortcut_ungrab(&settings.history_ks);
x_shortcut_grab(&settings.context_ks);
x_shortcut_ungrab(&settings.context_ks);
xctx.color_strings[ColFG][URG_LOW] = settings.lowfgcolor;
xctx.color_strings[ColFG][URG_NORM] = settings.normfgcolor;
xctx.color_strings[ColFG][URG_CRIT] = settings.critfgcolor;
xctx.color_strings[ColBG][URG_LOW] = settings.lowbgcolor;
xctx.color_strings[ColBG][URG_NORM] = settings.normbgcolor;
xctx.color_strings[ColBG][URG_CRIT] = settings.critbgcolor;
if (settings.lowframecolor)
xctx.color_strings[ColFrame][URG_LOW] = settings.lowframecolor;
else
xctx.color_strings[ColFrame][URG_LOW] = settings.frame_color;
if (settings.normframecolor)
xctx.color_strings[ColFrame][URG_NORM] = settings.normframecolor;
else
xctx.color_strings[ColFrame][URG_NORM] = settings.frame_color;
if (settings.critframecolor)
xctx.color_strings[ColFrame][URG_CRIT] = settings.critframecolor;
else
xctx.color_strings[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);
}
static void x_set_wm(Window win)
{
Atom data[2];
/* set window title */
char *title = settings.title != NULL ? settings.title : "Dunst";
Atom _net_wm_title =
XInternAtom(xctx.dpy, "_NET_WM_NAME", false);
XStoreName(xctx.dpy, win, title);
XChangeProperty(xctx.dpy,
win,
_net_wm_title,
XInternAtom(xctx.dpy, "UTF8_STRING", false),
8,
PropModeReplace,
(unsigned char *)title,
strlen(title));
/* set window class */
char *class = settings.class != NULL ? settings.class : "Dunst";
XClassHint classhint = { class, "Dunst" };
XSetClassHint(xctx.dpy, win, &classhint);
/* set window type */
Atom net_wm_window_type =
XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE", false);
data[0] = XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE_NOTIFICATION", false);
data[1] = XInternAtom(xctx.dpy, "_NET_WM_WINDOW_TYPE_UTILITY", false);
XChangeProperty(xctx.dpy,
win,
net_wm_window_type,
XA_ATOM,
32,
PropModeReplace,
(unsigned char *)data,
2L);
/* set state above */
Atom net_wm_state =
XInternAtom(xctx.dpy, "_NET_WM_STATE", false);
data[0] = XInternAtom(xctx.dpy, "_NET_WM_STATE_ABOVE", false);
XChangeProperty(xctx.dpy, win, net_wm_state, XA_ATOM, 32,
PropModeReplace, (unsigned char *) data, 1L);
}
/*
* Setup the window
*/
static void x_win_setup(void)
{
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;
wa.event_mask =
ExposureMask | KeyPressMask | VisibilityChangeMask |
ButtonReleaseMask | FocusChangeMask| StructureNotifyMask;
screen_info *scr = get_active_screen();
xctx.win = XCreateWindow(xctx.dpy,
root,
scr->dim.x,
scr->dim.y,
scr->dim.w,
1,
0,
DefaultDepth(xctx.dpy, DefaultScreen(xctx.dpy)),
CopyFromParent,
DefaultVisual(xctx.dpy, DefaultScreen(xctx.dpy)),
CWOverrideRedirect | CWBackPixmap | CWEventMask,
&wa);
x_set_wm(xctx.win);
settings.transparency =
settings.transparency > 100 ? 100 : settings.transparency;
setopacity(xctx.win,
(unsigned long)((100 - settings.transparency) *
(0xffffffff / 100)));
if (settings.f_mode != FOLLOW_NONE) {
long root_event_mask = FocusChangeMask | PropertyChangeMask;
XSelectInput(xctx.dpy, root, root_event_mask);
}
}
/*
* Show the window and grab shortcuts.
*/
void x_win_show(void)
{
/* window is already mapped or there's nothing to show */
if (xctx.visible || queues_length_displayed() == 0) {
return;
}
x_shortcut_grab(&settings.close_ks);
x_shortcut_grab(&settings.close_all_ks);
x_shortcut_grab(&settings.context_ks);
x_shortcut_setup_error_handler();
XGrabButton(xctx.dpy,
AnyButton,
AnyModifier,
xctx.win,
false,
BUTTONMASK,
GrabModeAsync,
GrabModeSync,
None,
None);
if (x_shortcut_tear_down_error_handler()) {
fprintf(stderr, "Unable to grab mouse button(s)\n");
}
XMapRaised(xctx.dpy, xctx.win);
xctx.visible = true;
}
/*
* Hide the window and ungrab unused keyboard_shortcuts
*/
void x_win_hide()
{
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);
XFlush(xctx.dpy);
xctx.visible = false;
}
/*
* Parse a string into a modifier mask.
*/
KeySym x_shortcut_string_to_mask(const char *str)
{
if (!strcmp(str, "ctrl")) {
return ControlMask;
} else if (!strcmp(str, "mod4")) {
return Mod4Mask;
} else if (!strcmp(str, "mod3")) {
return Mod3Mask;
} else if (!strcmp(str, "mod2")) {
return Mod2Mask;
} else if (!strcmp(str, "mod1")) {
return Mod1Mask;
} else if (!strcmp(str, "shift")) {
return ShiftMask;
} else {
fprintf(stderr, "Warning: Unknown Modifier: %s\n", str);
return 0;
}
}
/*
* Error handler for grabbing mouse and keyboard errors.
*/
static int GrabXErrorHandler(Display *display, XErrorEvent *e)
{
dunst_grab_errored = true;
char err_buf[BUFSIZ];
XGetErrorText(display, e->error_code, err_buf, BUFSIZ);
fputs(err_buf, stderr);
fputs("\n", stderr);
if (e->error_code != BadAccess) {
exit(EXIT_FAILURE);
}
return 0;
}
/*
* Setup the Error handler.
*/
static void x_shortcut_setup_error_handler(void)
{
dunst_grab_errored = false;
XFlush(xctx.dpy);
XSetErrorHandler(GrabXErrorHandler);
}
/*
* Tear down the Error handler.
*/
static int x_shortcut_tear_down_error_handler(void)
{
XFlush(xctx.dpy);
XSync(xctx.dpy, false);
XSetErrorHandler(NULL);
return dunst_grab_errored;
}
/*
* Grab the given keyboard shortcut.
*/
int x_shortcut_grab(keyboard_shortcut *ks)
{
if (!ks->is_valid)
return 1;
Window root;
root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy));
x_shortcut_setup_error_handler();
if (ks->is_valid) {
XGrabKey(xctx.dpy,
ks->code,
ks->mask,
root,
true,
GrabModeAsync,
GrabModeAsync);
XGrabKey(xctx.dpy,
ks->code,
ks->mask | x_numlock_mod(),
root,
true,
GrabModeAsync,
GrabModeAsync);
}
if (x_shortcut_tear_down_error_handler()) {
fprintf(stderr, "Unable to grab key \"%s\"\n", ks->str);
ks->is_valid = false;
return 1;
}
return 0;
}
/*
* Ungrab the given keyboard shortcut.
*/
void x_shortcut_ungrab(keyboard_shortcut *ks)
{
Window root;
root = RootWindow(xctx.dpy, DefaultScreen(xctx.dpy));
if (ks->is_valid) {
XUngrabKey(xctx.dpy, ks->code, ks->mask, root);
XUngrabKey(xctx.dpy, ks->code, ks->mask | x_numlock_mod(), root);
}
}
/*
* Initialize the keyboard shortcut.
*/
void x_shortcut_init(keyboard_shortcut *ks)
{
if (ks == NULL || ks->str == NULL)
return;
if (!strcmp(ks->str, "none") || (!strcmp(ks->str, ""))) {
ks->is_valid = false;
return;
}
char *str = g_strdup(ks->str);
char *str_begin = str;
while (strchr(str, '+')) {
char *mod = str;
while (*str != '+')
str++;
*str = '\0';
str++;
g_strchomp(mod);
ks->mask = ks->mask | x_shortcut_string_to_mask(mod);
}
g_strstrip(str);
ks->sym = XStringToKeysym(str);
/* find matching keycode for ks->sym */
int min_keysym, max_keysym;
XDisplayKeycodes(xctx.dpy, &min_keysym, &max_keysym);
ks->code = NoSymbol;
for (int i = min_keysym; i <= max_keysym; i++) {
if (XkbKeycodeToKeysym(xctx.dpy, i, 0, 0) == ks->sym
|| XkbKeycodeToKeysym(xctx.dpy, i, 0, 1) == ks->sym) {
ks->code = i;
break;
}
}
if (ks->sym == NoSymbol || ks->code == NoSymbol) {
fprintf(stderr, "Warning: Unknown keyboard shortcut: %s\n",
ks->str);
ks->is_valid = false;
} else {
ks->is_valid = true;
}
g_free(str_begin);
}
/* vim: set tabstop=8 shiftwidth=8 expandtab textwidth=0: */