From: Dirk-Jan C. Binnema Date: Wed, 17 May 2006 15:48:04 +0000 (+0000) Subject: * implement TnyMsgView using the new GtkHTML-based TnyStreamIface X-Git-Tag: git_migration_finished~4768 X-Git-Url: http://git.maemo.org/git/?p=modest;a=commitdiff_plain;h=344825f1b15c5dc19865940dce255dc1555de8bd * implement TnyMsgView using the new GtkHTML-based TnyStreamIface This means that HTML-emails are now supported as well. pmo-trunk-r82 --- diff --git a/src/modest-tny-msg-view.c b/src/modest-tny-msg-view.c index 409a73b..511078a 100644 --- a/src/modest-tny-msg-view.c +++ b/src/modest-tny-msg-view.c @@ -3,12 +3,46 @@ /* insert (c)/licensing information) */ #include "modest-tny-msg-view.h" -/* include other impl specific header files */ +#include "modest-tny-stream-gtkhtml.h" +#include +#include +#include +#include +#include /* 'private'/'protected' functions */ -static void modest_tny_msg_view_class_init (ModestTnyMsgViewClass *klass); -static void modest_tny_msg_view_init (ModestTnyMsgView *obj); -static void modest_tny_msg_view_finalize (GObject *obj); +static void modest_tny_msg_view_class_init (ModestTnyMsgViewClass *klass); +static void modest_tny_msg_view_init (ModestTnyMsgView *obj); +static void modest_tny_msg_view_finalize (GObject *obj); + + +static GSList* get_url_matches (GString *txt); + + +/* + * we need these regexps to find URLs in plain text e-mails + */ +typedef struct _UrlMatchPattern UrlMatchPattern; +struct _UrlMatchPattern { + gchar *regex; + regex_t *preg; + gchar *prefix; +}; +#define MAIL_VIEWER_URL_MATCH_PATTERNS {\ + { "(file|http|ftp|https)://[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]+[-A-Za-z0-9_$%&=?/~#]",\ + NULL, NULL },\ + { "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\ + NULL, "http://" },\ + { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\ + NULL, "ftp://" },\ + { "(voipto|callto|chatto|jabberto|xmpp):[-_a-z@0-9.\\+]+", \ + NULL, NULL}, \ + { "mailto:[-_a-z0-9.\\+]+@[-_a-z0-9.]+", \ + NULL, NULL},\ + { "[-_a-z0-9.\\+]+@[-_a-z0-9.]+",\ + NULL, "mailto:"}\ + } + /* list my signals */ enum { @@ -19,7 +53,7 @@ enum { typedef struct _ModestTnyMsgViewPrivate ModestTnyMsgViewPrivate; struct _ModestTnyMsgViewPrivate { - GtkWidget *text_view; + GtkWidget *gtkhtml; }; #define MODEST_TNY_MSG_VIEW_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), \ MODEST_TYPE_TNY_MSG_VIEW, \ @@ -63,29 +97,39 @@ modest_tny_msg_view_class_init (ModestTnyMsgViewClass *klass) gobject_class->finalize = modest_tny_msg_view_finalize; g_type_class_add_private (gobject_class, sizeof(ModestTnyMsgViewPrivate)); - - /* signal definitions go here, e.g.: */ -/* signals[MY_SIGNAL_1] = */ -/* g_signal_new ("my_signal_1",....); */ -/* signals[MY_SIGNAL_2] = */ -/* g_signal_new ("my_signal_2",....); */ -/* etc. */ } static void modest_tny_msg_view_init (ModestTnyMsgView *obj) { ModestTnyMsgViewPrivate *priv; + + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(obj), + GTK_POLICY_AUTOMATIC, + GTK_POLICY_AUTOMATIC); + priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(obj); + priv->gtkhtml = gtk_html_new(); + gtk_html_set_editable (GTK_HTML(priv->gtkhtml), FALSE); + gtk_html_allow_selection (GTK_HTML(priv->gtkhtml), TRUE); + gtk_html_set_caret_mode (GTK_HTML(priv->gtkhtml), TRUE); + gtk_html_set_blocking (GTK_HTML(priv->gtkhtml), FALSE); + gtk_html_set_images_blocking (GTK_HTML(priv->gtkhtml), FALSE); - priv->text_view = NULL; + gtk_widget_show (priv->gtkhtml); + gtk_container_add (GTK_CONTAINER(obj), priv->gtkhtml); } + static void modest_tny_msg_view_finalize (GObject *obj) -{ - /* no need to unref the text_view */ +{ + ModestTnyMsgViewPrivate *priv; + priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(obj); + + if (priv->gtkhtml) + g_object_unref (G_OBJECT(priv->gtkhtml)); } GtkWidget* @@ -93,24 +137,292 @@ modest_tny_msg_view_new (TnyMsgIface *msg) { GObject *obj; ModestTnyMsgView* self; - ModestTnyMsgViewPrivate *priv; obj = G_OBJECT(g_object_new(MODEST_TYPE_TNY_MSG_VIEW, NULL)); self = MODEST_TNY_MSG_VIEW(obj); + + if (msg) + modest_tny_msg_view_set_message (self, msg); + + return GTK_WIDGET(self); +} + + + + +typedef struct { + guint offset; + guint len; + const gchar* prefix; +} url_match_t; + + +static void +hyperlinkify_plain_text (GString *txt) +{ + GSList *cursor; + GSList *match_list = get_url_matches (txt); + + /* we will work backwards, so the offsets stay valid */ + for (cursor = match_list; cursor; cursor = cursor->next) { + + url_match_t *match = (url_match_t*) cursor->data; + gchar *url = g_strndup (txt->str + match->offset, match->len); + gchar *repl = NULL; /* replacement */ + + /* the prefix is NULL: use the one that is already there */ + repl = g_strdup_printf ("%s", + match->prefix ? match->prefix : "", url, url); + + /* replace the old thing with our hyperlink + * replacement thing */ + g_string_erase (txt, match->offset, match->len); + g_string_insert (txt, match->offset, repl); + + g_free (url); + g_free (repl); + + } + g_slist_free (match_list); +} + + + +static gchar * +convert_to_html (gchar *data) +{ + int i; + gboolean first_space = TRUE; + GString *html; + gsize len; + + if (!data) + return NULL; + + len = strlen (data); + html = g_string_sized_new (len + 100); /* just a guess... */ + + g_string_append_printf (html, + "" + "" + "" + "" + ""); + + /* replace with special html chars where needed*/ + for (i = 0; i != len; ++i) { + char kar = data[i]; + switch (kar) { + + case 0: break; /* ignore embedded \0s */ + case '<' : g_string_append (html, "<"); break; + case '>' : g_string_append (html, ">"); break; + case '&' : g_string_append (html, """); break; + case '\n': g_string_append (html, "
\n"); break; + default: + if (kar == ' ') { + g_string_append (html, first_space ? " " : " "); + first_space = FALSE; + } else if (kar == '\t') + g_string_append (html, "    "); + else { + int charnum = 0; + first_space = TRUE; + /* optimization trick: accumulate 'normal' chars, then copy */ + do { + kar = data [++charnum + i]; + + } while ((i + charnum < len) && + (kar > '>' || (kar != '<' && kar != '>' + && kar != '&' && kar != ' ' + && kar != '\n' && kar != '\t'))); + g_string_append_len (html, &data[i], charnum); + i += (charnum - 1); + } + } + } + + g_string_append (html, "
"); + hyperlinkify_plain_text (html); + + return g_string_free (html, FALSE); +} + + + + +static gint +cmp_offsets_reverse (const url_match_t *match1, const url_match_t *match2) +{ + return match2->offset - match1->offset; +} + + + +/* + * check if the match is inside an existing match... */ +static void +chk_partial_match (const url_match_t *match, int* offset) +{ + if (*offset >= match->offset && *offset < match->offset + match->len) + *offset = -1; +} + +static GSList* +get_url_matches (GString *txt) +{ + regmatch_t rm; + int rv, i, offset = 0; + GSList *match_list = NULL; + + static UrlMatchPattern patterns[] = MAIL_VIEWER_URL_MATCH_PATTERNS; + const size_t pattern_num = sizeof(patterns)/sizeof(UrlMatchPattern); + + /* initalize the regexps */ + for (i = 0; i != pattern_num; ++i) { + patterns[i].preg = g_new0 (regex_t,1); + g_assert(regcomp (patterns[i].preg, patterns[i].regex, + REG_ICASE|REG_EXTENDED|REG_NEWLINE) == 0); + } + /* find all the matches */ + for (i = 0; i != pattern_num; ++i) { + offset = 0; + while (1) { + int test_offset; + if ((rv = regexec (patterns[i].preg, txt->str + offset, 1, &rm, 0)) != 0) { + g_assert (rv == REG_NOMATCH); /* this should not happen */ + break; /* try next regexp */ + } + if (rm.rm_so == -1) + break; + + /* FIXME: optimize this */ + /* to avoid partial matches on something that was already found... */ + /* check_partial_match will put -1 in the data ptr if that is the case */ + test_offset = offset + rm.rm_so; + g_slist_foreach (match_list, (GFunc)chk_partial_match, &test_offset); + + /* make a list of our matches ( tupels)*/ + if (test_offset != -1) { + url_match_t *match = g_new (url_match_t,1); + match->offset = offset + rm.rm_so; + match->len = rm.rm_eo - rm.rm_so; + match->prefix = patterns[i].prefix; + match_list = g_slist_prepend (match_list, match); + } + offset += rm.rm_eo; + } + } + + for (i = 0; i != pattern_num; ++i) { + regfree (patterns[i].preg); + g_free (patterns[i].preg); + } /* don't free patterns itself -- it's static */ + + /* now sort the list, so the matches are in reverse order of occurence. + * that way, we can do the replacements starting from the end, so we don't need + * to recalculate the offsets + */ + match_list = g_slist_sort (match_list, + (GCompareFunc)cmp_offsets_reverse); + return match_list; +} + +static gboolean +fill_gtkhtml_with_txt (GtkHTML* gtkhtml, gchar* txt) +{ + gchar *html; + + g_return_val_if_fail (gtkhtml, FALSE); + g_return_val_if_fail (txt, FALSE); + + html = convert_to_html (txt); + gtk_html_load_from_string (gtkhtml, html, strlen(html)); + g_free (html); + + return TRUE; +} + + + +static TnyMsgMimePartIface * +find_body_part (TnyMsgIface *msg, const gchar *mime_type) +{ + TnyMsgMimePartIface *part = NULL; + GList *parts; + + g_return_val_if_fail (msg, NULL); + g_return_val_if_fail (mime_type, NULL); + + parts = (GList*) tny_msg_iface_get_parts (msg); + while (parts && !part) { + part = TNY_MSG_MIME_PART_IFACE(parts->data); + if (!tny_msg_mime_part_iface_content_type_is (part, mime_type)) + part = NULL; + parts = parts->next; + } + + return part; +} + +static gboolean +set_html_message (ModestTnyMsgView *self, TnyMsgMimePartIface *tny_body) +{ + TnyStreamIface *gtkhtml_stream; + ModestTnyMsgViewPrivate *priv; + + g_return_val_if_fail (self, FALSE); + g_return_val_if_fail (tny_body, FALSE); + priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self); - gtk_scrolled_window_set_policy(self, GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC); + gtkhtml_stream = + TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new(GTK_HTML(priv->gtkhtml))); - priv->text_view = gtk_text_view_new (); - gtk_text_view_set_editable (GTK_TEXT_VIEW(priv->text_view), FALSE); - gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW(priv->text_view), FALSE); + tny_stream_iface_reset (gtkhtml_stream); + tny_msg_mime_part_iface_decode_to_stream (tny_body, gtkhtml_stream); + tny_stream_iface_reset (gtkhtml_stream); - gtk_container_add (GTK_CONTAINER(self), priv->text_view); + g_object_unref (G_OBJECT(gtkhtml_stream)); - if (msg) - modest_tny_msg_view_set_message (self, msg); + return TRUE; +} - return GTK_WIDGET(self); + +/* this is a hack --> we use the tny_text_buffer_stream to + * get the message text, then write to gtkhtml 'by hand' */ +static gboolean +set_text_message (ModestTnyMsgView *self, TnyMsgMimePartIface *tny_body) +{ + GtkTextBuffer *buf; + GtkTextIter begin, end; + TnyStreamIface* txt_stream; + gchar *txt; + ModestTnyMsgViewPrivate *priv; + + g_return_val_if_fail (self, FALSE); + g_return_val_if_fail (tny_body, FALSE); + + priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self); + + buf = gtk_text_buffer_new (NULL); + txt_stream = TNY_STREAM_IFACE(tny_text_buffer_stream_new (buf)); + + tny_stream_iface_reset (txt_stream); + tny_msg_mime_part_iface_decode_to_stream (tny_body, txt_stream); + tny_stream_iface_reset (txt_stream); + + gtk_text_buffer_get_bounds (buf, &begin, &end); + txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE); + + fill_gtkhtml_with_txt (GTK_HTML(priv->gtkhtml), txt); + + g_object_unref (G_OBJECT(txt_stream)); + g_object_unref (G_OBJECT(buf)); + + g_free (txt); + return TRUE; } @@ -118,38 +430,34 @@ modest_tny_msg_view_new (TnyMsgIface *msg) void modest_tny_msg_view_set_message (ModestTnyMsgView *self, TnyMsgIface *msg) { + TnyMsgMimePartIface *body; ModestTnyMsgViewPrivate *priv; - GtkTextBuffer *buf; - GList *parts; - TnyStreamIface *stream; - - g_return_if_fail (self); + g_return_if_fail (self); + priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self); - buf = gtk_text_view_get_buffer (GTK_TEXT_VIEW(priv->text_view)); - /* clear the message view */ - gtk_text_buffer_set_text (buf, "", 0); - - /* if msg is NULL, do nothing else */ if (!msg) { + fill_gtkhtml_with_txt (GTK_HTML(priv->gtkhtml), ""); return; } - - /* otherwise... find the body part */ - stream = TNY_STREAM_IFACE(tny_text_buffer_stream_new(buf)); - parts = (GList*) tny_msg_iface_get_parts (msg); - while (parts) { - TnyMsgMimePartIface *part = - TNY_MSG_MIME_PART_IFACE(parts->data); - - if (tny_msg_mime_part_iface_content_type_is (part, "text/plain")) { - tny_stream_iface_reset (stream); - tny_msg_mime_part_iface_write_to_stream (part, stream); - tny_stream_iface_reset (stream); - break; - } - parts = parts->next; + body = find_body_part (msg, "text/html"); + if (body) { + set_html_message (self, body); + return; + } + + body = find_body_part (msg, "text/plain"); + if (body) { + set_text_message (self, body); + return; } + + /* hmmmmm */ + fill_gtkhtml_with_txt (GTK_HTML(priv->gtkhtml), + _("Unsupported message type")); } + + +