"SfR Fresh" - the SfR Freeware/Shareware Archive

Member "balsa-2.4.7/src/balsa-mime-widget-text.c" of archive balsa-2.4.7.tar.gz:


/* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
/* Balsa E-Mail Client
 * Copyright (C) 1997-2010 Stuart Parmenter and others,
 *                         See the file AUTHORS for a list.
 *
 * 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, 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.
 */

#if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
# include "config.h"
#endif                          /* HAVE_CONFIG_H */
#include "balsa-mime-widget-text.h"

#include <string.h>
#include <stdlib.h>
#include "balsa-app.h"
#include "html.h"
#include <glib/gi18n.h>
#include "quote-color.h"
#include "sendmsg-window.h"
#include "store-address.h"
#include "balsa-mime-widget.h"
#include "balsa-mime-widget-callbacks.h"
#include "balsa-cite-bar.h"

#if HAVE_GTKSOURCEVIEW
#include <gtksourceview/gtksourceview.h>
#include <gtksourceview/gtksourcebuffer.h>
#include <gtksourceview/gtksourcelanguage.h>
#if (HAVE_GTKSOURCEVIEW == 1)
#  include <gtksourceview/gtksourcelanguagesmanager.h>
#else
#  include <gtksourceview/gtksourcelanguagemanager.h>
#endif
#endif


static GtkWidget * create_text_widget(const char * content_type);
static void bm_modify_font_from_string(GtkWidget * widget, const char *font);
static GtkTextTag * quote_tag(GtkTextBuffer * buffer, gint level, gint margin);
static gboolean fix_text_widget(GtkWidget *widget, gpointer data);
static void text_view_populate_popup(GtkTextView *textview, GtkMenu *menu,
				     LibBalsaMessageBody * mime_body);

#ifdef HAVE_HTML_WIDGET
static BalsaMimeWidget *bm_widget_new_html(BalsaMessage * bm,
                                           LibBalsaMessageBody *
                                           mime_body);
#endif
static BalsaMimeWidget * bm_widget_new_vcard(BalsaMessage * bm,
                                             LibBalsaMessageBody * mime_body,
                                             gchar * ptr, size_t len);

/* URL related stuff */
typedef struct _message_url_t {
    GtkTextMark *end_mark;
    gint start, end;             /* pos in the buffer */
    gchar *url;                  /* the link */
} message_url_t;


/* citation bars */
typedef struct {
    gint start_offs;
    gint end_offs;
    GtkTextIter start_iter;
    GtkTextIter end_iter;
    gint y_pos;
    gint height;
    guint depth;
    GtkWidget * bar;
} cite_bar_t;

/* store the coordinates at which the button was pressed */
static gint stored_x = -1, stored_y = -1;
static GdkModifierType stored_mask = -1;
#define STORED_MASK_BITS (  GDK_SHIFT_MASK   \
                          | GDK_CONTROL_MASK \
                          | GDK_MOD1_MASK    \
                          | GDK_MOD2_MASK    \
                          | GDK_MOD3_MASK    \
                          | GDK_MOD4_MASK    \
                          | GDK_MOD5_MASK    )

/* the cursors which are displayed over URL's and normal message text */
static GdkCursor *url_cursor_normal = NULL;
static GdkCursor *url_cursor_over_url = NULL;


static gboolean store_button_coords(GtkWidget * widget, GdkEventButton * event, gpointer data);
static gboolean check_over_url(GtkWidget * widget, GdkEventMotion * event, GList * url_list);
static void pointer_over_url(GtkWidget * widget, message_url_t * url, gboolean set);
static void prepare_url_offsets(GtkTextBuffer * buffer, GList * url_list);
static void url_found_cb(GtkTextBuffer * buffer, GtkTextIter * iter,
                         const gchar * buf, guint len, gpointer data);
static gboolean check_call_url(GtkWidget * widget, GdkEventButton * event, GList * url_list);
static message_url_t * find_url(GtkWidget * widget, gint x, gint y, GList * url_list);
static void handle_url(const gchar* url);
static void free_url_list(GList * url_list);
static void bm_widget_on_url(const gchar *url);
static void phrase_highlight(GtkTextBuffer * buffer, const gchar * id,
			     gunichar tag_char, const gchar * property,
			     gint value);
static void destroy_cite_bars(GList * cite_bars);
static gboolean draw_cite_bars(GtkWidget * widget, GdkEventExpose *event, GList * cite_bars);


#define PHRASE_HIGHLIGHT_ON    1
#define PHRASE_HIGHLIGHT_OFF   2

#define BALSA_MIME_WIDGET_NEW_TEXT_NOTIFIED \
    "balsa-mime-widget-text-new-notified"

#define BALSA_LEFT_MARGIN   2
#define BALSA_RIGHT_MARGIN 15

