2 * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2005 Hiroyuki Yamamoto
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
27 #include <glib/gi18n.h>
29 #include <gdk/gdkkeysyms.h>
30 #include <gtk/gtkvbox.h>
31 #include <gtk/gtkscrolledwindow.h>
32 #include <gtk/gtksignal.h>
39 #include "summaryview.h"
40 #include "procheader.h"
41 #include "prefs_common.h"
49 #include "addressbook.h"
50 #include "displayheader.h"
53 #include "alertpanel.h"
55 #include "image_viewer.h"
68 static GdkColor quote_colors[3] = {
69 {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
70 {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
71 {(gulong)0, (gushort)0, (gushort)0, (gushort)0}
74 static GdkColor signature_color = {
81 static GdkColor uri_color = {
88 static GdkColor emphasis_color = {
96 static GdkColor error_color = {
105 static GdkCursor *hand_cursor = NULL;
106 static GdkCursor *text_cursor = NULL;
108 #define TEXTVIEW_STATUSBAR_PUSH(textview, str) \
110 gtk_statusbar_push(GTK_STATUSBAR(textview->messageview->statusbar), \
111 textview->messageview->statusbar_cid, str); \
114 #define TEXTVIEW_STATUSBAR_POP(textview) \
116 gtk_statusbar_pop(GTK_STATUSBAR(textview->messageview->statusbar), \
117 textview->messageview->statusbar_cid); \
120 static void textview_show_ertf (TextView *textview,
122 CodeConverter *conv);
123 static void textview_add_part (TextView *textview,
125 static void textview_add_parts (TextView *textview,
127 static void textview_write_body (TextView *textview,
129 static void textview_show_html (TextView *textview,
131 CodeConverter *conv);
133 static void textview_write_line (TextView *textview,
135 CodeConverter *conv);
136 static void textview_write_link (TextView *textview,
139 CodeConverter *conv);
141 static GPtrArray *textview_scan_header (TextView *textview,
143 static void textview_show_header (TextView *textview,
146 static gint textview_key_pressed (GtkWidget *widget,
149 static gboolean textview_motion_notify (GtkWidget *widget,
150 GdkEventMotion *motion,
152 static gboolean textview_leave_notify (GtkWidget *widget,
153 GdkEventCrossing *event,
155 static gboolean textview_visibility_notify (GtkWidget *widget,
156 GdkEventVisibility *event,
158 static void textview_uri_update (TextView *textview,
161 static gboolean textview_get_uri_range (TextView *textview,
164 GtkTextIter *start_iter,
165 GtkTextIter *end_iter);
166 static RemoteURI *textview_get_uri_from_range (TextView *textview,
169 GtkTextIter *start_iter,
170 GtkTextIter *end_iter);
171 static RemoteURI *textview_get_uri (TextView *textview,
174 static gboolean textview_uri_button_pressed (GtkTextTag *tag,
180 static void textview_smooth_scroll_do (TextView *textview,
184 static void textview_smooth_scroll_one_line (TextView *textview,
186 static gboolean textview_smooth_scroll_page (TextView *textview,
189 static gboolean textview_uri_security_check (TextView *textview,
191 static void textview_uri_list_remove_all (GSList *uri_list);
193 static void open_uri_cb (TextView *textview,
196 static void copy_uri_cb (TextView *textview,
199 static void add_uri_to_addrbook_cb (TextView *textview,
202 static void mail_to_uri_cb (TextView *textview,
205 static void copy_mail_to_uri_cb (TextView *textview,
208 static void save_file_cb (TextView *textview,
212 static GtkItemFactoryEntry textview_link_popup_entries[] =
214 {N_("/_Open link"), NULL, open_uri_cb, 0, NULL},
215 {N_("/_Copy link location"), NULL, copy_uri_cb, 0, NULL},
218 static GtkItemFactoryEntry textview_mail_popup_entries[] =
220 {N_("/_Add to addressbook"), NULL, add_uri_to_addrbook_cb, 0, NULL},
221 {N_("/_Email"), NULL, mail_to_uri_cb, 0, NULL},
222 {N_("/_Copy"), NULL, copy_mail_to_uri_cb, 0, NULL},
225 static GtkItemFactoryEntry textview_file_popup_entries[] =
227 {N_("/_Save this image..."), NULL, save_file_cb, 0, NULL},
231 TextView *textview_create(void)
235 GtkWidget *scrolledwin;
237 GtkTextBuffer *buffer;
238 GtkClipboard *clipboard;
239 GtkItemFactory *link_popupfactory, *mail_popupfactory, *file_popupfactory;
240 GtkWidget *link_popupmenu, *mail_popupmenu, *file_popupmenu;
243 debug_print("Creating text view...\n");
244 textview = g_new0(TextView, 1);
246 scrolledwin = gtk_scrolled_window_new(NULL, NULL);
247 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin),
248 GTK_POLICY_AUTOMATIC,
249 GTK_POLICY_AUTOMATIC);
250 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwin),
252 gtk_widget_set_size_request
253 (scrolledwin, prefs_common.mainview_width, -1);
255 /* create GtkSText widgets for single-byte and multi-byte character */
256 text = gtk_text_view_new();
257 gtk_widget_add_events(text, GDK_LEAVE_NOTIFY_MASK);
258 gtk_widget_show(text);
259 gtk_text_view_set_editable(GTK_TEXT_VIEW(text), FALSE);
260 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text), GTK_WRAP_WORD_CHAR);
261 gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(text), FALSE);
262 gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text), 6);
263 gtk_text_view_set_right_margin(GTK_TEXT_VIEW(text), 6);
265 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text));
266 clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
267 gtk_text_buffer_add_selection_clipboard(buffer, clipboard);
269 gtk_widget_ensure_style(text);
271 gtk_widget_ref(scrolledwin);
273 gtk_container_add(GTK_CONTAINER(scrolledwin), text);
275 g_signal_connect(G_OBJECT(text), "key-press-event",
276 G_CALLBACK(textview_key_pressed), textview);
277 g_signal_connect(G_OBJECT(text), "motion-notify-event",
278 G_CALLBACK(textview_motion_notify), textview);
279 g_signal_connect(G_OBJECT(text), "leave-notify-event",
280 G_CALLBACK(textview_leave_notify), textview);
281 g_signal_connect(G_OBJECT(text), "visibility-notify-event",
282 G_CALLBACK(textview_visibility_notify), textview);
284 gtk_widget_show(scrolledwin);
286 vbox = gtk_vbox_new(FALSE, 0);
287 gtk_box_pack_start(GTK_BOX(vbox), scrolledwin, TRUE, TRUE, 0);
289 gtk_widget_show(vbox);
291 n_entries = sizeof(textview_link_popup_entries) /
292 sizeof(textview_link_popup_entries[0]);
293 link_popupmenu = menu_create_items(textview_link_popup_entries, n_entries,
294 "<UriPopupMenu>", &link_popupfactory,
297 n_entries = sizeof(textview_mail_popup_entries) /
298 sizeof(textview_mail_popup_entries[0]);
299 mail_popupmenu = menu_create_items(textview_mail_popup_entries, n_entries,
300 "<UriPopupMenu>", &mail_popupfactory,
303 n_entries = sizeof(textview_file_popup_entries) /
304 sizeof(textview_file_popup_entries[0]);
305 file_popupmenu = menu_create_items(textview_file_popup_entries, n_entries,
306 "<FilePopupMenu>", &file_popupfactory,
309 textview->vbox = vbox;
310 textview->scrolledwin = scrolledwin;
311 textview->text = text;
312 textview->uri_list = NULL;
313 textview->body_pos = 0;
314 textview->show_all_headers = FALSE;
315 textview->last_buttonpress = GDK_NOTHING;
316 textview->link_popup_menu = link_popupmenu;
317 textview->link_popup_factory = link_popupfactory;
318 textview->mail_popup_menu = mail_popupmenu;
319 textview->mail_popup_factory = mail_popupfactory;
320 textview->file_popup_menu = file_popupmenu;
321 textview->file_popup_factory = file_popupfactory;
326 static void textview_create_tags(GtkTextView *text, TextView *textview)
328 GtkTextBuffer *buffer;
330 static PangoFontDescription *font_desc, *bold_font_desc;
333 font_desc = pango_font_description_from_string
336 if (!bold_font_desc) {
337 bold_font_desc = pango_font_description_from_string
339 pango_font_description_set_weight
340 (bold_font_desc, PANGO_WEIGHT_BOLD);
343 buffer = gtk_text_view_get_buffer(text);
345 gtk_text_buffer_create_tag(buffer, "header",
346 "pixels-above-lines", 0,
347 "pixels-above-lines-set", TRUE,
348 "pixels-below-lines", 0,
349 "pixels-below-lines-set", TRUE,
350 "font-desc", font_desc,
352 "left-margin-set", TRUE,
354 gtk_text_buffer_create_tag(buffer, "header_title",
355 "font-desc", bold_font_desc,
357 gtk_text_buffer_create_tag(buffer, "quote0",
358 "foreground-gdk", "e_colors[0],
360 gtk_text_buffer_create_tag(buffer, "quote1",
361 "foreground-gdk", "e_colors[1],
363 gtk_text_buffer_create_tag(buffer, "quote2",
364 "foreground-gdk", "e_colors[2],
366 gtk_text_buffer_create_tag(buffer, "emphasis",
367 "foreground-gdk", &emphasis_color,
369 gtk_text_buffer_create_tag(buffer, "signature",
370 "foreground-gdk", &signature_color,
372 tag = gtk_text_buffer_create_tag(buffer, "link",
373 "foreground-gdk", &uri_color,
375 gtk_text_buffer_create_tag(buffer, "link-hover",
376 "foreground-gdk", &uri_color,
377 "underline", PANGO_UNDERLINE_SINGLE,
380 g_signal_connect(G_OBJECT(tag), "event",
381 G_CALLBACK(textview_uri_button_pressed), textview);
384 void textview_init(TextView *textview)
387 hand_cursor = gdk_cursor_new(GDK_HAND2);
389 text_cursor = gdk_cursor_new(GDK_XTERM);
391 textview_reflect_prefs(textview);
392 textview_set_all_headers(textview, FALSE);
393 textview_set_font(textview, NULL);
394 textview_create_tags(GTK_TEXT_VIEW(textview->text), textview);
397 void textview_update_message_colors(void)
399 GdkColor black = {0, 0, 0, 0};
401 if (prefs_common.enable_color) {
402 /* grab the quote colors, converting from an int to a GdkColor */
403 gtkut_convert_int_to_gdk_color(prefs_common.quote_level1_col,
405 gtkut_convert_int_to_gdk_color(prefs_common.quote_level2_col,
407 gtkut_convert_int_to_gdk_color(prefs_common.quote_level3_col,
409 gtkut_convert_int_to_gdk_color(prefs_common.uri_col,
411 gtkut_convert_int_to_gdk_color(prefs_common.signature_col,
414 quote_colors[0] = quote_colors[1] = quote_colors[2] =
415 uri_color = emphasis_color = signature_color = black;
419 void textview_reflect_prefs(TextView *textview)
421 textview_update_message_colors();
422 gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(textview->text),
423 prefs_common.textview_cursor_visible);
426 void textview_show_message(TextView *textview, MimeInfo *mimeinfo,
431 if ((fp = fopen(file, "rb")) == NULL) {
432 FILE_OP_ERROR(file, "fopen");
436 textview_clear(textview);
438 textview_add_parts(textview, mimeinfo);
442 textview_set_position(textview, 0);
445 void textview_show_part(TextView *textview, MimeInfo *mimeinfo, FILE *fp)
447 g_return_if_fail(mimeinfo != NULL);
448 g_return_if_fail(fp != NULL);
450 if ((mimeinfo->type == MIMETYPE_MULTIPART) ||
451 ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_ascii_strcasecmp(mimeinfo->subtype, "rfc822"))) {
452 textview_clear(textview);
453 textview_add_parts(textview, mimeinfo);
457 if (fseek(fp, mimeinfo->offset, SEEK_SET) < 0)
460 textview_clear(textview);
462 if (mimeinfo->type == MIMETYPE_MULTIPART)
463 textview_add_parts(textview, mimeinfo);
465 textview_write_body(textview, mimeinfo);
469 static void textview_add_part(TextView *textview, MimeInfo *mimeinfo)
472 GtkTextBuffer *buffer;
473 GtkTextIter iter, start_iter;
475 GPtrArray *headers = NULL;
480 g_return_if_fail(mimeinfo != NULL);
481 text = GTK_TEXT_VIEW(textview->text);
482 buffer = gtk_text_view_get_buffer(text);
483 charcount = gtk_text_buffer_get_char_count(buffer);
484 gtk_text_buffer_get_end_iter(buffer, &iter);
486 if (mimeinfo->type == MIMETYPE_MULTIPART) return;
488 if ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_ascii_strcasecmp(mimeinfo->subtype, "rfc822")) {
491 fp = fopen(mimeinfo->data.filename, "rb");
492 fseek(fp, mimeinfo->offset, SEEK_SET);
493 headers = textview_scan_header(textview, fp);
496 gtk_text_buffer_insert(buffer, &iter, "\n", 1);
497 textview_show_header(textview, headers);
498 procheader_header_array_destroy(headers);
504 name = procmime_mimeinfo_get_parameter(mimeinfo, "filename");
505 content_type = procmime_get_content_type_str(mimeinfo->type,
508 name = procmime_mimeinfo_get_parameter(mimeinfo, "name");
510 g_snprintf(buf, sizeof(buf), "\n[%s %s (%d bytes)]\n",
511 name, content_type, mimeinfo->length);
513 g_snprintf(buf, sizeof(buf), "\n[%s (%d bytes)]\n",
514 content_type, mimeinfo->length);
516 g_free(content_type);
518 if (mimeinfo->type != MIMETYPE_TEXT) {
519 gtk_text_buffer_insert(buffer, &iter, buf, -1);
520 if (mimeinfo->type == MIMETYPE_IMAGE &&
521 prefs_common.inline_img ) {
523 GError *error = NULL;
529 fp = fopen(mimeinfo->data.filename, "rb");
530 fseek(fp, mimeinfo->offset, SEEK_SET);
532 filename = procmime_get_tmp_file_name(mimeinfo);
533 if (procmime_get_part(filename, mimeinfo) < 0) {
534 g_warning("Can't get the image file.");
539 pixbuf = gdk_pixbuf_new_from_file(filename, &error);
541 g_warning("%s\n", error->message);
545 g_warning("Can't load the image.");
550 if (prefs_common.resize_img) {
551 int new_width, new_height;
553 image_viewer_get_resized_size(gdk_pixbuf_get_width(pixbuf),
554 gdk_pixbuf_get_height(pixbuf),
555 textview->scrolledwin->allocation.width - 100,
556 gdk_pixbuf_get_height(pixbuf),
557 &new_width, &new_height);
558 scaled = gdk_pixbuf_scale_simple
559 (pixbuf, new_width, new_height, GDK_INTERP_BILINEAR);
561 g_object_unref(pixbuf);
565 uri_str = g_filename_to_uri(filename, NULL, NULL);
567 uri = g_new(RemoteURI, 1);
569 uri->start = gtk_text_iter_get_offset(&iter);
571 gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf);
573 uri->end = uri->start + 1;
574 uri->filename = procmime_get_part_file_name(mimeinfo);
576 g_slist_append(textview->uri_list, uri);
578 gtk_text_buffer_insert(buffer, &iter, " ", 1);
579 gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, uri->start);
580 gtk_text_buffer_apply_tag_by_name(buffer, "link",
583 gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf);
584 gtk_text_buffer_insert(buffer, &iter, " ", 1);
587 g_object_unref(pixbuf);
590 } else if (mimeinfo->disposition != DISPOSITIONTYPE_ATTACHMENT) {
591 if (prefs_common.display_header && (charcount > 0))
592 gtk_text_buffer_insert(buffer, &iter, "\n", 1);
594 textview_write_body(textview, mimeinfo);
598 static void recursive_add_parts(TextView *textview, GNode *node)
603 mimeinfo = (MimeInfo *) node->data;
605 textview_add_part(textview, mimeinfo);
607 if ((mimeinfo->type != MIMETYPE_MULTIPART) &&
608 (mimeinfo->type != MIMETYPE_MESSAGE))
611 if (g_ascii_strcasecmp(mimeinfo->subtype, "alternative") == 0) {
612 GNode * prefered_body;
620 prefered_body = NULL;
623 for (iter = g_node_first_child(node) ; iter != NULL ;
624 iter = g_node_next_sibling(iter)) {
629 submime = (MimeInfo *) iter->data;
630 if (submime->type == MIMETYPE_TEXT)
633 if (submime->subtype != NULL) {
634 if (g_ascii_strcasecmp(submime->subtype, "plain") == 0)
638 if (score > prefered_score) {
639 prefered_score = score;
640 prefered_body = iter;
644 if (prefered_body != NULL) {
645 recursive_add_parts(textview, prefered_body);
649 for (iter = g_node_first_child(node) ; iter != NULL ;
650 iter = g_node_next_sibling(iter)) {
651 recursive_add_parts(textview, iter);
656 static void textview_add_parts(TextView *textview, MimeInfo *mimeinfo)
658 g_return_if_fail(mimeinfo != NULL);
660 recursive_add_parts(textview, mimeinfo->node);
663 #define TEXT_INSERT(str) \
664 gtk_text_buffer_insert(buffer, &iter, str, -1)
666 void textview_show_error(TextView *textview)
669 GtkTextBuffer *buffer;
672 textview_set_font(textview, NULL);
673 textview_clear(textview);
675 text = GTK_TEXT_VIEW(textview->text);
676 buffer = gtk_text_view_get_buffer(text);
677 gtk_text_buffer_get_start_iter(buffer, &iter);
679 TEXT_INSERT(_("This message can't be displayed.\n"));
683 void textview_show_mime_part(TextView *textview, MimeInfo *partinfo)
686 GtkTextBuffer *buffer;
689 if (!partinfo) return;
691 textview_set_font(textview, NULL);
692 textview_clear(textview);
694 text = GTK_TEXT_VIEW(textview->text);
695 buffer = gtk_text_view_get_buffer(text);
696 gtk_text_buffer_get_start_iter(buffer, &iter);
698 TEXT_INSERT(_("The following can be performed on this part by "));
699 TEXT_INSERT(_("right-clicking the icon or list item:\n"));
701 TEXT_INSERT(_(" To save select 'Save as...' (Shortcut key: 'y')\n"));
702 TEXT_INSERT(_(" To display as text select 'Display as text' "));
703 TEXT_INSERT(_("(Shortcut key: 't')\n"));
704 TEXT_INSERT(_(" To open with an external program select 'Open' "));
705 TEXT_INSERT(_("(Shortcut key: 'l'),\n"));
706 TEXT_INSERT(_(" (alternately double-click, or click the middle "));
707 TEXT_INSERT(_("mouse button),\n"));
708 TEXT_INSERT(_(" or 'Open with...' (Shortcut key: 'o')\n"));
714 static void textview_write_body(TextView *textview, MimeInfo *mimeinfo)
719 const gchar *charset;
721 if (textview->messageview->forced_charset)
722 charset = textview->messageview->forced_charset;
724 charset = procmime_mimeinfo_get_parameter(mimeinfo, "charset");
726 textview_set_font(textview, charset);
728 conv = conv_code_converter_new(charset);
730 procmime_force_encoding(textview->messageview->forced_encoding);
732 textview->is_in_signature = FALSE;
734 procmime_decode_content(mimeinfo);
736 if (!g_ascii_strcasecmp(mimeinfo->subtype, "html") &&
737 prefs_common.render_html) {
740 filename = procmime_get_tmp_file_name(mimeinfo);
741 if (procmime_get_part(filename, mimeinfo) == 0) {
742 tmpfp = fopen(filename, "rb");
743 textview_show_html(textview, tmpfp, conv);
748 } else if (!g_ascii_strcasecmp(mimeinfo->subtype, "enriched")) {
751 filename = procmime_get_tmp_file_name(mimeinfo);
752 if (procmime_get_part(filename, mimeinfo) == 0) {
753 tmpfp = fopen(filename, "rb");
754 textview_show_ertf(textview, tmpfp, conv);
760 tmpfp = fopen(mimeinfo->data.filename, "rb");
761 fseek(tmpfp, mimeinfo->offset, SEEK_SET);
762 debug_print("Viewing text content of type: %s (length: %d)\n", mimeinfo->subtype, mimeinfo->length);
763 while ((fgets(buf, sizeof(buf), tmpfp) != NULL) &&
764 (ftell(tmpfp) <= mimeinfo->offset + mimeinfo->length))
765 textview_write_line(textview, buf, conv);
769 conv_code_converter_destroy(conv);
770 procmime_force_encoding(0);
773 static void textview_show_html(TextView *textview, FILE *fp,
779 parser = html_parser_new(fp, conv);
780 g_return_if_fail(parser != NULL);
782 while ((str = html_parse(parser)) != NULL) {
783 if (parser->state == HTML_HREF) {
784 /* first time : get and copy the URL */
785 if (parser->href == NULL) {
786 /* ALF - the sylpheed html parser returns an empty string,
787 * if still inside an <a>, but already parsed past HREF */
788 str = strtok(str, " ");
790 parser->href = g_strdup(str);
791 /* the URL may (or not) be followed by the
793 str = strtok(NULL, "");
797 textview_write_link(textview, str, parser->href, NULL);
799 textview_write_line(textview, str, NULL);
801 textview_write_line(textview, "\n", NULL);
802 html_parser_destroy(parser);
805 static void textview_show_ertf(TextView *textview, FILE *fp,
811 parser = ertf_parser_new(fp, conv);
812 g_return_if_fail(parser != NULL);
814 while ((str = ertf_parse(parser)) != NULL) {
815 textview_write_line(textview, str, NULL);
818 ertf_parser_destroy(parser);
821 /* get_uri_part() - retrieves a URI starting from scanpos.
822 Returns TRUE if succesful */
823 static gboolean get_uri_part(const gchar *start, const gchar *scanpos,
824 const gchar **bp, const gchar **ep)
828 g_return_val_if_fail(start != NULL, FALSE);
829 g_return_val_if_fail(scanpos != NULL, FALSE);
830 g_return_val_if_fail(bp != NULL, FALSE);
831 g_return_val_if_fail(ep != NULL, FALSE);
835 /* find end point of URI */
836 for (ep_ = scanpos; *ep_ != '\0'; ep_++) {
837 if (!isgraph(*(const guchar *)ep_) ||
838 !IS_ASCII(*(const guchar *)ep_) ||
839 strchr("()<>\"", *ep_))
843 /* no punctuation at end of string */
845 /* FIXME: this stripping of trailing punctuations may bite with other URIs.
846 * should pass some URI type to this function and decide on that whether
847 * to perform punctuation stripping */
849 #define IS_REAL_PUNCT(ch) (ispunct(ch) && ((ch) != '/'))
851 for (; ep_ - 1 > scanpos + 1 &&
852 IS_REAL_PUNCT(*(const guchar *)(ep_ - 1));
863 static gchar *make_uri_string(const gchar *bp, const gchar *ep)
865 return g_strndup(bp, ep - bp);
868 /* valid mail address characters */
869 #define IS_RFC822_CHAR(ch) \
874 !strchr("(),;<>\"", (ch)))
876 /* alphabet and number within 7bit ASCII */
877 #define IS_ASCII_ALNUM(ch) (IS_ASCII(ch) && isalnum(ch))
878 #define IS_QUOTE(ch) ((ch) == '\'' || (ch) == '"')
880 static GHashTable *create_domain_tab(void)
882 static const gchar *toplvl_domains [] = {
884 "arpa", "coop", "info", "name", "biz", "com", "edu", "gov",
885 "int", "mil", "net", "org", "ac", "ad", "ae", "af", "ag",
886 "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au",
887 "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi",
888 "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by",
889 "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl",
890 "cm", "cn", "co", "cr", "cu", "cv", "cx", "cy", "cz", "de",
891 "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er",
892 "es", "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gd",
893 "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq",
894 "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr",
895 "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir",
896 "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki",
897 "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc",
898 "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc",
899 "md", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq",
900 "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na",
901 "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu",
902 "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm",
903 "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru",
904 "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj",
905 "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz",
906 "tc", "td", "tf", "tg", "th", "tj", "tk", "tm", "tn", "to",
907 "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um",
908 "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu",
909 "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw"
912 GHashTable *htab = g_hash_table_new(g_stricase_hash, g_stricase_equal);
914 g_return_val_if_fail(htab, NULL);
915 for (n = 0; n < sizeof toplvl_domains / sizeof toplvl_domains[0]; n++)
916 g_hash_table_insert(htab, (gpointer) toplvl_domains[n], (gpointer) toplvl_domains[n]);
920 static gboolean is_toplvl_domain(GHashTable *tab, const gchar *first, const gchar *last)
922 const gint MAX_LVL_DOM_NAME_LEN = 6;
923 gchar buf[MAX_LVL_DOM_NAME_LEN + 1];
924 const gchar *m = buf + MAX_LVL_DOM_NAME_LEN + 1;
927 if (last - first > MAX_LVL_DOM_NAME_LEN || first > last)
930 for (p = buf; p < m && first < last; *p++ = *first++)
934 return g_hash_table_lookup(tab, buf) != NULL;
937 /* get_email_part() - retrieves an email address. Returns TRUE if succesful */
938 static gboolean get_email_part(const gchar *start, const gchar *scanpos,
939 const gchar **bp, const gchar **ep)
941 /* more complex than the uri part because we need to scan back and forward starting from
942 * the scan position. */
943 gboolean result = FALSE;
944 const gchar *bp_ = NULL;
945 const gchar *ep_ = NULL;
946 static GHashTable *dom_tab;
947 const gchar *last_dot = NULL;
948 const gchar *prelast_dot = NULL;
949 const gchar *last_tld_char = NULL;
951 /* the informative part of the email address (describing the name
952 * of the email address owner) may contain quoted parts. the
953 * closure stack stores the last encountered quotes. */
954 gchar closure_stack[128];
955 gchar *ptr = closure_stack;
957 g_return_val_if_fail(start != NULL, FALSE);
958 g_return_val_if_fail(scanpos != NULL, FALSE);
959 g_return_val_if_fail(bp != NULL, FALSE);
960 g_return_val_if_fail(ep != NULL, FALSE);
963 dom_tab = create_domain_tab();
964 g_return_val_if_fail(dom_tab, FALSE);
966 /* scan start of address */
967 for (bp_ = scanpos - 1;
968 bp_ >= start && IS_RFC822_CHAR(*(const guchar *)bp_); bp_--)
971 /* TODO: should start with an alnum? */
973 for (; bp_ < scanpos && !IS_ASCII_ALNUM(*(const guchar *)bp_); bp_++)
976 if (bp_ != scanpos) {
977 /* scan end of address */
978 for (ep_ = scanpos + 1;
979 *ep_ && IS_RFC822_CHAR(*(const guchar *)ep_); ep_++)
981 prelast_dot = last_dot;
983 if (*(last_dot + 1) == '.') {
984 if (prelast_dot == NULL)
986 last_dot = prelast_dot;
991 /* TODO: really should terminate with an alnum? */
992 for (; ep_ > scanpos && !IS_ASCII_ALNUM(*(const guchar *)ep_);
997 if (last_dot == NULL)
1000 last_dot = prelast_dot;
1001 if (last_dot == NULL || (scanpos + 1 >= last_dot))
1005 for (last_tld_char = last_dot; last_tld_char < ep_; last_tld_char++)
1006 if (*last_tld_char == '?')
1009 if (is_toplvl_domain(dom_tab, last_dot, last_tld_char))
1016 if (!result) return FALSE;
1018 if (*ep_ && *(bp_ - 1) == '"' && *(ep_) == '"'
1019 && *(ep_ + 1) == ' ' && *(ep_ + 2) == '<'
1020 && IS_RFC822_CHAR(*(ep_ + 3))) {
1021 /* this informative part with an @ in it is
1022 * followed by the email address */
1025 /* go to matching '>' (or next non-rfc822 char, like \n) */
1026 for (; *ep_ != '>' && *ep != '\0' && IS_RFC822_CHAR(*ep_); ep_++)
1029 /* include the bracket */
1030 if (*ep_ == '>') ep_++;
1032 /* include the leading quote */
1040 /* skip if it's between quotes "'alfons@proteus.demon.nl'" <alfons@proteus.demon.nl> */
1041 if (bp_ - 1 > start && IS_QUOTE(*(bp_ - 1)) && IS_QUOTE(*ep_))
1044 /* see if this is <bracketed>; in this case we also scan for the informative part. */
1045 if (bp_ - 1 <= start || *(bp_ - 1) != '<' || *ep_ != '>')
1048 #define FULL_STACK() ((size_t) (ptr - closure_stack) >= sizeof closure_stack)
1049 #define IN_STACK() (ptr > closure_stack)
1050 /* has underrun check */
1051 #define POP_STACK() if(IN_STACK()) --ptr
1052 /* has overrun check */
1053 #define PUSH_STACK(c) if(!FULL_STACK()) *ptr++ = (c); else return TRUE
1054 /* has underrun check */
1055 #define PEEK_STACK() (IN_STACK() ? *(ptr - 1) : 0)
1059 /* scan for the informative part. */
1060 for (bp_ -= 2; bp_ >= start; bp_--) {
1061 /* if closure on the stack keep scanning */
1062 if (PEEK_STACK() == *bp_) {
1066 if (*bp_ == '\'' || *bp_ == '"') {
1071 /* if nothing in the closure stack, do the special conditions
1072 * the following if..else expression simply checks whether
1073 * a token is acceptable. if not acceptable, the clause
1074 * should terminate the loop with a 'break' */
1075 if (!PEEK_STACK()) {
1077 && (((bp_ - 1) >= start) && isalnum(*(bp_ - 1)))
1078 && (((bp_ + 1) < ep_) && isalnum(*(bp_ + 1)))) {
1079 /* hyphens are allowed, but only in
1081 } else if (!strchr(",;:=?./+<>!&\r\n\t", *bp_)) {
1082 /* but anything not being a punctiation
1085 break; /* anything else is rejected */
1098 /* scan forward (should start with an alnum) */
1099 for (; *bp_ != '<' && isspace(*bp_) && *bp_ != '"'; bp_++)
1109 #undef IS_ASCII_ALNUM
1110 #undef IS_RFC822_CHAR
1112 static gchar *make_email_string(const gchar *bp, const gchar *ep)
1114 /* returns a mailto: URI; mailto: is also used to detect the
1115 * uri type later on in the button_pressed signal handler */
1119 tmp = g_strndup(bp, ep - bp);
1120 result = g_strconcat("mailto:", tmp, NULL);
1126 static gchar *make_http_string(const gchar *bp, const gchar *ep)
1128 /* returns an http: URI; */
1132 tmp = g_strndup(bp, ep - bp);
1133 result = g_strconcat("http://", tmp, NULL);
1139 #define ADD_TXT_POS(bp_, ep_, pti_) \
1140 if ((last->next = alloca(sizeof(struct txtpos))) != NULL) { \
1141 last = last->next; \
1142 last->bp = (bp_); last->ep = (ep_); last->pti = (pti_); \
1143 last->next = NULL; \
1145 g_warning("alloc error scanning URIs\n"); \
1146 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, \
1152 /* textview_make_clickable_parts() - colorizes clickable parts */
1153 static void textview_make_clickable_parts(TextView *textview,
1154 const gchar *fg_tag,
1155 const gchar *uri_tag,
1156 const gchar *linebuf)
1158 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1159 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1162 /* parse table - in order of priority */
1164 const gchar *needle; /* token */
1166 /* token search function */
1167 gchar *(*search) (const gchar *haystack,
1168 const gchar *needle);
1169 /* part parsing function */
1170 gboolean (*parse) (const gchar *start,
1171 const gchar *scanpos,
1174 /* part to URI function */
1175 gchar *(*build_uri) (const gchar *bp,
1179 static struct table parser[] = {
1180 {"http://", strcasestr, get_uri_part, make_uri_string},
1181 {"https://", strcasestr, get_uri_part, make_uri_string},
1182 {"ftp://", strcasestr, get_uri_part, make_uri_string},
1183 {"www.", strcasestr, get_uri_part, make_http_string},
1184 {"mailto:", strcasestr, get_uri_part, make_uri_string},
1185 {"@", strcasestr, get_email_part, make_email_string}
1187 const gint PARSE_ELEMS = sizeof parser / sizeof parser[0];
1190 const gchar *walk, *bp, *ep;
1193 const gchar *bp, *ep; /* text position */
1194 gint pti; /* index in parse table */
1195 struct txtpos *next; /* next */
1196 } head = {NULL, NULL, 0, NULL}, *last = &head;
1198 gtk_text_buffer_get_end_iter(buffer, &iter);
1200 /* parse for clickable parts, and build a list of begin and end positions */
1201 for (walk = linebuf, n = 0;;) {
1202 gint last_index = PARSE_ELEMS;
1203 gchar *scanpos = NULL;
1205 /* FIXME: this looks phony. scanning for anything in the parse table */
1206 for (n = 0; n < PARSE_ELEMS; n++) {
1209 tmp = parser[n].search(walk, parser[n].needle);
1211 if (scanpos == NULL || tmp < scanpos) {
1219 /* check if URI can be parsed */
1220 if (parser[last_index].parse(walk, scanpos, &bp, &ep)
1221 && (size_t) (ep - bp - 1) > strlen(parser[last_index].needle)) {
1222 ADD_TXT_POS(bp, ep, last_index);
1226 strlen(parser[last_index].needle);
1231 /* colorize this line */
1233 const gchar *normal_text = linebuf;
1236 for (last = head.next; last != NULL;
1237 normal_text = last->ep, last = last->next) {
1239 uri = g_new(RemoteURI, 1);
1240 if (last->bp - normal_text > 0)
1241 gtk_text_buffer_insert_with_tags_by_name
1244 last->bp - normal_text,
1246 uri->uri = parser[last->pti].build_uri(last->bp,
1248 uri->start = gtk_text_iter_get_offset(&iter);
1249 gtk_text_buffer_insert_with_tags_by_name
1250 (buffer, &iter, last->bp, last->ep - last->bp,
1251 uri_tag, fg_tag, NULL);
1252 uri->end = gtk_text_iter_get_offset(&iter);
1253 uri->filename = NULL;
1254 textview->uri_list =
1255 g_slist_append(textview->uri_list, uri);
1259 gtk_text_buffer_insert_with_tags_by_name
1260 (buffer, &iter, normal_text, -1, fg_tag, NULL);
1262 gtk_text_buffer_insert_with_tags_by_name
1263 (buffer, &iter, linebuf, -1, fg_tag, NULL);
1269 static void textview_write_line(TextView *textview, const gchar *str,
1270 CodeConverter *conv)
1273 GtkTextBuffer *buffer;
1275 gchar buf[BUFFSIZE];
1277 gint quotelevel = -1;
1278 gchar quote_tag_str[10];
1280 text = GTK_TEXT_VIEW(textview->text);
1281 buffer = gtk_text_view_get_buffer(text);
1282 gtk_text_buffer_get_end_iter(buffer, &iter);
1285 strncpy2(buf, str, sizeof(buf));
1286 else if (conv_convert(conv, buf, sizeof(buf), str) < 0)
1287 conv_utf8todisp(buf, sizeof(buf), str);
1290 //if (prefs_common.conv_mb_alnum) conv_mb_alnum(buf);
1293 /* change color of quotation
1294 >, foo>, _> ... ok, <foo>, foo bar>, foo-> ... ng
1295 Up to 3 levels of quotations are detected, and each
1296 level is colored using a different color. */
1297 if (prefs_common.enable_color
1298 && line_has_quote_char(buf, prefs_common.quote_chars)) {
1299 quotelevel = get_quote_level(buf, prefs_common.quote_chars);
1301 /* set up the correct foreground color */
1302 if (quotelevel > 2) {
1303 /* recycle colors */
1304 if (prefs_common.recycle_quote_colors)
1311 if (quotelevel == -1)
1314 g_snprintf(quote_tag_str, sizeof(quote_tag_str),
1315 "quote%d", quotelevel);
1316 fg_color = quote_tag_str;
1319 if (prefs_common.enable_color && (strcmp(buf,"-- \n") == 0 || textview->is_in_signature)) {
1320 fg_color = "signature";
1321 textview->is_in_signature = TRUE;
1324 if (prefs_common.enable_color)
1325 textview_make_clickable_parts(textview, fg_color, "link", buf);
1327 textview_make_clickable_parts(textview, fg_color, NULL, buf);
1330 void textview_write_link(TextView *textview, const gchar *str,
1331 const gchar *uri, CodeConverter *conv)
1333 GdkColor *link_color = NULL;
1335 GtkTextBuffer *buffer;
1337 gchar buf[BUFFSIZE];
1341 if (!str || *str == '\0')
1346 text = GTK_TEXT_VIEW(textview->text);
1347 buffer = gtk_text_view_get_buffer(text);
1348 gtk_text_buffer_get_end_iter(buffer, &iter);
1351 strncpy2(buf, str, sizeof(buf));
1352 else if (conv_convert(conv, buf, sizeof(buf), str) < 0)
1353 conv_utf8todisp(buf, sizeof(buf), str);
1355 if (g_utf8_validate(buf, -1, NULL) == FALSE)
1360 gtk_text_buffer_get_end_iter(buffer, &iter);
1361 for (bufp = buf; *bufp != '\0'; bufp = g_utf8_next_char(bufp)) {
1364 ch = g_utf8_get_char(bufp);
1365 if (!g_unichar_isspace(ch))
1369 gtk_text_buffer_insert(buffer, &iter, buf, bufp - buf);
1371 if (prefs_common.enable_color) {
1372 link_color = &uri_color;
1374 r_uri = g_new(RemoteURI, 1);
1375 r_uri->uri = g_strdup(uri);
1376 r_uri->start = gtk_text_iter_get_offset(&iter);
1377 gtk_text_buffer_insert_with_tags_by_name
1378 (buffer, &iter, bufp, -1, "link", NULL);
1379 r_uri->end = gtk_text_iter_get_offset(&iter);
1380 r_uri->filename = NULL;
1381 textview->uri_list = g_slist_append(textview->uri_list, r_uri);
1384 void textview_clear(TextView *textview)
1386 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1387 GtkTextBuffer *buffer;
1389 buffer = gtk_text_view_get_buffer(text);
1390 gtk_text_buffer_set_text(buffer, "", -1);
1392 TEXTVIEW_STATUSBAR_POP(textview);
1393 textview_uri_list_remove_all(textview->uri_list);
1394 textview->uri_list = NULL;
1396 textview->body_pos = 0;
1399 void textview_destroy(TextView *textview)
1401 textview_uri_list_remove_all(textview->uri_list);
1402 textview->uri_list = NULL;
1407 void textview_set_all_headers(TextView *textview, gboolean all_headers)
1409 textview->show_all_headers = all_headers;
1412 void textview_set_font(TextView *textview, const gchar *codeset)
1414 if (prefs_common.textfont) {
1415 PangoFontDescription *font_desc = NULL;
1417 font_desc = pango_font_description_from_string
1418 (prefs_common.textfont);
1420 gtk_widget_modify_font(textview->text, font_desc);
1421 pango_font_description_free(font_desc);
1424 gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(textview->text),
1425 prefs_common.line_space / 2);
1426 gtk_text_view_set_pixels_below_lines(GTK_TEXT_VIEW(textview->text),
1427 prefs_common.line_space / 2);
1430 void textview_set_text(TextView *textview, const gchar *text)
1433 GtkTextBuffer *buffer;
1435 g_return_if_fail(textview != NULL);
1436 g_return_if_fail(text != NULL);
1438 textview_clear(textview);
1440 view = GTK_TEXT_VIEW(textview->text);
1441 buffer = gtk_text_view_get_buffer(view);
1442 gtk_text_buffer_set_text(buffer, text, strlen(text));
1458 H_ORGANIZATION = 11,
1461 void textview_set_position(TextView *textview, gint pos)
1463 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1464 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1467 gtk_text_buffer_get_iter_at_offset(buffer, &iter, pos);
1468 gtk_text_buffer_place_cursor(buffer, &iter);
1469 gtk_text_view_scroll_to_iter(text, &iter, 0.0, FALSE, 0.0, 0.0);
1472 static GPtrArray *textview_scan_header(TextView *textview, FILE *fp)
1474 gchar buf[BUFFSIZE];
1475 GPtrArray *headers, *sorted_headers;
1476 GSList *disphdr_list;
1480 g_return_val_if_fail(fp != NULL, NULL);
1482 if (textview->show_all_headers)
1483 return procheader_get_header_array_asis(fp);
1485 if (!prefs_common.display_header) {
1486 while (fgets(buf, sizeof(buf), fp) != NULL)
1487 if (buf[0] == '\r' || buf[0] == '\n') break;
1491 headers = procheader_get_header_array_asis(fp);
1493 sorted_headers = g_ptr_array_new();
1495 for (disphdr_list = prefs_common.disphdr_list; disphdr_list != NULL;
1496 disphdr_list = disphdr_list->next) {
1497 DisplayHeaderProp *dp =
1498 (DisplayHeaderProp *)disphdr_list->data;
1500 for (i = 0; i < headers->len; i++) {
1501 header = g_ptr_array_index(headers, i);
1503 if (procheader_headername_equal(header->name,
1506 procheader_header_free(header);
1508 g_ptr_array_add(sorted_headers, header);
1510 g_ptr_array_remove_index(headers, i);
1516 if (prefs_common.show_other_header) {
1517 for (i = 0; i < headers->len; i++) {
1518 header = g_ptr_array_index(headers, i);
1519 g_ptr_array_add(sorted_headers, header);
1521 g_ptr_array_free(headers, TRUE);
1523 procheader_header_array_destroy(headers);
1526 return sorted_headers;
1529 static void textview_show_header(TextView *textview, GPtrArray *headers)
1531 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1532 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1537 g_return_if_fail(headers != NULL);
1539 for (i = 0; i < headers->len; i++) {
1540 header = g_ptr_array_index(headers, i);
1541 g_return_if_fail(header->name != NULL);
1543 gtk_text_buffer_get_end_iter (buffer, &iter);
1544 gtk_text_buffer_insert_with_tags_by_name
1545 (buffer, &iter, header->name, -1,
1546 "header_title", "header", NULL);
1547 if (header->name[strlen(header->name) - 1] != ' ')
1548 gtk_text_buffer_insert_with_tags_by_name
1549 (buffer, &iter, " ", 1,
1550 "header_title", "header", NULL);
1552 if (procheader_headername_equal(header->name, "Subject") ||
1553 procheader_headername_equal(header->name, "From") ||
1554 procheader_headername_equal(header->name, "To") ||
1555 procheader_headername_equal(header->name, "Cc"))
1556 unfold_line(header->body);
1558 if (prefs_common.enable_color &&
1559 (procheader_headername_equal(header->name, "X-Mailer") ||
1560 procheader_headername_equal(header->name,
1562 strstr(header->body, "Sylpheed") != NULL) {
1563 gtk_text_buffer_get_end_iter (buffer, &iter);
1564 gtk_text_buffer_insert_with_tags_by_name
1565 (buffer, &iter, header->body, -1,
1566 "header", "emphasis", NULL);
1567 } else if (prefs_common.enable_color) {
1568 textview_make_clickable_parts(textview, "header", "link",
1571 textview_make_clickable_parts(textview, "header", NULL,
1574 gtk_text_buffer_get_end_iter (buffer, &iter);
1575 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, "\n", 1,
1580 gboolean textview_search_string(TextView *textview, const gchar *str,
1583 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1584 GtkTextBuffer *buffer;
1585 GtkTextIter iter, match_pos;
1589 g_return_val_if_fail(str != NULL, FALSE);
1591 buffer = gtk_text_view_get_buffer(text);
1593 len = g_utf8_strlen(str, -1);
1594 g_return_val_if_fail(len >= 0, FALSE);
1596 mark = gtk_text_buffer_get_insert(buffer);
1597 gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
1599 if (gtkut_text_buffer_find(buffer, &iter, str, case_sens,
1601 GtkTextIter end = match_pos;
1603 gtk_text_iter_forward_chars(&end, len);
1604 /* place "insert" at the last character */
1605 gtk_text_buffer_select_range(buffer, &end, &match_pos);
1606 gtk_text_view_scroll_to_mark(text, mark, 0.0, FALSE, 0.0, 0.0);
1613 gboolean textview_search_string_backward(TextView *textview, const gchar *str,
1616 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1617 GtkTextBuffer *buffer;
1618 GtkTextIter iter, match_pos;
1622 g_return_val_if_fail(str != NULL, FALSE);
1624 buffer = gtk_text_view_get_buffer(text);
1626 len = g_utf8_strlen(str, -1);
1627 g_return_val_if_fail(len >= 0, FALSE);
1629 mark = gtk_text_buffer_get_insert(buffer);
1630 gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
1632 if (gtkut_text_buffer_find_backward(buffer, &iter, str, case_sens,
1634 GtkTextIter end = match_pos;
1636 gtk_text_iter_forward_chars(&end, len);
1637 gtk_text_buffer_select_range(buffer, &match_pos, &end);
1638 gtk_text_view_scroll_to_mark(text, mark, 0.0, FALSE, 0.0, 0.0);
1645 void textview_scroll_one_line(TextView *textview, gboolean up)
1647 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1648 GtkAdjustment *vadj = text->vadjustment;
1651 if (prefs_common.enable_smooth_scroll) {
1652 textview_smooth_scroll_one_line(textview, up);
1657 upper = vadj->upper - vadj->page_size;
1658 if (vadj->value < upper) {
1659 vadj->value += vadj->step_increment;
1660 vadj->value = MIN(vadj->value, upper);
1661 g_signal_emit_by_name(G_OBJECT(vadj),
1662 "value_changed", 0);
1665 if (vadj->value > 0.0) {
1666 vadj->value -= vadj->step_increment;
1667 vadj->value = MAX(vadj->value, 0.0);
1668 g_signal_emit_by_name(G_OBJECT(vadj),
1669 "value_changed", 0);
1674 gboolean textview_scroll_page(TextView *textview, gboolean up)
1676 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1677 GtkAdjustment *vadj = text->vadjustment;
1681 if (prefs_common.enable_smooth_scroll)
1682 return textview_smooth_scroll_page(textview, up);
1684 if (prefs_common.scroll_halfpage)
1685 page_incr = vadj->page_increment / 2;
1687 page_incr = vadj->page_increment;
1690 upper = vadj->upper - vadj->page_size;
1691 if (vadj->value < upper) {
1692 vadj->value += page_incr;
1693 vadj->value = MIN(vadj->value, upper);
1694 g_signal_emit_by_name(G_OBJECT(vadj),
1695 "value_changed", 0);
1699 if (vadj->value > 0.0) {
1700 vadj->value -= page_incr;
1701 vadj->value = MAX(vadj->value, 0.0);
1702 g_signal_emit_by_name(G_OBJECT(vadj),
1703 "value_changed", 0);
1711 static void textview_smooth_scroll_do(TextView *textview,
1712 gfloat old_value, gfloat last_value,
1715 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1716 GtkAdjustment *vadj = text->vadjustment;
1721 if (old_value < last_value) {
1722 change_value = last_value - old_value;
1725 change_value = old_value - last_value;
1729 for (i = step; i <= change_value; i += step) {
1730 vadj->value = old_value + (up ? -i : i);
1731 g_signal_emit_by_name(G_OBJECT(vadj),
1732 "value_changed", 0);
1735 vadj->value = last_value;
1736 g_signal_emit_by_name(G_OBJECT(vadj), "value_changed", 0);
1738 gtk_widget_queue_draw(GTK_WIDGET(text));
1741 static void textview_smooth_scroll_one_line(TextView *textview, gboolean up)
1743 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1744 GtkAdjustment *vadj = text->vadjustment;
1750 upper = vadj->upper - vadj->page_size;
1751 if (vadj->value < upper) {
1752 old_value = vadj->value;
1753 last_value = vadj->value + vadj->step_increment;
1754 last_value = MIN(last_value, upper);
1756 textview_smooth_scroll_do(textview, old_value,
1758 prefs_common.scroll_step);
1761 if (vadj->value > 0.0) {
1762 old_value = vadj->value;
1763 last_value = vadj->value - vadj->step_increment;
1764 last_value = MAX(last_value, 0.0);
1766 textview_smooth_scroll_do(textview, old_value,
1768 prefs_common.scroll_step);
1773 static gboolean textview_smooth_scroll_page(TextView *textview, gboolean up)
1775 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1776 GtkAdjustment *vadj = text->vadjustment;
1782 if (prefs_common.scroll_halfpage)
1783 page_incr = vadj->page_increment / 2;
1785 page_incr = vadj->page_increment;
1788 upper = vadj->upper - vadj->page_size;
1789 if (vadj->value < upper) {
1790 old_value = vadj->value;
1791 last_value = vadj->value + page_incr;
1792 last_value = MIN(last_value, upper);
1794 textview_smooth_scroll_do(textview, old_value,
1796 prefs_common.scroll_step);
1800 if (vadj->value > 0.0) {
1801 old_value = vadj->value;
1802 last_value = vadj->value - page_incr;
1803 last_value = MAX(last_value, 0.0);
1805 textview_smooth_scroll_do(textview, old_value,
1807 prefs_common.scroll_step);
1815 #define KEY_PRESS_EVENT_STOP() \
1816 g_signal_stop_emission_by_name(G_OBJECT(widget), \
1819 static gint textview_key_pressed(GtkWidget *widget, GdkEventKey *event,
1822 SummaryView *summaryview = NULL;
1823 MessageView *messageview = textview->messageview;
1825 if (!event) return FALSE;
1826 if (messageview->mainwin)
1827 summaryview = messageview->mainwin->summaryview;
1829 switch (event->keyval) {
1844 summary_pass_key_press_event(summaryview, event);
1846 textview_scroll_page
1849 (GDK_SHIFT_MASK|GDK_MOD1_MASK)) != 0);
1852 textview_scroll_page(textview, TRUE);
1855 textview_scroll_one_line
1856 (textview, (event->state &
1857 (GDK_SHIFT_MASK|GDK_MOD1_MASK)) != 0);
1861 summary_pass_key_press_event(summaryview, event);
1866 if ((event->state & (GDK_MOD1_MASK|GDK_CONTROL_MASK)) == 0) {
1867 KEY_PRESS_EVENT_STOP();
1868 mimeview_pass_key_press_event(messageview->mimeview,
1872 /* possible fall through */
1875 event->window != messageview->mainwin->window->window) {
1876 GdkEventKey tmpev = *event;
1878 tmpev.window = messageview->mainwin->window->window;
1879 KEY_PRESS_EVENT_STOP();
1880 gtk_widget_event(messageview->mainwin->window,
1881 (GdkEvent *)&tmpev);
1889 static gboolean textview_motion_notify(GtkWidget *widget,
1890 GdkEventMotion *event,
1893 textview_uri_update(textview, event->x, event->y);
1894 gdk_window_get_pointer(widget->window, NULL, NULL, NULL);
1899 static gboolean textview_leave_notify(GtkWidget *widget,
1900 GdkEventCrossing *event,
1903 textview_uri_update(textview, -1, -1);
1908 static gboolean textview_visibility_notify(GtkWidget *widget,
1909 GdkEventVisibility *event,
1915 window = gtk_text_view_get_window(GTK_TEXT_VIEW(widget),
1916 GTK_TEXT_WINDOW_TEXT);
1918 /* check if occurred for the text window part */
1919 if (window != event->window)
1922 gdk_window_get_pointer(widget->window, &wx, &wy, NULL);
1923 textview_uri_update(textview, wx, wy);
1928 static void textview_uri_update(TextView *textview, gint x, gint y)
1930 GtkTextBuffer *buffer;
1931 GtkTextIter start_iter, end_iter;
1932 RemoteURI *uri = NULL;
1934 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text));
1936 if (x != -1 && y != -1) {
1942 gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(textview->text),
1943 GTK_TEXT_WINDOW_WIDGET,
1945 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(textview->text),
1948 tags = gtk_text_iter_get_tags(&iter);
1949 for (cur = tags; cur != NULL; cur = cur->next) {
1950 GtkTextTag *tag = cur->data;
1953 g_object_get(G_OBJECT(tag), "name", &name, NULL);
1954 if (!strcmp(name, "link")
1955 && textview_get_uri_range(textview, &iter, tag,
1956 &start_iter, &end_iter))
1957 uri = textview_get_uri_from_range(textview,
1969 if (uri != textview->uri_hover) {
1972 if (textview->uri_hover)
1973 gtk_text_buffer_remove_tag_by_name(buffer,
1975 &textview->uri_hover_start_iter,
1976 &textview->uri_hover_end_iter);
1978 textview->uri_hover = uri;
1980 textview->uri_hover_start_iter = start_iter;
1981 textview->uri_hover_end_iter = end_iter;
1984 window = gtk_text_view_get_window(GTK_TEXT_VIEW(textview->text),
1985 GTK_TEXT_WINDOW_TEXT);
1986 gdk_window_set_cursor(window, uri ? hand_cursor : text_cursor);
1988 TEXTVIEW_STATUSBAR_POP(textview);
1993 gtk_text_buffer_apply_tag_by_name(buffer,
1998 trimmed_uri = trim_string(uri->uri, 60);
1999 TEXTVIEW_STATUSBAR_PUSH(textview, trimmed_uri);
2000 g_free(trimmed_uri);
2005 static gboolean textview_get_uri_range(TextView *textview,
2008 GtkTextIter *start_iter,
2009 GtkTextIter *end_iter)
2011 GtkTextIter _start_iter, _end_iter;
2014 if (!gtk_text_iter_forward_to_tag_toggle(&_end_iter, tag)) {
2015 debug_print("Can't find end");
2019 _start_iter = _end_iter;
2020 if (!gtk_text_iter_backward_to_tag_toggle(&_start_iter, tag)) {
2021 debug_print("Can't find start.");
2025 *start_iter = _start_iter;
2026 *end_iter = _end_iter;
2031 static RemoteURI *textview_get_uri_from_range(TextView *textview,
2034 GtkTextIter *start_iter,
2035 GtkTextIter *end_iter)
2037 gint start_pos, end_pos, cur_pos;
2038 RemoteURI *uri = NULL;
2041 start_pos = gtk_text_iter_get_offset(start_iter);
2042 end_pos = gtk_text_iter_get_offset(end_iter);
2043 cur_pos = gtk_text_iter_get_offset(iter);
2045 for (cur = textview->uri_list; cur != NULL; cur = cur->next) {
2046 RemoteURI *uri_ = (RemoteURI *)cur->data;
2047 if (start_pos == uri_->start &&
2048 end_pos == uri_->end) {
2051 } else if (start_pos == uri_->start ||
2052 end_pos == uri_->end) {
2053 /* in case of contiguous links, textview_get_uri_range
2054 * returns a broader range (start of 1st link to end
2056 * In that case, correct link is the one covering
2059 if (uri_->start <= cur_pos && cur_pos <= uri_->end) {
2069 static RemoteURI *textview_get_uri(TextView *textview,
2073 GtkTextIter start_iter, end_iter;
2074 RemoteURI *uri = NULL;
2076 if (textview_get_uri_range(textview, iter, tag, &start_iter,
2078 uri = textview_get_uri_from_range(textview, iter, tag,
2079 &start_iter, &end_iter);
2084 static gboolean textview_uri_button_pressed(GtkTextTag *tag, GObject *obj,
2085 GdkEvent *event, GtkTextIter *iter,
2088 GdkEventButton *bevent;
2089 RemoteURI *uri = NULL;
2094 if (event->type != GDK_BUTTON_PRESS && event->type != GDK_2BUTTON_PRESS
2095 && event->type != GDK_MOTION_NOTIFY)
2098 uri = textview_get_uri(textview, iter, tag);
2102 bevent = (GdkEventButton *) event;
2104 /* doubleclick: open compose / add address / browser */
2105 if ((event->type == GDK_BUTTON_PRESS && bevent->button == 1) ||
2106 bevent->button == 2 || bevent->button == 3) {
2107 if (!g_ascii_strncasecmp(uri->uri, "mailto:", 7)) {
2108 if (bevent->button == 3) {
2110 G_OBJECT(textview->mail_popup_menu),
2111 "menu_button", uri);
2112 gtk_menu_popup(GTK_MENU(textview->mail_popup_menu),
2113 NULL, NULL, NULL, NULL,
2114 bevent->button, bevent->time);
2116 PrefsAccount *account = NULL;
2118 if (textview->messageview && textview->messageview->msginfo &&
2119 textview->messageview->msginfo->folder) {
2120 FolderItem *folder_item;
2122 folder_item = textview->messageview->msginfo->folder;
2123 if (folder_item->prefs && folder_item->prefs->enable_default_account)
2124 account = account_find_from_id(folder_item->prefs->default_account);
2126 compose_new(account, uri->uri + 7, NULL);
2129 } else if (g_ascii_strncasecmp(uri->uri, "file:", 5)) {
2130 if (bevent->button == 1 &&
2131 textview_uri_security_check(textview, uri) == TRUE)
2133 prefs_common.uri_cmd);
2134 else if (bevent->button == 3) {
2136 G_OBJECT(textview->link_popup_menu),
2137 "menu_button", uri);
2138 gtk_menu_popup(GTK_MENU(textview->link_popup_menu),
2139 NULL, NULL, NULL, NULL,
2140 bevent->button, bevent->time);
2144 if (bevent->button == 3) {
2146 G_OBJECT(textview->file_popup_menu),
2147 "menu_button", uri);
2148 gtk_menu_popup(GTK_MENU(textview->file_popup_menu),
2149 NULL, NULL, NULL, NULL,
2150 bevent->button, bevent->time);
2160 *\brief Check to see if a web URL has been disguised as a different
2161 * URL (possible with HTML email).
2163 *\param uri The uri to check
2165 *\param textview The TextView the URL is contained in
2167 *\return gboolean TRUE if the URL is ok, or if the user chose to open
2168 * it anyway, otherwise FALSE
2170 static gboolean textview_uri_security_check(TextView *textview, RemoteURI *uri)
2173 gboolean retval = TRUE;
2174 GtkTextBuffer *buffer;
2175 GtkTextIter start, end;
2177 if (is_uri_string(uri->uri) == FALSE)
2180 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text));
2182 gtk_text_buffer_get_iter_at_offset(buffer, &start, uri->start);
2183 gtk_text_buffer_get_iter_at_offset(buffer, &end, uri->end);
2185 visible_str = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
2187 if (visible_str == NULL)
2190 if (strcmp(visible_str, uri->uri) != 0 && is_uri_string(visible_str)) {
2192 gchar *visible_uri_path;
2194 uri_path = get_uri_path(uri->uri);
2195 visible_uri_path = get_uri_path(visible_str);
2196 if (strcmp(uri_path, visible_uri_path) != 0)
2200 if (retval == FALSE) {
2204 msg = g_strdup_printf(_("The real URL (%s) is different from\n"
2205 "the apparent URL (%s).\n"
2207 uri->uri, visible_str);
2208 aval = alertpanel_with_type(_("Warning"), msg,
2209 GTK_STOCK_YES, GTK_STOCK_NO,
2210 NULL, NULL, ALERT_WARNING);
2212 if (aval == G_ALERTDEFAULT)
2216 g_free(visible_str);
2221 static void textview_uri_list_remove_all(GSList *uri_list)
2225 for (cur = uri_list; cur != NULL; cur = cur->next) {
2227 g_free(((RemoteURI *)cur->data)->uri);
2228 g_free(((RemoteURI *)cur->data)->filename);
2233 g_slist_free(uri_list);
2236 static void open_uri_cb (TextView *textview, guint action, void *data)
2238 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->link_popup_menu),
2243 if (textview_uri_security_check(textview, uri) == TRUE)
2245 prefs_common.uri_cmd);
2246 g_object_set_data(G_OBJECT(textview->link_popup_menu), "menu_button",
2250 static void save_file_cb (TextView *textview, guint action, void *data)
2252 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->file_popup_menu),
2254 gchar *filename = NULL;
2255 gchar *filepath = NULL;
2256 gchar *filedir = NULL;
2257 gchar *tmp_filename = NULL;
2261 if (uri->filename == NULL)
2264 filename = g_strdup(uri->filename);
2266 if (!g_utf8_validate(filename, -1, NULL)) {
2267 gchar *tmp = conv_filename_to_utf8(filename);
2272 subst_for_filename(filename);
2274 if (prefs_common.attach_save_dir)
2275 filepath = g_strconcat(prefs_common.attach_save_dir,
2276 G_DIR_SEPARATOR_S, filename, NULL);
2278 filepath = g_strdup(filename);
2282 filename = filesel_select_file_save(_("Save as"), filepath);
2288 if (is_file_exist(filename)) {
2292 res = g_strdup_printf(_("Overwrite existing file '%s'?"),
2294 aval = alertpanel(_("Overwrite"), res, GTK_STOCK_OK,
2295 GTK_STOCK_CANCEL, NULL);
2297 if (G_ALERTDEFAULT != aval)
2301 tmp_filename = g_filename_from_uri(uri->uri, NULL, NULL);
2302 copy_file(tmp_filename, filename, FALSE);
2303 g_free(tmp_filename);
2305 filedir = g_path_get_dirname(filename);
2306 if (filedir && strcmp(filedir, ".")) {
2307 if (prefs_common.attach_save_dir)
2308 g_free(prefs_common.attach_save_dir);
2309 prefs_common.attach_save_dir = g_strdup(filedir);
2315 g_object_set_data(G_OBJECT(textview->file_popup_menu), "menu_button",
2319 static void copy_uri_cb (TextView *textview, guint action, void *data)
2321 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->link_popup_menu),
2326 gtk_clipboard_set_text(gtk_clipboard_get(GDK_NONE), uri->uri, -1);
2327 g_object_set_data(G_OBJECT(textview->link_popup_menu), "menu_button",
2331 static void add_uri_to_addrbook_cb (TextView *textview, guint action, void *data)
2333 gchar *fromname, *fromaddress;
2334 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu),
2340 fromaddress = g_strdup(uri->uri + 7);
2341 /* Hiroyuki: please put this function in utils.c! */
2342 fromname = procheader_get_fromname(fromaddress);
2343 extract_address(fromaddress);
2344 g_message("adding from textview %s <%s>", fromname, fromaddress);
2345 /* Add to address book - Match */
2346 addressbook_add_contact( fromname, fromaddress, NULL );
2348 g_free(fromaddress);
2352 static void mail_to_uri_cb (TextView *textview, guint action, void *data)
2354 PrefsAccount *account = NULL;
2355 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu),
2360 if (textview->messageview && textview->messageview->msginfo &&
2361 textview->messageview->msginfo->folder) {
2362 FolderItem *folder_item;
2364 folder_item = textview->messageview->msginfo->folder;
2365 if (folder_item->prefs && folder_item->prefs->enable_default_account)
2366 account = account_find_from_id(folder_item->prefs->default_account);
2368 compose_new(account, uri->uri + 7, NULL);
2371 static void copy_mail_to_uri_cb (TextView *textview, guint action, void *data)
2373 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu),
2378 gtk_clipboard_set_text(gtk_clipboard_get(GDK_NONE), uri->uri + 7, -1);
2379 g_object_set_data(G_OBJECT(textview->mail_popup_menu), "menu_button",