Blob Blame History Raw
/*
 * Copyright (C) 2006 Sergey V. Udaltsov <svu@gnome.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
#include <config.h>

#include <memory.h>

#include <cairo.h>

#include <gdk/gdkkeysyms.h>
#include <gdk/gdkx.h>
#include <glib/gi18n-lib.h>

#include <gkbd-status.h>

#include <gkbd-desktop-config.h>
#include <gkbd-indicator-config.h>
#include <gkbd-configuration.h>

typedef struct _gki_globals {
	GkbdConfiguration *config;

	gint current_width;
	gint current_height;
	int real_width;

	GSList *icons;		/* list of GdkPixbuf */
} gki_globals;

static gchar *settings_signal_names[] = {
	"notify::gtk-theme-name",
	"notify::gtk-key-theme-name",
	"notify::gtk-font-name",
	"notify::font-options",
};

struct _GkbdStatusPrivate {
	gulong settings_signal_handlers[sizeof (settings_signal_names) /
					sizeof (settings_signal_names[0])];
};

/* one instance for ALL widgets */
static gki_globals globals;

G_DEFINE_TYPE (GkbdStatus, gkbd_status, GTK_TYPE_STATUS_ICON)

typedef struct {
	GtkWidget *tray_icon;
} GkbdStatusPrivHack;

static void
gkbd_status_global_init (void);
static void
gkbd_status_global_term (void);
static GdkPixbuf *
gkbd_status_prepare_drawing (GkbdStatus * gki, int group);
static void
gkbd_status_set_current_page_for_group (GkbdStatus * gki, int group);
static void
gkbd_status_set_current_page (GkbdStatus * gki);
static void
gkbd_status_reinit_globals (GkbdStatus * gki);
static void
gkbd_status_cleanup_icons (void);
static void
gkbd_status_fill_icons (GkbdStatus * gki);
static void
gkbd_status_set_tooltips (GkbdStatus * gki, const char *str);

void
gkbd_status_set_tooltips (GkbdStatus * gki, const char *str)
{
	g_assert (str == NULL || g_utf8_validate (str, -1, NULL));

	gtk_status_icon_set_tooltip_text (GTK_STATUS_ICON (gki), str);
}

void
gkbd_status_cleanup_icons ()
{
	while (globals.icons) {
		if (globals.icons->data)
			g_object_unref (G_OBJECT (globals.icons->data));
		globals.icons =
		    g_slist_delete_link (globals.icons, globals.icons);
	}
}

static void
gkbd_status_fill_icons (GkbdStatus * gki)
{
	int grp;
	int total_groups =
	    xkl_engine_get_num_groups (gkbd_configuration_get_xkl_engine
				       (globals.config));

	for (grp = 0; grp < total_groups; grp++) {
		GdkPixbuf *page = gkbd_status_prepare_drawing (gki, grp);
		globals.icons = g_slist_append (globals.icons, page);
	}
}

static void
gkbd_status_activate (GkbdStatus * gki)
{
	xkl_debug (150, "Mouse button pressed on applet\n");
	gkbd_configuration_lock_next_group (globals.config);
}