BalsaMimeWidget *
balsa_mime_widget_new_text(BalsaMessage * bm, LibBalsaMessageBody * mime_body,
			   const gchar * content_type, gpointer data)
{
    LibBalsaHTMLType html_type;
    gchar *ptr = NULL;
    ssize_t alloced;
    BalsaMimeWidget *mw;
    GtkTextBuffer *buffer;
#if USE_GREGEX
    GRegex *rex;
#else                           /* USE_GREGEX */
    regex_t rex;
#endif                          /* USE_GREGEX */
    GList *url_list = NULL;
    const gchar *target_cs;
    GError *err = NULL;
    gboolean is_text_plain;


    g_return_val_if_fail(mime_body != NULL, NULL);
    g_return_val_if_fail(content_type != NULL, NULL);

    /* handle HTML if possible */
    html_type = libbalsa_html_type(content_type);
    if (html_type) {
        BalsaMimeWidget *html_widget = NULL;

#ifdef HAVE_HTML_WIDGET
	/* Force vertical scrollbar while we render the html, otherwise
	 * the widget will make itself too wide to accept one, forcing
	 * otherwise unnecessary horizontal scrolling. */
        gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(bm->scroll),
                                       GTK_POLICY_AUTOMATIC,
                                       GTK_POLICY_ALWAYS);
        html_widget = bm_widget_new_html(bm, mime_body);
#endif
        return html_widget;
    }

    is_text_plain = !g_ascii_strcasecmp(content_type, "text/plain");
    alloced = libbalsa_message_body_get_content(mime_body, &ptr, &err);
    if (alloced < 0) {
        balsa_information(LIBBALSA_INFORMATION_ERROR,
                          _("Could not save a text part: %s"),
                          err ? err->message : "Unknown error");
        g_clear_error(&err);
        return NULL;
    }

    if(g_ascii_strcasecmp(content_type, "text/x-vcard") == 0 ||
       g_ascii_strcasecmp(content_type, "text/directory") == 0) {
        mw = bm_widget_new_vcard(bm, mime_body, ptr, alloced);
        if (mw) {
            g_free(ptr);
            return mw;
        }
        /* else it was not a vCard with at least one address; we'll just
         * show it as if it were text/plain. */
    }

    /* prepare a text part */
    if (!libbalsa_utf8_sanitize(&ptr, balsa_app.convert_unknown_8bit,
				&target_cs)
        && !g_object_get_data(G_OBJECT(bm->message), 
                              BALSA_MIME_WIDGET_NEW_TEXT_NOTIFIED)) {
	gchar *from =
	    balsa_message_sender_to_gchar(bm->message->headers->from, 0);
	gchar *subject =
	    g_strdup(LIBBALSA_MESSAGE_GET_SUBJECT(bm->message));
        
	libbalsa_utf8_sanitize(&from,    balsa_app.convert_unknown_8bit, 
			       NULL);
	libbalsa_utf8_sanitize(&subject, balsa_app.convert_unknown_8bit, 
			       NULL);
	libbalsa_information
	    (LIBBALSA_INFORMATION_MESSAGE,
	     _("The message sent by %s with subject \"%s\" contains 8-bit "
	       "characters, but no header describing the used codeset "
	       "(converted to %s)"),
	     from, subject,
	     target_cs ? target_cs : "\"?\"");
	g_free(subject);
	g_free(from);
        /* Avoid multiple notifications: */
        g_object_set_data(G_OBJECT(bm->message),
                          BALSA_MIME_WIDGET_NEW_TEXT_NOTIFIED, 
                          GUINT_TO_POINTER(TRUE));
    }

    mw = g_object_new(BALSA_TYPE_MIME_WIDGET, NULL);
    mw->widget = create_text_widget(content_type);

    if (libbalsa_message_body_is_flowed(mime_body)) {
	/* Parse, but don't wrap. */
	gboolean delsp = libbalsa_message_body_is_delsp(mime_body);
	ptr = libbalsa_wrap_rfc2646(ptr, G_MAXINT, FALSE, TRUE, delsp);
    } else if (bm->wrap_text
#if HAVE_GTKSOURCEVIEW
	       && !GTK_IS_SOURCE_VIEW(mw->widget)
#endif
	       )
	libbalsa_wrap_string(ptr, balsa_app.browse_wrap_length);

    gtk_text_view_set_editable(GTK_TEXT_VIEW(mw->widget), FALSE);
    gtk_text_view_set_left_margin(GTK_TEXT_VIEW(mw->widget),  BALSA_LEFT_MARGIN);
    gtk_text_view_set_right_margin(GTK_TEXT_VIEW(mw->widget), BALSA_RIGHT_MARGIN);
    gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(mw->widget), GTK_WRAP_WORD_CHAR);

    /* set the message font */
    bm_modify_font_from_string(mw->widget, balsa_app.message_font);

    g_signal_connect(G_OBJECT(mw->widget), "key_press_event",
		     G_CALLBACK(balsa_mime_widget_key_press_event),
		     (gpointer) bm);
    g_signal_connect(G_OBJECT(mw->widget), "populate-popup",
		     G_CALLBACK(text_view_populate_popup),
		     (gpointer)mime_body);

    buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mw->widget));
    allocate_quote_colors(GTK_WIDGET(bm), balsa_app.quoted_color,
			  0, MAX_QUOTED_COLOR - 1);
#if USE_GREGEX
    if (!(rex = balsa_quote_regex_new()))
	gtk_text_buffer_insert_at_cursor(buffer, ptr, -1);
#else                           /* USE_GREGEX */
    if (!balsa_app.mark_quoted
        || regcomp(&rex, balsa_app.quote_regex, REG_EXTENDED)) {
	if (balsa_app.mark_quoted)
            g_warning("%s: quote regex compilation failed.", __func__);
	gtk_text_buffer_insert_at_cursor(buffer, ptr, -1);
    }
