* Guifications - The end all, be all, toaster popup plugin * Copyright (C) 2003-2005 Gary Kramlich * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program 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 General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # include "../gf_config.h" # include <pango/pangoft2.h> #include <conversation.h> #include "gf_event_info.h" #include "gf_gtk_utils.h" #include "gf_item_text.h" #include "gf_notification.h" #include "gf_preferences.h" #include "gf_theme_ops.h" GfItemTextClipping clipping; #define IS_EVEN(number) ((number & 1) == 1 ? FALSE: TRUE) /******************************************************************************* * If I read the mail thread correctly, a ft2 font map caches the glyphs. * I was disregarding the font map and everything because I only needed it to * create the context in order to create the layout, this was causing some * major memory issues. Therefore the gf_item_text subsystem was created. All * this does is hold our font map and context. On win32 we do not need a font * map so all it does is keep the context around until uninit is called. With * the caching in ft2, we should in theory be drawing anything thats not a * format token faster, which is a good thing. Also, we aren't constantly * getting the context and unref'n it which will save a few cycles as well. ******************************************************************************/ static PangoFontMap *map = NULL; static PangoContext *context = NULL; gdouble xdpi = 75, ydpi = 75; # if GTK_CHECK_VERSION(2,2,0) map = pango_ft2_font_map_new(); # if GTK_CHECK_VERSION(2,2,0) display = gdk_display_get_default(); screen = gdk_display_get_screen(display, gaim_prefs_get_int(GF_PREF_ADVANCED_SCREEN)); xdpi = (double)((float)gdk_screen_get_width(screen) / (float)gdk_screen_get_width_mm(screen) * 25.4); ydpi = (double)((float)gdk_screen_get_height(screen) / (float)gdk_screen_get_height_mm(screen) * 25.4); pango_ft2_font_map_set_resolution(PANGO_FT2_FONT_MAP(map), xdpi, ydpi); context = pango_ft2_font_map_create_context(PANGO_FT2_FONT_MAP(map)); g_object_unref(G_OBJECT(map)); g_object_unref(G_OBJECT(context)); /******************************************************************************* ******************************************************************************/ gf_item_text_destroy(GfItemText *item_text) { g_return_if_fail(item_text); g_free(item_text->format); item_text->format = NULL; g_free(item_text->color); item_text->clipping = GF_ITEM_TEXT_CLIPPING_UNKNOWN; gf_item_text_new(GfItem *item) { g_return_val_if_fail(item, NULL); item_text = g_new0(GfItemText, 1); static GfItemTextClipping text_clipping_from_string(const gchar *string) { g_return_val_if_fail(string, GF_ITEM_TEXT_CLIPPING_UNKNOWN); if(!g_ascii_strcasecmp(string, "truncate")) return GF_ITEM_TEXT_CLIPPING_TRUNCATE; if(!g_ascii_strcasecmp(string, "ellipsis-start")) return GF_ITEM_TEXT_CLIPPING_ELLIPSIS_START; if(!g_ascii_strcasecmp(string, "ellipsis-middle")) return GF_ITEM_TEXT_CLIPPING_ELLIPSIS_MIDDLE; if(!g_ascii_strcasecmp(string, "ellipsis-end")) return GF_ITEM_TEXT_CLIPPING_ELLIPSIS_END; return GF_ITEM_TEXT_CLIPPING_UNKNOWN; text_clipping_to_string(GfItemTextClipping clip) { g_return_val_if_fail(clip != GF_ITEM_TEXT_CLIPPING_UNKNOWN, NULL); case GF_ITEM_TEXT_CLIPPING_TRUNCATE: case GF_ITEM_TEXT_CLIPPING_ELLIPSIS_START: case GF_ITEM_TEXT_CLIPPING_ELLIPSIS_MIDDLE: return "ellipsis-middle"; case GF_ITEM_TEXT_CLIPPING_ELLIPSIS_END: case GF_ITEM_TEXT_CLIPPING_UNKNOWN: gf_item_text_new_from_xmlnode(GfItem *item, xmlnode *node) { const gchar *data = NULL; g_return_val_if_fail(item, NULL); g_return_val_if_fail(node, NULL); item_text = gf_item_text_new(item); if(!(data = xmlnode_get_attrib(node, "format"))) { gaim_debug_info("Guifications", "** Error loading text item: 'No format given'\n"); gf_item_text_destroy(item_text); item_text->format = g_strdup(data); if((data = xmlnode_get_attrib(node, "font"))) item_text->font = g_strdup(data); if((data = xmlnode_get_attrib(node, "color"))) item_text->color = g_strdup(data); data = xmlnode_get_attrib(node, "clipping"); item_text->clipping = text_clipping_from_string(data); if(item_text->clipping == GF_ITEM_TEXT_CLIPPING_UNKNOWN) { gaim_debug_info("Guifications", "** Error loading text item: " "'Unknown clipping type'\n"); data = xmlnode_get_attrib(node, "width"); item_text->width = atoi(data); gf_item_text_copy(GfItemText *text) { g_return_val_if_fail(text, NULL); new_text = gf_item_text_new(text->item); new_text->format = g_strdup(text->format); new_text->font = g_strdup(text->font); new_text->color = g_strdup(text->color); new_text->clipping = text->clipping; new_text->width = text->width; gf_item_text_to_xmlnode(GfItemText *text) { parent = xmlnode_new("text"); xmlnode_set_attrib(parent, "format", text->format); xmlnode_set_attrib(parent, "font", text->font); xmlnode_set_attrib(parent, "color", text->color); if(text->clipping != GF_ITEM_TEXT_CLIPPING_UNKNOWN) xmlnode_set_attrib(parent, "clipping", text_clipping_to_string(text->clipping)); gchar *width = g_strdup_printf("%d", text->width); xmlnode_set_attrib(parent, "width", width); gf_item_text_set_format(GfItemText *item_text, const gchar *format) { g_return_if_fail(item_text); g_return_if_fail(format); g_free(item_text->format); item_text->format = g_strdup(format); gf_item_text_get_format(GfItemText *item_text) { g_return_val_if_fail(item_text, NULL); return item_text->format; gf_item_text_set_font(GfItemText *item_text, const gchar *font) { g_return_if_fail(item_text); item_text->font = g_strdup(font); gf_item_text_get_font(GfItemText *item_text) { g_return_val_if_fail(item_text, NULL); gf_item_text_set_color(GfItemText *item_text, const gchar *color) { g_return_if_fail(item_text); g_free(item_text->color); item_text->color = g_strdup(color); gf_item_text_get_color(GfItemText *item_text) { g_return_val_if_fail(item_text, NULL); gf_item_text_set_clipping(GfItemText *item_text, GfItemTextClipping clipping) { g_return_if_fail(item_text); g_return_if_fail(clipping >= 0 || clipping < GF_ITEM_TEXT_CLIPPING_UNKNOWN); item_text->clipping = clipping; gf_item_text_get_clipping(GfItemText *item_text) { g_return_val_if_fail(item_text, GF_ITEM_TEXT_CLIPPING_UNKNOWN); return item_text->clipping; gf_item_text_set_width(GfItemText *item_text, gint width) { g_return_if_fail(item_text); g_return_if_fail(width >= 0); item_text->width = width; gf_item_text_get_width(GfItemText *item_text) { g_return_val_if_fail(item_text, -1); gf_item_text_set_item(GfItemText *item_text, GfItem *item) { g_return_if_fail(item_text); gf_item_text_get_item(GfItemText *item_text) { g_return_val_if_fail(item_text, NULL); /******************************************************************************* ******************************************************************************/ /* why did I think this was a good idea? */ gf_item_text_parse_format(GfItemText *item_text, GfEventInfo *info) { GfNotification *notification; GaimConversation *conv = NULL; const gchar *tokens, *format, *time_format, *date_format, *warning; const gchar *target, *message, *extra; g_return_val_if_fail(item_text, NULL); g_return_val_if_fail(info, NULL); format = item_text->format; notification = gf_item_get_notification(item_text->item); theme = gf_notification_get_theme(notification); ops = gf_theme_get_theme_options(theme); time_format = gf_theme_options_get_time_format(ops); date_format = gf_theme_options_get_date_format(ops); warning = gf_theme_options_get_warning(ops); event = gf_event_info_get_event(info); target = gf_event_info_get_target(info); message = gf_event_info_get_message(info); extra = gf_event_info_get_extra(info); tokens = gf_event_get_tokens(event); ltime = localtime(&rtime); account = gf_event_info_get_account(info); conv = gf_event_info_get_conversation(info); while(format && format[0]) { str = g_string_append_c(str, format[0]); /* this increment is to get past the % */ if(!strchr(tokens, format[0])) { str = g_string_append_c(str, '%'); case 'a': /* account name */ str = g_string_append(str, gaim_account_get_username(account)); case 'C': /* conversation title */ str = g_string_append(str, conv ? gaim_conversation_get_title(conv) : target); case 'c': /* conversation name */ if(conv->type == GAIM_CONV_TYPE_IM) { buddy = gaim_find_buddy(account, conv->name); str = g_string_append(str, gaim_buddy_get_contact_alias(buddy)); str = g_string_append(str, conv->name); } else if(conv->type == GAIM_CONV_TYPE_CHAT) { chat = gaim_blist_find_chat(account, conv->name); str = g_string_append(str, gaim_chat_get_name(chat)); str = g_string_append(str, conv->name); str = g_string_append(str, conv->name); str = g_string_append(str, target); strftime(buff, sizeof(buff), date_format, ltime); str = g_string_append(str, buff); case 'd': /* day 01-31 */ strftime(buff, sizeof(buff), "%d", ltime); str = g_string_append(str, buff); case 'F': /* Chat Flags */ * which of course means to add theme options to give these numbers * text to make these make sense :) case 'f': /* Chat Flag Prefixes */ * which means to add theme options for the prefix, these should be 1 * char, like an ircd would send us. ie: @/+ case 'H': /* hour 01-23 */ strftime(buff, sizeof(buff), "%H", ltime); str = g_string_append(str, buff); case 'h': /* hour 01-12 */ strftime(buff, sizeof(buff), "%I", ltime); str = g_string_append(str, buff); str = g_string_append(str, gaim_network_get_public_ip()); strftime(buff, sizeof(buff), "%m", ltime); str = g_string_append(str, buff); strftime(buff, sizeof(buff), "%M", ltime); str = g_string_append(str, buff); case 'N': /* computer name */ gethostname(buff, sizeof(buff)); str = g_string_append(str, buff); case 'n': /* buddy screen name */ buddy = gf_event_info_get_buddy(info); const gchar *alias = gaim_buddy_get_contact_alias(buddy); str = g_string_append(str, alias); const gchar *target = gf_event_info_get_target(info); target = gf_event_info_get_target(info); buddy = gaim_find_buddy(account, target); alias = gaim_buddy_get_contact_alias(buddy); str = g_string_append(str, alias); str = g_string_append(str, target); case 'p': /* protocol name */ str = g_string_append(str, gaim_account_get_protocol_id(account)); case 'r': /* received message */ str = g_string_append(str, message); case 's': /* seconds 00-59 */ strftime(buff, sizeof(buff), "%S", ltime); str = g_string_append(str, buff); case 'T': /* Time according to the theme var */ strftime(buff, sizeof(buff), time_format, ltime); str = g_string_append(str, buff); case 't': /* seconds since the epoc */ strftime(buff, sizeof(buff), "%s", ltime); str = g_string_append(str, buff); case 'u': /* computer user name */ str = g_string_append(str, g_get_user_name()); #if !GAIM_VERSION_CHECK(2,0,0) str = g_string_append(str, target); case 'w': /* warning level */ buddy = gf_event_info_get_buddy(info); prpl_id = gaim_account_get_protocol_id(account); if(!g_ascii_strcasecmp(prpl_id, "prpl-toc") || !g_ascii_strcasecmp(prpl_id, "prpl-oscar")) g_string_append_printf(str, "%d", gf_get_warning_level(buddy)); str = g_string_append(str, warning); str = g_string_append(str, warning); case 'X': /* extra info */ str = g_string_append(str, extra); case 'Y': /* four digit year */ strftime(buff, sizeof(buff), "%Y", ltime); str = g_string_append(str, buff); case 'y': { /* two digit year */ /* dirty hack to avoid compiler warning */ strftime(buff, sizeof(buff), fmt, ltime); str = g_string_append(str, buff); /* this increment is to get past the formatting char */ g_string_free(str, FALSE); /* ugly hack to keep us working on glib 2.0 */ #if !GLIB_CHECK_VERSION(2,2,0) g_utf8_strreverse(const gchar *str, gssize len) { result = g_new0(gchar, len + 1); skip = g_utf8_skip[*(guchar*)p]; for (m = r; skip; skip--) /* this will probably break.. be sure to test it!!! */ gf_utf8_strrndup(const gchar *text, gint n) { gchar *rev = NULL, *tmp = NULL; rev = g_utf8_strreverse(text, -1); /* ewww, what's going on here? */ /* well, g_utf8_strncpy doesn't allocate, so we use strdup to allocate for us */ /* oh neat, that's pretty ugly though */ /* yeah, there's probably a better way... */ tmp = g_utf8_strncpy(tmp, rev, n); rev = g_utf8_strreverse(tmp, -1); /******************************************************************************* * The dreaded text clipping functions * Hopefully now "utf8 safe" (tm) ******************************************************************************/ text_truncate(PangoLayout *layout, gint width, gint offset) { g_return_if_fail(layout); pango_layout_get_pixel_size(layout, &l_width, NULL); if(l_width + offset <= width) text = pango_layout_get_text(layout); new_text = g_strdup(text); new_text = g_utf8_strncpy(new_text, text, g_utf8_strlen(text, -1) - 1); pango_layout_set_text(layout, new_text, -1); text_ellipsis_start(PangoLayout *layout, gint width, gint offset, const gchar *ellipsis_text, gint ellipsis_width) g_return_if_fail(layout); pango_layout_get_pixel_size(layout, &l_width, NULL); if(l_width + offset + ellipsis_width <= width) text = pango_layout_get_text(layout); new_text = gf_utf8_strrndup(text, g_utf8_strlen(text, -1) - 1); pango_layout_set_text(layout, new_text, -1); text = pango_layout_get_text(layout); new_text = g_strdup_printf("%s%s", ellipsis_text, text); pango_layout_set_text(layout, new_text, -1); text_ellipsis_middle(PangoLayout *layout, gint width, gint offset, const gchar *ellipsis_text, gint ellipsis_width) gchar *new_text = NULL, *left_text = NULL, *right_text = NULL; g_return_if_fail(layout); pango_layout_get_pixel_size(layout, &l_width, NULL); if(l_width + offset + ellipsis_width <= width) text = pango_layout_get_text(layout); mid = g_utf8_strlen(text, -1) / 2; left_text = g_strdup(text); left_text = g_utf8_strncpy(left_text, text, mid); if(IS_EVEN(g_utf8_strlen(text, -1))) right_text = gf_utf8_strrndup(text, mid - 1); right_text = gf_utf8_strrndup(text, mid); new_text = g_strdup_printf("%s%s", left_text, right_text); pango_layout_set_text(layout, new_text, -1); text = pango_layout_get_text(layout); mid = g_utf8_strlen(text, -1) / 2; left_text = g_strdup(text); left_text = g_utf8_strncpy(left_text, text, mid); if(IS_EVEN(g_utf8_strlen(text, -1))) right_text = gf_utf8_strrndup(text, mid - 1); right_text = gf_utf8_strrndup(text, mid); new_text = g_strdup_printf("%s%s%s", left_text, ellipsis_text, right_text); pango_layout_set_text(layout, new_text, -1); text_ellipsis_end(PangoLayout *layout, gint width, gint offset, const gchar *ellipsis_text, gint ellipsis_width) g_return_if_fail(layout); pango_layout_get_pixel_size(layout, &l_width, NULL); if(l_width + offset + ellipsis_width <= width) text = pango_layout_get_text(layout); new_text = g_strdup(text); new_text = g_utf8_strncpy(new_text, text, g_utf8_strlen(text, -1) - 1); pango_layout_set_text(layout, new_text, -1); text = pango_layout_get_text(layout); new_text = g_strdup_printf("%s%s", text, ellipsis_text); pango_layout_set_text(layout, new_text, -1); gf_item_text_clip(GfItemText *item_text, PangoLayout *layout, GfNotification *notification; const gchar *ellipsis_text; gint e_width = 0, l_width = 0, width = 0, offset = 0; g_return_if_fail(item_text); g_return_if_fail(layout); notification = gf_item_get_notification(item_text->item); theme = gf_notification_get_theme(notification); ops = gf_theme_get_theme_options(theme); ellipsis_text = gf_theme_options_get_ellipsis(ops); if((ioffset = gf_item_get_horz_offset(item_text->item))) { if(ioffset && gf_item_offset_get_is_percentage(ioffset)) offset = (pixbuf_width * gf_item_offset_get_value(ioffset)) / 100; offset = gf_item_offset_get_value(ioffset); width = item_text->width; ellipsis = pango_layout_copy(layout); pango_layout_set_text(ellipsis, ellipsis_text, -1); pango_layout_get_pixel_size(ellipsis, &e_width, NULL); g_object_unref(G_OBJECT(ellipsis)); pango_layout_get_pixel_size(layout, &l_width, NULL); switch (item_text->clipping) { case GF_ITEM_TEXT_CLIPPING_ELLIPSIS_START: text_ellipsis_start(layout, width, offset, ellipsis_text, e_width); case GF_ITEM_TEXT_CLIPPING_ELLIPSIS_MIDDLE: text_ellipsis_middle(layout, width, offset, ellipsis_text, e_width); case GF_ITEM_TEXT_CLIPPING_ELLIPSIS_END: text_ellipsis_end(layout, width, offset, ellipsis_text, e_width); case GF_ITEM_TEXT_CLIPPING_TRUNCATE: text_truncate(layout, width, offset); gf_item_text_create_layout(GfItemText *item_text, GfEventInfo *info, gint width) PangoLayout *layout = NULL; PangoFontDescription *font = NULL; g_return_val_if_fail(item_text, NULL); g_return_val_if_fail(info, NULL); layout = pango_layout_new(context); pango_layout_set_width(layout, -1); font = pango_font_description_from_string(item_text->font); pango_layout_set_font_description(layout, font); pango_font_description_free(font); pango_layout_set_font_description(layout, gf_gtk_theme_get_font()); text = gf_item_text_parse_format(item_text, info); pango_layout_set_text(layout, text, -1); gf_item_text_clip(item_text, layout, width); /* This function has cost me 5 days of trying to find some way to make gtk/pango/gdk do * this. I'm only using this way because 2 gtk/pango developers said this is the only * way at this time. So if you change it.. Your changes better fucking work :P gf_pixbuf_new_from_ft2_bitmap(FT_Bitmap *bitmap, PangoColor *color) { guchar *buffer, *pbuffer; /* Grab the colors from the PangoColor and shift 8 bits because we're only in 8 bit * mode, and a PangoColor's elements are guint16's which are 16 bits... /* create the new pixbuf */ pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, TRUE, 8, bitmap->width, bitmap->rows); /* clear out the pixbuf to transparent black */ gdk_pixbuf_fill(pixbuf, 0x00000000); buffer = gdk_pixbuf_get_pixels(pixbuf); rowstride = gdk_pixbuf_get_rowstride(pixbuf); /* ok here's the run down... * Image data in a pixbuf is stored in memory in uncompressed, packed format. Rows * in the image are stored top to bottom, and in each row pixels are stored from * left to right. There may be padding at the end of a row. The "rowstride" value of * a pixbuf, as returned by gdk_pixbuf_get_rowstride(), indicates the number of * So we take the height of the FT_Bitmap (the text), and increment until we're done. * Then we get the alpha for the row in the FT_Bitmap * Next we move left to right and draw accordingly. * Repeat until we've gone through the whole FT_Bitmap. for(h = 0; h < bitmap->rows; h++) { pbuffer = buffer + (h * rowstride); /* get the alpha from the FT_Bitmap */ alpha = bitmap->buffer + (h * (bitmap->pitch)); for(w = 0; w < bitmap->width; w++) { gf_item_text_render(GfItemText *item_text, GdkPixbuf *pixbuf, GfEventInfo *info) GdkPixbuf *t_pixbuf = NULL; PangoLayout *layout = NULL; gint n_width = 0, n_height = 0; gint t_width = 0, t_height = 0; gint l_width = 0, l_height = 0; g_return_if_fail(item_text); g_return_if_fail(pixbuf); /* get the width and height of the notification pixbuf */ n_width = gdk_pixbuf_get_width(pixbuf); n_height = gdk_pixbuf_get_height(pixbuf); layout = gf_item_text_create_layout(item_text, info, n_width); /* setup the FT_BITMAP */ pango_layout_get_pixel_size(layout, &l_width, &l_height); bitmap.pitch = (bitmap.width + 3) & ~3; bitmap.buffer = g_new0(guint8, bitmap.rows * bitmap.pitch); bitmap.pixel_mode = ft_pixel_mode_grays; pango_ft2_render_layout(&bitmap, layout, 0, 0); g_object_unref(G_OBJECT(layout)); gf_gtk_theme_get_fg_color(&g_color); gf_gtk_color_pango_from_gdk(&color, &g_color); } else if(!pango_color_parse(&color, item_text->color)) color.red = color.green = color.blue = 0; t_pixbuf = gf_pixbuf_new_from_ft2_bitmap(&bitmap, &color); t_width = gdk_pixbuf_get_width(t_pixbuf); t_height = gdk_pixbuf_get_height(t_pixbuf); gf_item_get_render_position(&x, &y, t_width, t_height, n_width, n_height, gf_gtk_pixbuf_clip_composite(t_pixbuf, x, y, pixbuf); g_object_unref(G_OBJECT(t_pixbuf));