static void
gkbd_status_render_cairo (GkbdStatusPrivHack * gkh, cairo_t * cr, int group)
{
	double r, g, b;
	GdkRGBA *fg_color;
	gchar *font_family;
	int font_size;
	PangoFontDescription *pfd;
	PangoContext *pcc;
	PangoLayout *pl;
	int lwidth, lheight;
	gchar *layout_name, *lbl_title;
	cairo_font_options_t *fo;
	static GHashTable *ln2cnt_map = NULL;

	GkbdIndicatorConfig *ind_cfg =
	    gkbd_configuration_get_indicator_config (globals.config);

	xkl_debug (160, "Rendering cairo for group %d\n", group);
	if (ind_cfg->background_color != NULL &&
	    ind_cfg->background_color[0] != 0) {
		if (sscanf
		    (ind_cfg->background_color, "%lg %lg %lg", &r,
		     &g, &b) == 3) {
			cairo_set_source_rgb (cr, r, g, b);
			cairo_rectangle (cr, 0, 0, globals.current_width,
					 globals.current_height);
			cairo_fill (cr);
		}
	}

	g_object_get (gkh->tray_icon, "fg-color", &fg_color, NULL);
	cairo_set_source_rgb (cr, fg_color->red, fg_color->green, fg_color->blue);
	gdk_rgba_free (fg_color);

	gkbd_indicator_config_get_font_for_widget (ind_cfg,
						   gkh->tray_icon,
						   &font_family,
						   &font_size);

	if (font_family != NULL && font_family[0] != 0) {
		cairo_select_font_face (cr, font_family,
					CAIRO_FONT_SLANT_NORMAL,
					CAIRO_FONT_WEIGHT_NORMAL);
	}

	pfd = pango_font_description_new ();
	pango_font_description_set_family (pfd, font_family);
	pango_font_description_set_style (pfd, PANGO_STYLE_NORMAL);
	pango_font_description_set_weight (pfd, PANGO_WEIGHT_NORMAL);
	pango_font_description_set_size (pfd,
					 ind_cfg->font_size * PANGO_SCALE);

	g_free (font_family);

	pcc = pango_cairo_create_context (cr);

	fo = cairo_font_options_copy (gdk_screen_get_font_options
				      (gdk_screen_get_default ()));
	/* SUBPIXEL antialiasing gives bad results on in-memory images */
	if (cairo_font_options_get_antialias (fo) ==
	    CAIRO_ANTIALIAS_SUBPIXEL)
		cairo_font_options_set_antialias (fo,
						  CAIRO_ANTIALIAS_GRAY);
	pango_cairo_context_set_font_options (pcc, fo);

	pl = pango_layout_new (pcc);

	layout_name =
	    gkbd_configuration_extract_layout_name (globals.config, group);
	lbl_title =
	    gkbd_configuration_create_label_title (group, &ln2cnt_map,
						   layout_name);

	if (group + 1 ==
	    xkl_engine_get_num_groups (gkbd_configuration_get_xkl_engine
				       (globals.config))) {
		g_hash_table_destroy (ln2cnt_map);
		ln2cnt_map = NULL;
	}

	pango_layout_set_text (pl, lbl_title, -1);

	g_free (lbl_title);

	pango_layout_set_font_description (pl, pfd);
	pango_layout_get_size (pl, &lwidth, &lheight);

	cairo_move_to (cr,
		       (globals.current_width - lwidth / PANGO_SCALE) / 2,
		       (globals.current_height -
			lheight / PANGO_SCALE) / 2);

	pango_cairo_show_layout (cr, pl);

	pango_font_description_free (pfd);
	g_object_unref (pl);
	g_object_unref (pcc);
	cairo_font_options_destroy (fo);
	cairo_destroy (cr);

	globals.real_width = (lwidth / PANGO_SCALE) + 4;
	if (globals.real_width > globals.current_width)
		globals.real_width = globals.current_width;
	if (globals.real_width < globals.current_height)
		globals.real_width = globals.current_height;
}

static inline guint8
convert_color_channel (guint8 src, guint8 alpha)
{
	return alpha ? ((((guint) src) << 8) - src) / alpha : 0;
}

static void
convert_bgra_to_rgba (guint8 const *src, guint8 * dst, int width,
		      int height, int new_width)
{
	int xoffset = width - new_width;

	/* *4 */
	int ptr_step = xoffset << 2;

	int x, y;

	/* / 2 * 4 */
	src = src + ((xoffset >> 1) << 2);

	for (y = height; --y >= 0; src += ptr_step) {
		for (x = new_width; --x >= 0;) {
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
			dst[0] = convert_color_channel (src[2], src[3]);
			dst[1] = convert_color_channel (src[1], src[3]);
			dst[2] = convert_color_channel (src[0], src[3]);
			dst[3] = src[3];
#else
			dst[0] = convert_color_channel (src[1], src[0]);
			dst[1] = convert_color_channel (src[2], src[0]);
			dst[2] = convert_color_channel (src[3], src[0]);
			dst[3] = src[0];
#endif
			dst += 4;
			src += 4;
		}
	}
}

