/* * Copyright (C) 2006 Sergey V. Udaltsov * * 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 #include #include #include #include #include #include #include #include #include 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); }