#endif                          /* USE_GREGEX */
    else {
	const gchar *line_start;
	LibBalsaUrlInsertInfo url_info;
	GList * cite_bars_list;
	guint cite_level;
	guint cite_start;
	gint margin;
        gdouble char_width;
        PangoContext *context = gtk_widget_get_pango_context(mw->widget);
        PangoFontDescription *desc =
            pango_context_get_font_description(context);

        /* width of monospace characters is 3/5 of the size */
        char_width = 0.6 * pango_font_description_get_size(desc);
        if (!pango_font_description_get_size_is_absolute(desc))
            char_width = char_width / PANGO_SCALE;

        /* convert char_width from points to pixels */
        margin = (char_width / 72.0) *
            gdk_screen_get_resolution(gdk_screen_get_default());

	gtk_text_buffer_create_tag(buffer, "url",
				   "foreground-gdk",
				   &balsa_app.url_color, NULL);
	gtk_text_buffer_create_tag(buffer, "emphasize", 
				   "foreground", "red",
				   "underline", PANGO_UNDERLINE_SINGLE,
				   NULL);
	url_info.callback = url_found_cb;
	url_info.callback_data = &url_list;
	url_info.buffer_is_flowed = libbalsa_message_body_is_flowed(mime_body);
	url_info.ml_url_buffer = NULL;

	line_start = ptr;
	cite_bars_list = NULL;
	cite_level = 0;
	cite_start = 0;
	while (*line_start) {
	    const gchar *line_end;
	    GtkTextTag *tag = NULL;
	    int len;

	    if (!(line_end = strchr(line_start, '\n')))
                line_end = line_start + strlen(line_start);

	    if (is_text_plain) {
		guint quote_level;
		guint cite_idx;

		/* get the cite level only for text/plain parts */
#if USE_GREGEX
		libbalsa_match_regex(line_start, rex, &quote_level,
                                     &cite_idx);
#else                           /* USE_GREGEX */
		libbalsa_match_regex(line_start, &rex, &quote_level,
                                     &cite_idx);
#endif                          /* USE_GREGEX */
	    
		/* check if the citation level changed */
		if (cite_level != quote_level) {
		    if (cite_level > 0) {
			cite_bar_t * cite_bar = g_new0(cite_bar_t, 1);
		    
			cite_bar->start_offs = cite_start;
			cite_bar->end_offs = gtk_text_buffer_get_char_count(buffer);
			cite_bar->depth = cite_level;
			cite_bars_list = g_list_append(cite_bars_list, cite_bar);
		    }
		    if (quote_level > 0)
			cite_start = gtk_text_buffer_get_char_count(buffer);
		    cite_level = quote_level;
		}

		/* skip the citation prefix */
		if (quote_level) {
		    line_start += cite_idx;
                    if (line_start < line_end
                        && g_unichar_isspace(g_utf8_get_char(line_start)))
                        line_start = g_utf8_next_char(line_start);
		}
		tag = quote_tag(buffer, quote_level, margin);
	    }

            len = line_end - line_start;
	    if (len > 0 && line_start[len - 1] == '\r')
                --len;
	    /* tag is NULL if the line isn't quoted, but it causes
	     * no harm */
	    if (!libbalsa_insert_with_url(buffer, line_start, len,
					  tag, &url_info))
		gtk_text_buffer_insert_at_cursor(buffer, "\n", 1);
	    
	    line_start = *line_end ? line_end + 1 : line_end;
	}

	/* add any pending cited part */
	if (cite_level > 0) {
	    cite_bar_t * cite_bar = g_new0(cite_bar_t, 1);
		    
	    cite_bar->start_offs = cite_start;
	    cite_bar->end_offs = gtk_text_buffer_get_char_count(buffer);
	    cite_bar->depth = cite_level;
	    cite_bars_list = g_list_append(cite_bars_list, cite_bar);
	}

	/* add list of citation bars (if any) */
	if (cite_bars_list) {
	    g_object_set_data_full(G_OBJECT(mw->widget), "cite-bars", cite_bars_list,
				   (GDestroyNotify) destroy_cite_bars);
	    g_object_set_data(G_OBJECT(mw->widget), "cite-margin", GINT_TO_POINTER(margin));
	    g_signal_connect_after(G_OBJECT(mw->widget), "expose-event",
				   G_CALLBACK(draw_cite_bars), cite_bars_list);
	}
#if USE_GREGEX
	g_regex_unref(rex);
#else                           /* USE_GREGEX */
	regfree(&rex);
#endif                          /* USE_GREGEX */
    }

    prepare_url_offsets(buffer, url_list);
    g_signal_connect_after(G_OBJECT(mw->widget), "realize",
			   G_CALLBACK(fix_text_widget), url_list);
    if (url_list) {
	g_signal_connect(G_OBJECT(mw->widget), "button_press_event",
			 G_CALLBACK(store_button_coords), NULL);
	g_signal_connect(G_OBJECT(mw->widget), "button_release_event",
			 G_CALLBACK(check_call_url), url_list);
	g_signal_connect(G_OBJECT(mw->widget), "motion-notify-event",
			 G_CALLBACK(check_over_url), url_list);
	g_signal_connect(G_OBJECT(mw->widget), "leave-notify-event",
			 G_CALLBACK(check_over_url), url_list);
	g_object_set_data_full(G_OBJECT(mw->widget), "url-list", url_list,
			       (GDestroyNotify)free_url_list);
    }

    if (is_text_plain) {
	/* plain-text highlighting */
	g_object_set_data(G_OBJECT(mw->widget), "phrase-highlight",
			  GINT_TO_POINTER(PHRASE_HIGHLIGHT_ON));
	phrase_highlight(buffer, "hp-bold", '*', "weight", PANGO_WEIGHT_BOLD);
	phrase_highlight(buffer, "hp-underline", '_', "underline", PANGO_UNDERLINE_SINGLE);
	phrase_highlight(buffer, "hp-italic", '/', "style", PANGO_STYLE_ITALIC);
    }

    /* size allocation may not be correct, so we'll check back later */
    balsa_mime_widget_schedule_resize(mw->widget);
    
    g_free(ptr);

    return mw;
}


/* -- local functions -- */
static GtkWidget *
create_text_widget(const char * content_type)
{
#if (HAVE_GTKSOURCEVIEW == 1)
    static GtkSourceLanguagesManager * lm = NULL;
    GtkSourceLanguage * lang;
    GtkSourceBuffer * buffer;
    GtkWidget * widget;

    /* we use or own highlighting for text/plain */
    if (!g_ascii_strcasecmp(content_type, "text/plain"))
	return gtk_text_view_new();

    /* check if GtkSourceView knows our content type */
    if (!lm)
	lm = gtk_source_languages_manager_new();
    if (!lm ||
	!(lang = gtk_source_languages_manager_get_language_from_mime_type(lm, content_type)))
	return gtk_text_view_new();

    /* create a GtkSourceView for our content type */
    buffer = gtk_source_buffer_new_with_language(lang);
    gtk_source_buffer_set_highlight(buffer, TRUE);
    // TODO: maybe we want to use (a) our own highlighting styles or (b) use
    // GEdit-2's here?
    widget = gtk_source_view_new_with_buffer(buffer);
    g_object_unref(buffer);
    return widget;
#elif (HAVE_GTKSOURCEVIEW == 2)
    static GtkSourceLanguageManager * lm = NULL;
    static const gchar * const * lm_ids = NULL;
    GtkWidget * widget = NULL;
    gint n;

    /* we use or own highlighting for text/plain */
    if (!g_ascii_strcasecmp(content_type, "text/plain"))
	return gtk_text_view_new();
    
    /* try to initialise the source language manager and return a "simple"
     * text view if this fails */
    if (!lm)
	lm = gtk_source_language_manager_get_default();
    if (lm && !lm_ids)
	lm_ids = gtk_source_language_manager_get_language_ids(lm);
    if (!lm_ids)
	return gtk_text_view_new();
    
    /* search for a language supporting our mime type */
    for (n = 0; !widget && lm_ids[n]; n++) {
	GtkSourceLanguage * src_lang =
	    gtk_source_language_manager_get_language(lm, lm_ids[n]);
	gchar ** mime_types;

	if (src_lang &&
	    (mime_types = gtk_source_language_get_mime_types(src_lang))) {
	    gint k;

	    for (k = 0;
		 mime_types[k] && g_ascii_strcasecmp(mime_types[k], content_type);
		 k++);
	    if (mime_types[k]) {
		GtkSourceBuffer * buffer =
		    gtk_source_buffer_new_with_language(src_lang);
		
		gtk_source_buffer_set_highlight_syntax(buffer, TRUE);
		widget = gtk_source_view_new_with_buffer(buffer);
		g_object_unref(buffer);
	    }
	    g_strfreev(mime_types);
	}
    }
    
    /* fall back to the simple text view if the mime type is not supported */
    return widget ? widget : gtk_text_view_new();
#else /* no GtkSourceview */
    return gtk_text_view_new();
#endif
}