static GdkPixbuf *
gkbd_status_prepare_drawing (GkbdStatus * gki, int group)
{
	GError *gerror = NULL;
	char *image_filename;
	GdkPixbuf *image;

	if (globals.current_width == 0)
		return NULL;

	if (gkbd_configuration_if_flags_shown (globals.config)) {

		image_filename =
		    gkbd_configuration_get_image_filename (globals.config,
							   group);

		image = gdk_pixbuf_new_from_file_at_size (image_filename,
							  globals.current_width,
							  globals.current_height,
							  &gerror);

		if (image == NULL) {
			GtkWidget *dialog = gtk_message_dialog_new (NULL,
								    GTK_DIALOG_DESTROY_WITH_PARENT,
								    GTK_MESSAGE_ERROR,
								    GTK_BUTTONS_OK,
								    _
								    ("There was an error loading an image: %s"),
								    gerror
								    ==
								    NULL ?
								    "Unknown"
								    :
								    gerror->message);
			g_signal_connect (G_OBJECT (dialog), "response",
					  G_CALLBACK (gtk_widget_destroy),
					  NULL);

			gtk_window_set_resizable (GTK_WINDOW (dialog),
						  FALSE);

			gtk_widget_show (dialog);
			g_error_free (gerror);

			return NULL;
		}
		xkl_debug (150,
			   "Image %d[%s] loaded -> %p[%dx%d], alpha: %d\n",
			   group, image_filename, image,
			   gdk_pixbuf_get_width (image),
			   gdk_pixbuf_get_height (image),
			   gdk_pixbuf_get_has_alpha (image));

		return image;
	} else {
		cairo_surface_t *cs =
		    cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
						globals.current_width,
						globals.current_height);
		unsigned char *cairo_data;
		guchar *pixbuf_data;
		gkbd_status_render_cairo ((GkbdStatusPrivHack *) GTK_STATUS_ICON (gki)->priv,
					  cairo_create (cs), group);
		cairo_data = cairo_image_surface_get_data (cs);
#if 0
		char pngfilename[20];
		g_sprintf (pngfilename, "label%d.png", group);
		cairo_surface_write_to_png (cs, pngfilename);
		xkl_debug (150, "file %s is created\n", pngfilename);
#endif
		pixbuf_data =
		    g_new0 (guchar,
			    4 * globals.real_width *
			    globals.current_height);
		convert_bgra_to_rgba (cairo_data, pixbuf_data,
				      globals.current_width,
				      globals.current_height,
				      globals.real_width);

		cairo_surface_destroy (cs);

		image = gdk_pixbuf_new_from_data (pixbuf_data,
						  GDK_COLORSPACE_RGB,
						  TRUE,
						  8,
						  globals.real_width,
						  globals.current_height,
						  globals.real_width *
						  4,
						  (GdkPixbufDestroyNotify)
						  g_free, NULL);
		xkl_debug (150,
			   "Image %d created -> %p[%dx%d], alpha: %d\n",
			   group, image, gdk_pixbuf_get_width (image),
			   gdk_pixbuf_get_height (image),
			   gdk_pixbuf_get_has_alpha (image));

		return image;
	}
	return NULL;
}

static void
gkbd_status_update_tooltips (GkbdStatus * gki)
{
	gchar *buf =
	    gkbd_configuration_get_current_tooltip (globals.config);
	if (buf != NULL) {
		gkbd_status_set_tooltips (gki, buf);
		g_free (buf);
	}
}

static void
gkbd_status_reinit_globals (GkbdStatus * gki)
{
	gkbd_status_cleanup_icons ();
	gkbd_status_fill_icons (gki);
}

void
gkbd_status_reinit_ui (GkbdStatus * gki)
{
	gkbd_status_set_current_page (gki);
        /* To work around combined bugs in notification-area
         * and GtkStatusIcon, reshow the icon here, to ensure
         * size changes are picked up.
         */
        gtk_status_icon_set_visible (GTK_STATUS_ICON (gki), FALSE);
        gtk_status_icon_set_visible (GTK_STATUS_ICON (gki), TRUE);
}

/* Should be called once for all widgets */
static void
gkbd_status_cfg_callback (GkbdConfiguration * configuration)
{
	GSList *objects;
	xkl_debug (150, "Config changed: reinit ui\n");
	objects = gkbd_configuration_get_all_objects (configuration);
	if (objects)
		gkbd_status_reinit_globals (objects->data);
	ForAllObjects (configuration) {
		gkbd_status_reinit_ui (GKBD_STATUS (gki));
	} NextObject ()
}