static void
bm_modify_font_from_string(GtkWidget * widget, const char *font)
{
    PangoFontDescription *desc =
        pango_font_description_from_string(balsa_app.message_font);
    gtk_widget_modify_font(widget, desc);
    pango_font_description_free(desc);
}

/* quote_tag:
 * lookup the GtkTextTag for coloring quoted lines of a given level;
 * create the tag if it isn't found.
 *
 * returns NULL if the level is 0 (unquoted)
 */
static GtkTextTag *
quote_tag(GtkTextBuffer * buffer, gint level, gint margin)
{
    GtkTextTag *tag = NULL;

    if (level > 0) {
        GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buffer);
        gchar *name;
	gint q_level;

        /* Modulus the quote level by the max,
         * ie, always have "1 <= quote level <= MAX"
         * this allows cycling through the possible
         * quote colors over again as the quote level
         * grows arbitrarily deep. */
        q_level = (level - 1) % MAX_QUOTED_COLOR;
        name = g_strdup_printf("quote-%d", level);
        tag = gtk_text_tag_table_lookup(table, name);

        if (!tag) {
            tag =
                gtk_text_buffer_create_tag(buffer, name, "foreground-gdk",
                                           &balsa_app.quoted_color[q_level],
					   "left-margin",
                                           BALSA_LEFT_MARGIN
                                           + margin * level,
                                           NULL);
            /* Set a low priority, so we can set both quote color and
             * URL color, and URL color will take precedence. */
            gtk_text_tag_set_priority(tag, 0);
        }
        g_free(name);
    }

    return tag;
}

/* set the gtk_text widget's cursor to a vertical bar
   fix event mask so that pointer motions are reported (if necessary) */
static gboolean
fix_text_widget(GtkWidget *widget, gpointer data)
{
    GdkWindow *w =
        gtk_text_view_get_window(GTK_TEXT_VIEW(widget),
                                 GTK_TEXT_WINDOW_TEXT);
    
    if (data)
        gdk_window_set_events(w,
                              gdk_window_get_events(w) |
                              GDK_POINTER_MOTION_MASK |
                              GDK_LEAVE_NOTIFY_MASK);
    if (!url_cursor_normal || !url_cursor_over_url) {
        url_cursor_normal = gdk_cursor_new(GDK_XTERM);
        url_cursor_over_url = gdk_cursor_new(GDK_HAND2);
    }
    gdk_window_set_cursor(w, url_cursor_normal);

    return FALSE;
}

static void
gtk_widget_destroy_insensitive(GtkWidget * widget)
{
#if GTK_CHECK_VERSION(2, 18, 0)
    if (!gtk_widget_get_sensitive(widget) ||
	GTK_IS_SEPARATOR_MENU_ITEM(widget))
	gtk_widget_destroy(widget);
#else                           /* GTK_CHECK_VERSION(2, 18, 0) */
    if (!GTK_WIDGET_SENSITIVE(widget) ||
	GTK_IS_SEPARATOR_MENU_ITEM(widget))
	gtk_widget_destroy(widget);
#endif                          /* GTK_CHECK_VERSION(2, 18, 0) */
}

static void
structured_phrases_toggle(GtkCheckMenuItem *checkmenuitem, 
			  GtkTextView *textview)
{
    GtkTextTagTable * table;
    GtkTextTag * tag;
    gint phrase_hl =
        GPOINTER_TO_INT(g_object_get_data
                        (G_OBJECT(textview), "phrase-highlight"));
    gboolean new_hl = gtk_check_menu_item_get_active(checkmenuitem);

    table = gtk_text_buffer_get_tag_table(gtk_text_view_get_buffer(textview));
    if (!table || phrase_hl == 0 ||
	(phrase_hl == PHRASE_HIGHLIGHT_ON && new_hl) ||
	(!phrase_hl == PHRASE_HIGHLIGHT_OFF && !new_hl))
	return;

    if ((tag = gtk_text_tag_table_lookup(table, "hp-bold")))
	g_object_set(G_OBJECT(tag), "weight",
		     new_hl ? PANGO_WEIGHT_BOLD : PANGO_WEIGHT_NORMAL,
		     NULL);
    if ((tag = gtk_text_tag_table_lookup(table, "hp-underline")))
	g_object_set(G_OBJECT(tag), "underline",
		     new_hl ? PANGO_UNDERLINE_SINGLE : PANGO_UNDERLINE_NONE,
		     NULL);
    if ((tag = gtk_text_tag_table_lookup(table, "hp-italic")))
	g_object_set(G_OBJECT(tag), "style",
		     new_hl ? PANGO_STYLE_ITALIC : PANGO_STYLE_NORMAL,
		     NULL);

    g_object_set_data(G_OBJECT(textview), "phrase-highlight",
                      GINT_TO_POINTER(new_hl ? PHRASE_HIGHLIGHT_ON :
                                      PHRASE_HIGHLIGHT_OFF));
}

static void
url_copy_cb(GtkWidget * menu_item, message_url_t * uri)
{
    gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY),
			   uri->url, -1);
}

static void
url_open_cb(GtkWidget * menu_item, message_url_t * uri)
{
    handle_url(uri->url);
}

static void
url_send_cb(GtkWidget * menu_item, message_url_t * uri)
{
    BalsaSendmsg * newmsg;

    newmsg = sendmsg_window_compose();
    sendmsg_window_set_field(newmsg, "body", uri->url);
}

static gboolean
text_view_url_popup(GtkTextView *textview, GtkMenu *menu)
{
    GList *url_list = g_object_get_data(G_OBJECT(textview), "url-list");
    message_url_t *url;
    gint x, y;
    GdkModifierType mask;
    GdkWindow *window;
    GtkWidget *menu_item;
    
    /* no url list: no check... */
    if (!url_list)
	return FALSE;

    /* check if we are over an url */
    window = gtk_text_view_get_window(textview, GTK_TEXT_WINDOW_TEXT);
    gdk_window_get_pointer(window, &x, &y, &mask);
    url = find_url(GTK_WIDGET(textview), x, y, url_list);
    if (!url)
	return FALSE;

    /* build a popup to copy or open the URL */
    gtk_container_foreach(GTK_CONTAINER(menu),
                          (GtkCallback)gtk_widget_destroy, NULL);

    menu_item = gtk_menu_item_new_with_label (_("Copy link"));
    g_signal_connect (G_OBJECT (menu_item), "activate",
                      G_CALLBACK (url_copy_cb), (gpointer)url);
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);

    menu_item = gtk_menu_item_new_with_label (_("Open link"));
    g_signal_connect (G_OBJECT (menu_item), "activate",
                      G_CALLBACK (url_open_cb), (gpointer)url);
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);

    menu_item = gtk_menu_item_new_with_label (_("Send link..."));
    g_signal_connect (G_OBJECT (menu_item), "activate",
                      G_CALLBACK (url_send_cb), (gpointer)url);
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);

    gtk_widget_show_all(GTK_WIDGET(menu));

    return TRUE;
}

static void
text_view_populate_popup(GtkTextView *textview, GtkMenu *menu,
                         LibBalsaMessageBody * mime_body)
{
    GtkWidget *menu_item;
    gint phrase_hl;

    gtk_widget_hide_all(GTK_WIDGET(menu));
    if (text_view_url_popup(textview, menu))
	return;

    gtk_container_foreach(GTK_CONTAINER(menu),
                          (GtkCallback)gtk_widget_destroy_insensitive, NULL);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu),
			  gtk_separator_menu_item_new ());
    libbalsa_vfs_fill_menu_by_content_type(menu, "text/plain",
					   G_CALLBACK (balsa_mime_widget_ctx_menu_cb),
					   (gpointer)mime_body);

    menu_item = gtk_menu_item_new_with_label (_("Save..."));
    g_signal_connect (G_OBJECT (menu_item), "activate",
                      G_CALLBACK (balsa_mime_widget_ctx_menu_save), (gpointer)mime_body);
    gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);

    phrase_hl = GPOINTER_TO_INT(g_object_get_data
                                (G_OBJECT(textview), "phrase-highlight"));
    if (phrase_hl != 0) {
	gtk_menu_shell_append(GTK_MENU_SHELL(menu),
			      gtk_separator_menu_item_new ());
	menu_item = gtk_check_menu_item_new_with_label (_("Highlight structured phrases"));
	gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM(menu_item),
					phrase_hl == PHRASE_HIGHLIGHT_ON);
	g_signal_connect (G_OBJECT (menu_item), "toggled",
			  G_CALLBACK (structured_phrases_toggle),
			  (gpointer)textview);
	gtk_menu_shell_append (GTK_MENU_SHELL (menu), menu_item);
    }

    gtk_widget_show_all(GTK_WIDGET(menu));
}


/* -- URL related stuff -- */

static gboolean
store_button_coords(GtkWidget * widget, GdkEventButton * event,
                    gpointer data)
{
    if (event->type == GDK_BUTTON_PRESS && event->button == 1) {
        GdkWindow *window =
            gtk_text_view_get_window(GTK_TEXT_VIEW(widget),
                                     GTK_TEXT_WINDOW_TEXT);

        gdk_window_get_pointer(window, &stored_x, &stored_y, &stored_mask);

        /* compare only shift, ctrl, and mod1-mod5 */
        stored_mask &= STORED_MASK_BITS;
    }
    return FALSE;
}

/* check if we are over an url and change the cursor in this case */
static gboolean
check_over_url(GtkWidget * widget, GdkEventMotion * event,
               GList * url_list)
{
    static gboolean was_over_url = FALSE;
    static message_url_t *current_url = NULL;
    GdkWindow *window;
    message_url_t *url;

    window = gtk_text_view_get_window(GTK_TEXT_VIEW(widget),
                                      GTK_TEXT_WINDOW_TEXT);
    if (event->type == GDK_LEAVE_NOTIFY)
        url = NULL;
    else {
	gint x, y;
	GdkModifierType mask;

        /* FIXME: why can't we just use
         * x = event->x;
         * y = event->y;
         * ??? */
        gdk_window_get_pointer(window, &x, &y, &mask);
        url = find_url(widget, x, y, url_list);
    }

    if (url) {
        if (!url_cursor_normal || !url_cursor_over_url) {
            url_cursor_normal = gdk_cursor_new(GDK_LEFT_PTR);
            url_cursor_over_url = gdk_cursor_new(GDK_HAND2);
        }
        if (!was_over_url) {
            gdk_window_set_cursor(window, url_cursor_over_url);
            was_over_url = TRUE;
        }
        if (url != current_url) {
            pointer_over_url(widget, current_url, FALSE);
            pointer_over_url(widget, url, TRUE);
        }
    } else if (was_over_url) {
        gdk_window_set_cursor(window, url_cursor_normal);
        pointer_over_url(widget, current_url, FALSE);
        was_over_url = FALSE;
    }

    current_url = url;
    return FALSE;
}

/* pointer_over_url:
 * change style of a url and set/clear the status bar.
 */
static void
pointer_over_url(GtkWidget * widget, message_url_t * url, gboolean set)
{
    GtkTextBuffer *buffer;
    GtkTextTagTable *table;
    GtkTextTag *tag;
    GtkTextIter start, end;

    if (!url)
        return;

    buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
    table = gtk_text_buffer_get_tag_table(buffer);
    tag = gtk_text_tag_table_lookup(table, "emphasize");

    gtk_text_buffer_get_iter_at_offset(buffer, &start, url->start);
    gtk_text_buffer_get_iter_at_offset(buffer, &end, url->end);
    
    if (set) {
        gtk_text_buffer_apply_tag(buffer, tag, &start, &end);
        bm_widget_on_url(url->url);
    } else {
        gtk_text_buffer_remove_tag(buffer, tag, &start, &end);
        bm_widget_on_url(NULL);
    }
}

/* After wrapping the buffer, populate the start and end offsets for
 * each url. */