/* Should be called once for all applets */
static void
gkbd_status_state_callback (GkbdConfiguration * configuration, gint group)
{
	xkl_debug (150, "Set page to group %d\n", group);
	ForAllObjects (configuration) {
		xkl_debug (150, "do repaint for icon %p\n", gki);
		gkbd_status_set_current_page_for_group (GKBD_STATUS (gki),
							group);
	}
	NextObject ()
}

void
gkbd_status_set_current_page (GkbdStatus * gki)
{
	XklEngine *engine =
	    gkbd_configuration_get_xkl_engine (globals.config);
	XklState *cur_state = xkl_engine_get_current_state (engine);
	if (cur_state->group >= 0)
		gkbd_status_set_current_page_for_group (gki,
							cur_state->group);
}

void
gkbd_status_set_current_page_for_group (GkbdStatus * gki, int group)
{
	GdkPixbuf *page =
	    GDK_PIXBUF (g_slist_nth_data (globals.icons, group));
	xkl_debug (150, "Revalidating for group %d: %p\n", group, page);

	if (page == NULL) {
		xkl_debug (0, "Page for group %d is not ready\n", group);
		return;
	}

	gtk_status_icon_set_from_pixbuf (GTK_STATUS_ICON (gki), page);

	gkbd_status_update_tooltips (gki);
}

/* Should be called once for all widgets */
static GdkFilterReturn
gkbd_status_filter_x_evt (GdkXEvent * xev, GdkEvent * event)
{
	XEvent *xevent = (XEvent *) xev;
	XklEngine *engine =
	    gkbd_configuration_get_xkl_engine (globals.config);

	xkl_engine_filter_events (engine, xevent);
	switch (xevent->type) {
	case ReparentNotify:
		{
			XReparentEvent *rne = (XReparentEvent *) xev;

			ForAllObjects (globals.config) {
				guint32 xid =
				    gtk_status_icon_get_x11_window_id
				    (GTK_STATUS_ICON (gki));

				/* compare the indicator's parent window with the even window */
				if (xid == rne->window) {
					/* if so - make it transparent... */
					xkl_engine_set_window_transparent
					    (engine, rne->window, TRUE);
				}
			}
		NextObject ()}
		break;
	}
	return GDK_FILTER_CONTINUE;
}


/* Should be called once for all widgets */
static void
gkbd_status_start_listen (void)
{
	gdk_window_add_filter (NULL, (GdkFilterFunc)
			       gkbd_status_filter_x_evt, NULL);
	gdk_window_add_filter (gdk_get_default_root_window (),
			       (GdkFilterFunc) gkbd_status_filter_x_evt,
			       NULL);
}

/* Should be called once for all widgets */
static void
gkbd_status_stop_listen (void)
{
	gdk_window_remove_filter (NULL, (GdkFilterFunc)
				  gkbd_status_filter_x_evt, NULL);
	gdk_window_remove_filter
	    (gdk_get_default_root_window (),
	     (GdkFilterFunc) gkbd_status_filter_x_evt, NULL);
}

static void
gkbd_status_size_changed (GkbdStatus * gki, gint size)
{
	xkl_debug (150, "Size changed to %d\n", size);
        /* Ignore the initial size 200 that we get before
         * we are embedded
         */
        if (!gtk_status_icon_is_embedded (GTK_STATUS_ICON (gki)))
                return;
	if (globals.current_height != size) {
		globals.current_height = size;
		globals.current_width = size * 3 / 2;
		gkbd_status_reinit_globals (gki);
		gkbd_status_reinit_ui (gki);
	}
}

static void
gkbd_status_theme_changed (GtkSettings * settings, GParamSpec * pspec,
			   GkbdStatus * gki)
{
	xkl_debug (150, "Theme changed\n");
	gkbd_indicator_config_refresh_style
	    (gkbd_configuration_get_indicator_config (globals.config));
	gkbd_status_reinit_globals (gki);
	gkbd_status_reinit_ui (gki);
}