static void
prepare_url_offsets(GtkTextBuffer * buffer, GList * url_list)
{
    GtkTextTagTable *table = gtk_text_buffer_get_tag_table(buffer);
    GtkTextTag *url_tag = gtk_text_tag_table_lookup(table, "url");

    for (; url_list; url_list = g_list_next(url_list)) {
        message_url_t *url = url_list->data;
        GtkTextIter iter;

        gtk_text_buffer_get_iter_at_mark(buffer, &iter, url->end_mark);
        url->end = gtk_text_iter_get_offset(&iter);
#ifdef BUG_102711_FIXED
        gtk_text_iter_backward_to_tag_toggle(&iter, url_tag);
#else
        while (gtk_text_iter_backward_char(&iter))
            if (gtk_text_iter_begins_tag(&iter, url_tag))
                break;
#endif                          /* BUG_102711_FIXED */
        url->start = gtk_text_iter_get_offset(&iter);
    }
}

static void
url_found_cb(GtkTextBuffer * buffer, GtkTextIter * iter,
             const gchar * buf, guint len, gpointer data)
{
    GList **url_list = data;
    message_url_t *url_found;

    url_found = g_new(message_url_t, 1);
    url_found->end_mark =
        gtk_text_buffer_create_mark(buffer, NULL, iter, TRUE);
    url_found->url = g_strndup(buf, len);       /* gets freed later... */
    *url_list = g_list_append(*url_list, url_found);
}

/* if the mouse button was released over an URL, and the mouse hasn't
 * moved since the button was pressed, try to call the URL */
static gboolean
check_call_url(GtkWidget * widget, GdkEventButton * event,
               GList * url_list)
{
    gint x, y;
    message_url_t *url;

    if (event->type != GDK_BUTTON_RELEASE || event->button != 1)
        return FALSE;

    x = event->x;
    y = event->y;
    /* 2-pixel motion tolerance */
    if (abs(x - stored_x) <= 2 && abs(y - stored_y) <= 2
        && (event->state & STORED_MASK_BITS) == stored_mask) {
        url = find_url(widget, x, y, url_list);
        if (url)
            handle_url(url->url);
    }
    return FALSE;
}

/* find_url:
 * look in widget at coordinates x, y for a URL in url_list.
 */
static message_url_t *
find_url(GtkWidget * widget, gint x, gint y, GList * url_list)
{
    GtkTextIter iter;
    gint offset;
    message_url_t *url;

    gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(widget),
                                          GTK_TEXT_WINDOW_TEXT,
                                          x, y, &x, &y);
    gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(widget), &iter, x, y);
    offset = gtk_text_iter_get_offset(&iter);

    for (; url_list; url_list = g_list_next(url_list)) {
        url = (message_url_t *) url_list->data;
        if (url->start <= offset && offset < url->end)
            return url;
    }

    return NULL;
}

static gboolean
statusbar_pop(gpointer data)
{
    GtkStatusbar *statusbar;
    guint context_id;

    gdk_threads_enter();

    statusbar = GTK_STATUSBAR(balsa_app.main_window->statusbar);
    context_id = gtk_statusbar_get_context_id(statusbar, "BalsaMimeWidget message");
    gtk_statusbar_pop(statusbar, context_id);

    gdk_threads_leave();
    return FALSE;
}

#define SCHEDULE_BAR_REFRESH() \
    g_timeout_add_seconds(5, statusbar_pop, NULL);

static void
handle_url(const gchar * url)
{
    if (!g_ascii_strncasecmp(url, "mailto:", 7)) {
        BalsaSendmsg *snd = sendmsg_window_compose();
        sendmsg_window_process_url(url + 7, sendmsg_window_set_field, snd);
    } else {
        GtkStatusbar *statusbar;
        guint context_id;
        gchar *notice = g_strdup_printf(_("Calling URL %s..."), url);
        GError *err = NULL;

        statusbar = GTK_STATUSBAR(balsa_app.main_window->statusbar);
        context_id =
            gtk_statusbar_get_context_id(statusbar,
                                         "BalsaMimeWidget message");
        gtk_statusbar_push(statusbar, context_id, notice);
        SCHEDULE_BAR_REFRESH();
        g_free(notice);
        gtk_show_uri(NULL, url, gtk_get_current_event_time(), &err);
        if (err) {
            balsa_information(LIBBALSA_INFORMATION_WARNING,
                              _("Error showing %s: %s\n"),
                              url, err->message);
            g_error_free(err);
        }
    }
}

static void
free_url_list(GList * url_list)
{
    GList *list;

    for (list = url_list; list; list = g_list_next(list)) {
        message_url_t *url_data = (message_url_t *) list->data;

        g_free(url_data->url);
        g_free(url_data);
    }
    g_list_free(url_list);
}

/* --- Hacker's Jargon highlighting --- */
#define UNICHAR_PREV(p)  g_utf8_get_char(g_utf8_prev_char(p))

static void
phrase_highlight(GtkTextBuffer * buffer, const gchar * id, gunichar tag_char,
		 const gchar * property, gint value)
{
    GtkTextTag *tag = NULL;
    gchar * buf_chars;
    gchar * utf_start;
    GtkTextIter iter_start;
    GtkTextIter iter_end;

    /* get the utf8 buffer */
    gtk_text_buffer_get_start_iter(buffer, &iter_start);
    gtk_text_buffer_get_end_iter(buffer, &iter_end);
    buf_chars = gtk_text_buffer_get_text(buffer, &iter_start, &iter_end, TRUE);
    g_return_if_fail(buf_chars != NULL);

    /* find the tag char in the text and scan the buffer for
       <buffer start or whitespace><tag char><alnum><any text><alnum><tagchar>
       <whitespace, punctuation or buffer end> */
    utf_start = g_utf8_strchr(buf_chars, -1, tag_char);
    while (utf_start) {
	gchar * s_next = g_utf8_next_char(utf_start);

	if ((utf_start == buf_chars || g_unichar_isspace(UNICHAR_PREV(utf_start))) &&
	    *s_next != '\0' && g_unichar_isalnum(g_utf8_get_char(s_next))) {
	    gchar * utf_end;
	    gchar * line_end;
	    gchar * e_next;

	    /* found a proper start sequence - find the end or eject */
	    if (!(utf_end = g_utf8_strchr(s_next, -1, tag_char))) {
                g_free(buf_chars);
		return;
            }
	    line_end = g_utf8_strchr(s_next, -1, '\n');
	    e_next = g_utf8_next_char(utf_end);
	    while (!g_unichar_isalnum(UNICHAR_PREV(utf_end)) ||
		   !(*e_next == '\0' || 
		     g_unichar_isspace(g_utf8_get_char(e_next)) ||
		     g_unichar_ispunct(g_utf8_get_char(e_next)))) {
		if (!(utf_end = g_utf8_strchr(e_next, -1, tag_char))) {
                    g_free(buf_chars);
		    return;
                }
		e_next = g_utf8_next_char(utf_end);
	    }
	    
	    /* insert the tag if there is no line break */
	    if (!line_end || line_end >= e_next) {
		if (!tag)
		    tag = gtk_text_buffer_create_tag(buffer, id, property, value, NULL);
		gtk_text_buffer_get_iter_at_offset(buffer, &iter_start,
						   g_utf8_pointer_to_offset(buf_chars, utf_start));
		gtk_text_buffer_get_iter_at_offset(buffer, &iter_end,
						   g_utf8_pointer_to_offset(buf_chars, e_next));
		gtk_text_buffer_apply_tag(buffer, tag, &iter_start, &iter_end);
		
		/* set the next start properly */
		utf_start = *e_next ? g_utf8_strchr(e_next, -1, tag_char) : NULL;
	    } else
		utf_start = *s_next ? g_utf8_strchr(s_next, -1, tag_char) : NULL;
	} else
	    /* no start sequence, find the next start tag char */
	    utf_start = *s_next ? g_utf8_strchr(s_next, -1, tag_char) : NULL;
    }
    g_free(buf_chars);
}

/* --- citation bar stuff --- */
static void
destroy_cite_bars(GList * cite_bars)
{
    /* note: the widgets are destroyed by the text view */
    g_list_foreach(cite_bars, (GFunc) g_free, NULL);
    g_list_free(cite_bars);
}

typedef struct {
    GtkTextView * view;
    GtkTextBuffer * buffer;
    gint dimension;
} cite_bar_draw_mode_t;


static void
draw_cite_bar_real(cite_bar_t * bar, cite_bar_draw_mode_t * draw_mode)
{
    GdkRectangle location;
    gint x_pos;
    gint y_pos;
    gint height;

    /* initialise iters if we don't have the widget yet */
    if (!bar->bar) {
	gtk_text_buffer_get_iter_at_offset(draw_mode->buffer, &bar->start_iter,
					   bar->start_offs);
	gtk_text_buffer_get_iter_at_offset(draw_mode->buffer, &bar->end_iter,
					   bar->end_offs);
    }

    /* get the locations */
    gtk_text_view_get_iter_location(draw_mode->view, &bar->start_iter, &location);
    gtk_text_view_buffer_to_window_coords(draw_mode->view, GTK_TEXT_WINDOW_TEXT,
					  location.x, location.y,
					  &x_pos, &y_pos);
    gtk_text_view_get_iter_location(draw_mode->view, &bar->end_iter, &location);
    gtk_text_view_buffer_to_window_coords(draw_mode->view, GTK_TEXT_WINDOW_TEXT,
					  location.x, location.y,
					  &x_pos, &height);
    height -= y_pos;

    /* add a new widget if necessary */
    if (bar->bar == NULL) {
	bar->bar = balsa_cite_bar_new(height, bar->depth, draw_mode->dimension);
	gtk_widget_modify_fg(bar->bar, GTK_STATE_NORMAL,
			     &balsa_app.quoted_color[(bar->depth - 1) % MAX_QUOTED_COLOR]);
        gtk_widget_modify_bg(bar->bar, GTK_STATE_NORMAL,
                             &gtk_widget_get_style(GTK_WIDGET
                                                   (draw_mode->view))->
                             base[GTK_STATE_NORMAL]);
	gtk_widget_show(bar->bar);
	gtk_text_view_add_child_in_window(draw_mode->view, bar->bar,
					  GTK_TEXT_WINDOW_TEXT, 0, y_pos);
    } else if (bar->y_pos != y_pos || bar->height != height) {
	/* shift/resize existing widget */
	balsa_cite_bar_resize(BALSA_CITE_BAR(bar->bar), height);
	gtk_text_view_move_child(draw_mode->view, bar->bar, 0, y_pos);
    }

    /* remember current values */
    bar->y_pos = y_pos;
    bar->height = height;
}


static gboolean
draw_cite_bars(GtkWidget * widget, GdkEventExpose *event, GList * cite_bars)
{
    cite_bar_draw_mode_t draw_mode;

    draw_mode.view = GTK_TEXT_VIEW(widget);
    draw_mode.buffer = gtk_text_view_get_buffer(draw_mode.view);
    draw_mode.dimension =
	GPOINTER_TO_INT(g_object_get_data(G_OBJECT(widget), "cite-margin"));
    g_list_foreach(cite_bars, (GFunc)draw_cite_bar_real, &draw_mode);
    return FALSE;
}


/* --- HTML related functions -- */
static void
bm_widget_on_url(const gchar *url)
{
    GtkStatusbar *statusbar;
    guint context_id;

    statusbar = GTK_STATUSBAR(balsa_app.main_window->statusbar);
    context_id = gtk_statusbar_get_context_id(statusbar, "BalsaMimeWidget URL");

    if( url ) {
        gtk_statusbar_push(statusbar, context_id, url);
        SCHEDULE_BAR_REFRESH();
    } else 
        gtk_statusbar_pop(statusbar, context_id);
}

#ifdef HAVE_HTML_WIDGET
static void
bm_zoom_in(BalsaMessage * bm)
{
    balsa_message_zoom(bm, 1);
}

static void
bm_zoom_out(BalsaMessage * bm)
{
    balsa_message_zoom(bm, -1);
}

static void
bm_zoom_reset(BalsaMessage * bm)
{
    balsa_message_zoom(bm, 0);
}