static void
gkbd_status_init (GkbdStatus * gki)
{
	int i;

	if (!gkbd_configuration_if_any_object_exists (globals.config))
		gkbd_status_global_init ();

	gki->priv = g_new0 (GkbdStatusPrivate, 1);

	/* This should give Notification Area a hint about the order of icons */
	gtk_status_icon_set_name (GTK_STATUS_ICON (gki), "keyboard");

	xkl_debug (100, "The status icon startup process for %p started\n",
		   gki);

	if (gkbd_configuration_get_xkl_engine (globals.config) == NULL) {
		gkbd_status_set_tooltips (gki,
					  _("XKB initialization error"));
		return;
	}

	/* append AFTER all initialization work is finished */
	gkbd_configuration_append_object (globals.config, G_OBJECT (gki));

	g_signal_connect (gki, "size-changed",
			  G_CALLBACK (gkbd_status_size_changed), NULL);
	g_signal_connect (gki, "activate",
			  G_CALLBACK (gkbd_status_activate), NULL);

	for (i = sizeof (settings_signal_names) /
	     sizeof (settings_signal_names[0]); --i >= 0;)
		gki->priv->settings_signal_handlers[i] =
		    g_signal_connect_after (gtk_settings_get_default (),
					    settings_signal_names[i],
					    G_CALLBACK
					    (gkbd_status_theme_changed),
					    gki);

	xkl_debug (100,
		   "The status icon startup process for %p completed\n",
		   gki);
}

static void
gkbd_status_finalize (GObject * obj)
{
	int i;
	GkbdStatus *gki = GKBD_STATUS (obj);
	xkl_debug (100,
		   "Starting the gnome-kbd-status widget shutdown process for %p\n",
		   gki);

	for (i = sizeof (settings_signal_names) /
	     sizeof (settings_signal_names[0]); --i >= 0;)
		g_signal_handler_disconnect (gtk_settings_get_default (),
					     gki->
					     priv->settings_signal_handlers
					     [i]);

	/* remove BEFORE all termination work is finished */
	gkbd_configuration_remove_object (globals.config, G_OBJECT (gki));

	gkbd_status_cleanup_icons ();

	xkl_debug (100,
		   "The instance of gnome-kbd-status successfully finalized\n");

	g_free (gki->priv);

	G_OBJECT_CLASS (gkbd_status_parent_class)->finalize (obj);

	if (!gkbd_configuration_if_any_object_exists (globals.config))
		gkbd_status_global_term ();
}

static void
gkbd_status_global_term (void)
{
	xkl_debug (100, "*** Last  GkbdStatus instance *** \n");
	gkbd_status_stop_listen ();

	g_object_unref (globals.config);
	globals.config = NULL;

	xkl_debug (100, "*** Terminated globals *** \n");
}

static void
gkbd_status_class_init (GkbdStatusClass * klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	xkl_debug (100, "*** First GkbdStatus instance *** \n");

	memset (&globals, 0, sizeof (globals));

	/* Initing vtable */
	object_class->finalize = gkbd_status_finalize;
}

static void
gkbd_status_global_init (void)
{
	globals.config = gkbd_configuration_get ();

	g_signal_connect (globals.config, "group-changed",
			  G_CALLBACK (gkbd_status_state_callback), NULL);
	g_signal_connect (globals.config, "changed",
			  G_CALLBACK (gkbd_status_cfg_callback), NULL);

	gkbd_status_start_listen ();

	xkl_debug (100, "*** Inited globals *** \n");
}

GtkStatusIcon *
gkbd_status_new (void)
{
	return
	    GTK_STATUS_ICON (g_object_new (gkbd_status_get_type (), NULL));
}

/**
 * gkbd_status_get_xkl_engine:
 *
 * Returns: (transfer none): The engine shared by all GkbdStatus objects
 */
XklEngine *
gkbd_status_get_xkl_engine ()
{
	return gkbd_configuration_get_xkl_engine (globals.config);
}

/**
 * gkbd_status_get_group_names:
 *
 * Returns: (transfer none) (array zero-terminated=1): List of group names
 */
gchar **
gkbd_status_get_group_names ()
{
	return (gchar **)
	    gkbd_configuration_get_group_names (globals.config);
}

gchar *
gkbd_status_get_image_filename (guint group)
{
	return gkbd_configuration_get_image_filename (globals.config,
						      group);
}