static void
bmwt_populate_popup_menu(BalsaMessage * bm,
                         GtkWidget    * html,
                         GtkMenu      * menu)
{
    GtkWidget *menuitem;
    gpointer mime_body = g_object_get_data(G_OBJECT(html), "mime-body");

    menuitem = gtk_image_menu_item_new_from_stock(GTK_STOCK_ZOOM_IN, NULL);
    g_signal_connect_swapped(G_OBJECT(menuitem), "activate",
                             G_CALLBACK(bm_zoom_in), bm);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);

    menuitem =
        gtk_image_menu_item_new_from_stock(GTK_STOCK_ZOOM_OUT, NULL);
    g_signal_connect_swapped(G_OBJECT(menuitem), "activate",
                             G_CALLBACK(bm_zoom_out), bm);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);

    menuitem =
        gtk_image_menu_item_new_from_stock(GTK_STOCK_ZOOM_100, NULL);
    g_signal_connect_swapped(G_OBJECT(menuitem), "activate",
                             G_CALLBACK(bm_zoom_reset), bm);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);

    menuitem = gtk_separator_menu_item_new();
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);

    libbalsa_vfs_fill_menu_by_content_type(GTK_MENU(menu), "text/html",
                                           G_CALLBACK
                                           (balsa_mime_widget_ctx_menu_cb),
                                           mime_body);

    menuitem = gtk_menu_item_new_with_label(_("Save..."));
    g_signal_connect(G_OBJECT(menuitem), "activate",
                     G_CALLBACK(balsa_mime_widget_ctx_menu_save),
                     mime_body);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);

    menuitem = gtk_separator_menu_item_new();
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);

    menuitem =
        gtk_image_menu_item_new_from_stock(GTK_STOCK_PRINT, NULL);
    g_signal_connect_swapped(G_OBJECT(menuitem), "activate",
                             G_CALLBACK(libbalsa_html_print), html);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), menuitem);
    gtk_widget_set_sensitive(menuitem, libbalsa_html_can_print(html));
}

static gboolean
balsa_gtk_html_popup(GtkWidget * html, BalsaMessage * bm)
{
    GtkWidget *menu;

    menu = gtk_menu_new();
    bmwt_populate_popup_menu(bm, html, GTK_MENU(menu));

    gtk_widget_show_all(menu);
    g_object_ref_sink(menu);
    gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL,
                   0, gtk_get_current_event_time());
    g_object_unref(menu);

    return TRUE;
}

static gboolean
balsa_gtk_html_button_press_cb(GtkWidget * html, GdkEventButton * event,
                               BalsaMessage * bm)
{
    return ((event->type == GDK_BUTTON_PRESS && event->button == 3)
            ? balsa_gtk_html_popup(html, bm) : FALSE);
}

static void
bmwt_populate_popup_cb(GtkWidget * widget, GtkMenu * menu, gpointer data)
{
    BalsaMessage *bm =
        g_object_get_data(G_OBJECT(widget), "balsa-message");
    GtkWidget *html = data;

    /* Remove WebKitWebView's items--they're irrelevant and confusing */
    gtk_container_foreach(GTK_CONTAINER(menu),
                          (GtkCallback) gtk_widget_destroy, NULL);
    bmwt_populate_popup_menu(bm, html, menu);
    gtk_widget_show_all(GTK_WIDGET(menu));
}

BalsaMimeWidget *
bm_widget_new_html(BalsaMessage * bm, LibBalsaMessageBody * mime_body)
{
    BalsaMimeWidget *mw = g_object_new(BALSA_TYPE_MIME_WIDGET, NULL);
    GtkWidget *widget;

    mw->widget =
        libbalsa_html_new(mime_body,
                          (LibBalsaHtmlCallback) bm_widget_on_url,
                          (LibBalsaHtmlCallback) handle_url);
    g_object_set_data(G_OBJECT(mw->widget), "mime-body", mime_body);

    g_signal_connect(G_OBJECT(mw->widget), "key_press_event",
                     G_CALLBACK(balsa_mime_widget_key_press_event), bm);
    if ((widget = libbalsa_html_popup_menu_widget(mw->widget))) {
        g_object_set_data(G_OBJECT(widget), "balsa-message", bm);
        g_signal_connect(widget, "populate-popup",
                         G_CALLBACK(bmwt_populate_popup_cb), mw->widget);
    } else {
        g_signal_connect(mw->widget, "button-press-event",
                         G_CALLBACK(balsa_gtk_html_button_press_cb), bm);
        g_signal_connect(mw->widget, "popup-menu",
                         G_CALLBACK(balsa_gtk_html_popup), bm);
    }

    return mw;
}
#endif /* defined HAVE_HTML_WIDGET */

#define TABLE_ATTACH(t,str,label) \
    if(str) { GtkWidget *lbl = gtk_label_new(label);              \
        gtk_table_attach(t, lbl, 0, 1, row, row+1,                \
                         GTK_FILL, GTK_FILL, 4, 2);               \
        gtk_misc_set_alignment(GTK_MISC(lbl), 1.0, 0.0); \
        gtk_table_attach(table, lbl=gtk_label_new(str), 1, 2, row, row+1, \
                         GTK_FILL|GTK_EXPAND, GTK_FILL|GTK_EXPAND, 4, 2); \
        gtk_misc_set_alignment(GTK_MISC(lbl), 0.0, 0.0); \
        row++;                                                    \
    }

static BalsaMimeWidget *
bm_widget_new_vcard(BalsaMessage *bm, LibBalsaMessageBody *mime_body,
                    gchar *ptr, size_t len)
{
    BalsaMimeWidget *mw = g_object_new(BALSA_TYPE_MIME_WIDGET, NULL);
    LibBalsaAddress * addr =
	libbalsa_address_new_from_vcard(ptr, mime_body->charset ? mime_body->charset : "us-ascii");
    GtkTable *table;
    GtkWidget *w;
    int row = 1;

    if (!addr)
        return NULL;

    mw->widget = gtk_table_new(10, 2, FALSE);
    table = (GtkTable*)mw->widget;
        
    gtk_table_attach_defaults(table, w=gtk_label_new(_("Address")),
                              0, 1, 0, 1);
    gtk_misc_set_alignment(GTK_MISC(w), 1.0, 0.0);
    w = gtk_button_new_with_mnemonic(_("S_tore"));
    /* FIXME: Connect signal */
    gtk_table_attach(table, w, 1, 2, 0, 1, 0,0,  2, 2);
    g_signal_connect_swapped(w, "clicked",
                             G_CALLBACK(balsa_store_address), addr);
    g_object_weak_ref(G_OBJECT(mw), (GWeakNotify)g_object_unref, addr);
 

    TABLE_ATTACH(table, addr->full_name,    _("Full Name"));
    TABLE_ATTACH(table, addr->nick_name,    _("Nick Name"));
    TABLE_ATTACH(table, addr->first_name,   _("First Name"));
    TABLE_ATTACH(table, addr->last_name,    _("Last Name"));
    TABLE_ATTACH(table, addr->organization, _("Organization"));
    if(addr->address_list) {
        TABLE_ATTACH(table, addr->address_list->data, _("Email Address"));
    }
        
    g_object_set_data(G_OBJECT(mw->widget), "mime-body", mime_body);
    gtk_widget_show_all(mw->widget);
    return mw;
}