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 with Web browser"), NULL, open_uri_cb, 0, NULL},
215 {N_("/Copy this _link"), NULL, copy_uri_cb, 0, NULL},
218 static GtkItemFactoryEntry textview_mail_popup_entries[] =
220 {N_("/Compose _new message"), NULL, mail_to_uri_cb, 0, NULL},
221 {N_("/Add to _address book"), NULL, add_uri_to_addrbook_cb, 0, NULL},
222 {N_("/Copy this add_ress"), 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,
379 g_signal_connect(G_OBJECT(tag), "event",
380 G_CALLBACK(textview_uri_button_pressed), textview);
383 void textview_init(TextView *textview)
386 hand_cursor = gdk_cursor_new(GDK_HAND2);
388 text_cursor = gdk_cursor_new(GDK_XTERM);
390 textview_reflect_prefs(textview);
391 textview_set_all_headers(textview, FALSE);
392 textview_set_font(textview, NULL);
393 textview_create_tags(GTK_TEXT_VIEW(textview->text), textview);
396 #define CHANGE_TAG_COLOR(tagname, color) { \
397 tag = gtk_text_tag_table_lookup(tags, tagname); \
399 g_object_set(G_OBJECT(tag), "foreground-gdk", color, NULL); \
402 static void textview_update_message_colors(TextView *textview)
404 GdkColor black = {0, 0, 0, 0};
405 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text));
407 GtkTextTagTable *tags = gtk_text_buffer_get_tag_table(buffer);
408 GtkTextTag *tag = NULL;
410 if (prefs_common.enable_color) {
411 /* grab the quote colors, converting from an int to a GdkColor */
412 gtkut_convert_int_to_gdk_color(prefs_common.quote_level1_col,
414 gtkut_convert_int_to_gdk_color(prefs_common.quote_level2_col,
416 gtkut_convert_int_to_gdk_color(prefs_common.quote_level3_col,
418 gtkut_convert_int_to_gdk_color(prefs_common.uri_col,
420 gtkut_convert_int_to_gdk_color(prefs_common.signature_col,
423 quote_colors[0] = quote_colors[1] = quote_colors[2] =
424 uri_color = emphasis_color = signature_color = black;
426 CHANGE_TAG_COLOR("quote0", "e_colors[0]);
427 CHANGE_TAG_COLOR("quote1", "e_colors[1]);
428 CHANGE_TAG_COLOR("quote2", "e_colors[2]);
429 CHANGE_TAG_COLOR("emphasis", &emphasis_color);
430 CHANGE_TAG_COLOR("signature", &signature_color);
431 CHANGE_TAG_COLOR("link", &uri_color);
432 CHANGE_TAG_COLOR("link-hover", &uri_color);
435 #undef CHANGE_TAG_COLOR
437 void textview_reflect_prefs(TextView *textview)
439 textview_set_font(textview, NULL);
440 textview_update_message_colors(textview);
441 gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(textview->text),
442 prefs_common.textview_cursor_visible);
445 void textview_show_message(TextView *textview, MimeInfo *mimeinfo,
448 textview_clear(textview);
450 textview_add_parts(textview, mimeinfo);
452 textview_set_position(textview, 0);
455 void textview_show_part(TextView *textview, MimeInfo *mimeinfo, FILE *fp)
457 g_return_if_fail(mimeinfo != NULL);
458 g_return_if_fail(fp != NULL);
460 if ((mimeinfo->type == MIMETYPE_MULTIPART) ||
461 ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_ascii_strcasecmp(mimeinfo->subtype, "rfc822"))) {
462 textview_clear(textview);
463 textview_add_parts(textview, mimeinfo);
467 if (fseek(fp, mimeinfo->offset, SEEK_SET) < 0)
470 textview_clear(textview);
472 if (mimeinfo->type == MIMETYPE_MULTIPART)
473 textview_add_parts(textview, mimeinfo);
475 textview_write_body(textview, mimeinfo);
479 static void textview_add_part(TextView *textview, MimeInfo *mimeinfo)
482 GtkTextBuffer *buffer;
483 GtkTextIter iter, start_iter;
485 GPtrArray *headers = NULL;
490 g_return_if_fail(mimeinfo != NULL);
491 text = GTK_TEXT_VIEW(textview->text);
492 buffer = gtk_text_view_get_buffer(text);
493 charcount = gtk_text_buffer_get_char_count(buffer);
494 gtk_text_buffer_get_end_iter(buffer, &iter);
496 if (mimeinfo->type == MIMETYPE_MULTIPART) return;
498 if ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_ascii_strcasecmp(mimeinfo->subtype, "rfc822")) {
501 fp = fopen(mimeinfo->data.filename, "rb");
502 fseek(fp, mimeinfo->offset, SEEK_SET);
503 headers = textview_scan_header(textview, fp);
506 gtk_text_buffer_insert(buffer, &iter, "\n", 1);
507 textview_show_header(textview, headers);
508 procheader_header_array_destroy(headers);
514 name = procmime_mimeinfo_get_parameter(mimeinfo, "filename");
515 content_type = procmime_get_content_type_str(mimeinfo->type,
518 name = procmime_mimeinfo_get_parameter(mimeinfo, "name");
520 g_snprintf(buf, sizeof(buf), "\n[%s %s (%d bytes)]\n",
521 name, content_type, mimeinfo->length);
523 g_snprintf(buf, sizeof(buf), "\n[%s (%d bytes)]\n",
524 content_type, mimeinfo->length);
526 g_free(content_type);
528 if (mimeinfo->type != MIMETYPE_TEXT) {
529 gtk_text_buffer_insert(buffer, &iter, buf, -1);
530 if (mimeinfo->type == MIMETYPE_IMAGE &&
531 prefs_common.inline_img ) {
533 GError *error = NULL;
539 fp = fopen(mimeinfo->data.filename, "rb");
540 fseek(fp, mimeinfo->offset, SEEK_SET);
542 filename = procmime_get_tmp_file_name(mimeinfo);
543 if (procmime_get_part(filename, mimeinfo) < 0) {
544 g_warning("Can't get the image file.");
549 pixbuf = gdk_pixbuf_new_from_file(filename, &error);
551 g_warning("%s\n", error->message);
555 g_warning("Can't load the image.");
560 if (prefs_common.resize_img) {
561 int new_width, new_height;
563 image_viewer_get_resized_size(gdk_pixbuf_get_width(pixbuf),
564 gdk_pixbuf_get_height(pixbuf),
565 textview->scrolledwin->allocation.width - 100,
566 gdk_pixbuf_get_height(pixbuf),
567 &new_width, &new_height);
568 scaled = gdk_pixbuf_scale_simple
569 (pixbuf, new_width, new_height, GDK_INTERP_BILINEAR);
571 g_object_unref(pixbuf);
575 uri_str = g_filename_to_uri(filename, NULL, NULL);
577 uri = g_new(RemoteURI, 1);
579 uri->start = gtk_text_iter_get_offset(&iter);
581 gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf);
583 uri->end = uri->start + 1;
584 uri->filename = procmime_get_part_file_name(mimeinfo);
586 g_slist_append(textview->uri_list, uri);
588 gtk_text_buffer_insert(buffer, &iter, " ", 1);
589 gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, uri->start);
590 gtk_text_buffer_apply_tag_by_name(buffer, "link",
593 gtk_text_buffer_insert_pixbuf(buffer, &iter, pixbuf);
594 gtk_text_buffer_insert(buffer, &iter, " ", 1);
597 g_object_unref(pixbuf);
600 } else if (mimeinfo->disposition != DISPOSITIONTYPE_ATTACHMENT) {
601 if (prefs_common.display_header && (charcount > 0))
602 gtk_text_buffer_insert(buffer, &iter, "\n", 1);
604 textview_write_body(textview, mimeinfo);
608 static void recursive_add_parts(TextView *textview, GNode *node)
613 mimeinfo = (MimeInfo *) node->data;
615 textview_add_part(textview, mimeinfo);
617 if ((mimeinfo->type != MIMETYPE_MULTIPART) &&
618 (mimeinfo->type != MIMETYPE_MESSAGE))
621 if (g_ascii_strcasecmp(mimeinfo->subtype, "alternative") == 0) {
622 GNode * prefered_body;
630 prefered_body = NULL;
633 for (iter = g_node_first_child(node) ; iter != NULL ;
634 iter = g_node_next_sibling(iter)) {
639 submime = (MimeInfo *) iter->data;
640 if (submime->type == MIMETYPE_TEXT)
643 if (submime->subtype != NULL) {
644 if (g_ascii_strcasecmp(submime->subtype, "plain") == 0)
648 if (score > prefered_score) {
649 prefered_score = score;
650 prefered_body = iter;
654 if (prefered_body != NULL) {
655 recursive_add_parts(textview, prefered_body);
659 for (iter = g_node_first_child(node) ; iter != NULL ;
660 iter = g_node_next_sibling(iter)) {
661 recursive_add_parts(textview, iter);
666 static void textview_add_parts(TextView *textview, MimeInfo *mimeinfo)
668 g_return_if_fail(mimeinfo != NULL);
670 recursive_add_parts(textview, mimeinfo->node);
673 #define TEXT_INSERT(str) \
674 gtk_text_buffer_insert(buffer, &iter, str, -1)
676 void textview_show_error(TextView *textview)
679 GtkTextBuffer *buffer;
682 textview_set_font(textview, NULL);
683 textview_clear(textview);
685 text = GTK_TEXT_VIEW(textview->text);
686 buffer = gtk_text_view_get_buffer(text);
687 gtk_text_buffer_get_start_iter(buffer, &iter);
689 TEXT_INSERT(_("This message can't be displayed.\n"));
693 void textview_show_mime_part(TextView *textview, MimeInfo *partinfo)
696 GtkTextBuffer *buffer;
699 if (!partinfo) return;
701 textview_set_font(textview, NULL);
702 textview_clear(textview);
704 text = GTK_TEXT_VIEW(textview->text);
705 buffer = gtk_text_view_get_buffer(text);
706 gtk_text_buffer_get_start_iter(buffer, &iter);
708 TEXT_INSERT(_("The following can be performed on this part by "));
709 TEXT_INSERT(_("right-clicking the icon or list item:\n"));
711 TEXT_INSERT(_(" To save select 'Save as...' (Shortcut key: 'y')\n"));
712 TEXT_INSERT(_(" To display as text select 'Display as text' "));
713 TEXT_INSERT(_("(Shortcut key: 't')\n"));
714 TEXT_INSERT(_(" To open with an external program select 'Open' "));
715 TEXT_INSERT(_("(Shortcut key: 'l'),\n"));
716 TEXT_INSERT(_(" (alternately double-click, or click the middle "));
717 TEXT_INSERT(_("mouse button),\n"));
718 TEXT_INSERT(_(" or 'Open with...' (Shortcut key: 'o')\n"));
724 static void textview_write_body(TextView *textview, MimeInfo *mimeinfo)
729 const gchar *charset;
731 if (textview->messageview->forced_charset)
732 charset = textview->messageview->forced_charset;
734 charset = procmime_mimeinfo_get_parameter(mimeinfo, "charset");
736 textview_set_font(textview, charset);
738 conv = conv_code_converter_new(charset);
740 procmime_force_encoding(textview->messageview->forced_encoding);
742 textview->is_in_signature = FALSE;
744 procmime_decode_content(mimeinfo);
746 if (!g_ascii_strcasecmp(mimeinfo->subtype, "html") &&
747 prefs_common.render_html) {
750 filename = procmime_get_tmp_file_name(mimeinfo);
751 if (procmime_get_part(filename, mimeinfo) == 0) {
752 tmpfp = fopen(filename, "rb");
753 textview_show_html(textview, tmpfp, conv);
758 } else if (!g_ascii_strcasecmp(mimeinfo->subtype, "enriched")) {
761 filename = procmime_get_tmp_file_name(mimeinfo);
762 if (procmime_get_part(filename, mimeinfo) == 0) {
763 tmpfp = fopen(filename, "rb");
764 textview_show_ertf(textview, tmpfp, conv);
770 tmpfp = fopen(mimeinfo->data.filename, "rb");
771 fseek(tmpfp, mimeinfo->offset, SEEK_SET);
772 debug_print("Viewing text content of type: %s (length: %d)\n", mimeinfo->subtype, mimeinfo->length);
773 while ((fgets(buf, sizeof(buf), tmpfp) != NULL) &&
774 (ftell(tmpfp) <= mimeinfo->offset + mimeinfo->length))
775 textview_write_line(textview, buf, conv);
779 conv_code_converter_destroy(conv);
780 procmime_force_encoding(0);
783 static void textview_show_html(TextView *textview, FILE *fp,
789 parser = html_parser_new(fp, conv);
790 g_return_if_fail(parser != NULL);
792 while ((str = html_parse(parser)) != NULL) {
793 if (parser->state == HTML_HREF) {
794 /* first time : get and copy the URL */
795 if (parser->href == NULL) {
796 /* ALF - the sylpheed html parser returns an empty string,
797 * if still inside an <a>, but already parsed past HREF */
798 str = strtok(str, " ");
800 parser->href = g_strdup(str);
801 /* the URL may (or not) be followed by the
803 str = strtok(NULL, "");
807 textview_write_link(textview, str, parser->href, NULL);
809 textview_write_line(textview, str, NULL);
811 textview_write_line(textview, "\n", NULL);
812 html_parser_destroy(parser);
815 static void textview_show_ertf(TextView *textview, FILE *fp,
821 parser = ertf_parser_new(fp, conv);
822 g_return_if_fail(parser != NULL);
824 while ((str = ertf_parse(parser)) != NULL) {
825 textview_write_line(textview, str, NULL);
828 ertf_parser_destroy(parser);
831 /* get_uri_part() - retrieves a URI starting from scanpos.
832 Returns TRUE if succesful */
833 static gboolean get_uri_part(const gchar *start, const gchar *scanpos,
834 const gchar **bp, const gchar **ep)
838 g_return_val_if_fail(start != NULL, FALSE);
839 g_return_val_if_fail(scanpos != NULL, FALSE);
840 g_return_val_if_fail(bp != NULL, FALSE);
841 g_return_val_if_fail(ep != NULL, FALSE);
845 /* find end point of URI */
846 for (ep_ = scanpos; *ep_ != '\0'; ep_++) {
847 if (!isgraph(*(const guchar *)ep_) ||
848 !IS_ASCII(*(const guchar *)ep_) ||
849 strchr("[]{}()<>\"", *ep_))
853 /* no punctuation at end of string */
855 /* FIXME: this stripping of trailing punctuations may bite with other URIs.
856 * should pass some URI type to this function and decide on that whether
857 * to perform punctuation stripping */
859 #define IS_REAL_PUNCT(ch) (ispunct(ch) && ((ch) != '/'))
861 for (; ep_ - 1 > scanpos + 1 &&
862 IS_REAL_PUNCT(*(const guchar *)(ep_ - 1));
873 static gchar *make_uri_string(const gchar *bp, const gchar *ep)
875 return g_strndup(bp, ep - bp);
878 /* valid mail address characters */
879 #define IS_RFC822_CHAR(ch) \
884 !strchr("(),;<>\"", (ch)))
886 /* alphabet and number within 7bit ASCII */
887 #define IS_ASCII_ALNUM(ch) (IS_ASCII(ch) && isalnum(ch))
888 #define IS_QUOTE(ch) ((ch) == '\'' || (ch) == '"')
890 static GHashTable *create_domain_tab(void)
892 static const gchar *toplvl_domains [] = {
894 "arpa", "coop", "info", "name", "biz", "com", "edu", "gov",
895 "int", "mil", "net", "org", "ac", "ad", "ae", "af", "ag",
896 "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au",
897 "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi",
898 "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by",
899 "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl",
900 "cm", "cn", "co", "cr", "cu", "cv", "cx", "cy", "cz", "de",
901 "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er",
902 "es", "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gd",
903 "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq",
904 "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr",
905 "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir",
906 "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki",
907 "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc",
908 "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc",
909 "md", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq",
910 "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na",
911 "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu",
912 "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm",
913 "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru",
914 "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj",
915 "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz",
916 "tc", "td", "tf", "tg", "th", "tj", "tk", "tm", "tn", "to",
917 "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um",
918 "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu",
919 "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw"
922 GHashTable *htab = g_hash_table_new(g_stricase_hash, g_stricase_equal);
924 g_return_val_if_fail(htab, NULL);
925 for (n = 0; n < sizeof toplvl_domains / sizeof toplvl_domains[0]; n++)
926 g_hash_table_insert(htab, (gpointer) toplvl_domains[n], (gpointer) toplvl_domains[n]);
930 static gboolean is_toplvl_domain(GHashTable *tab, const gchar *first, const gchar *last)
932 const gint MAX_LVL_DOM_NAME_LEN = 6;
933 gchar buf[MAX_LVL_DOM_NAME_LEN + 1];
934 const gchar *m = buf + MAX_LVL_DOM_NAME_LEN + 1;
937 if (last - first > MAX_LVL_DOM_NAME_LEN || first > last)
940 for (p = buf; p < m && first < last; *p++ = *first++)
944 return g_hash_table_lookup(tab, buf) != NULL;
947 /* get_email_part() - retrieves an email address. Returns TRUE if succesful */
948 static gboolean get_email_part(const gchar *start, const gchar *scanpos,
949 const gchar **bp, const gchar **ep)
951 /* more complex than the uri part because we need to scan back and forward starting from
952 * the scan position. */
953 gboolean result = FALSE;
954 const gchar *bp_ = NULL;
955 const gchar *ep_ = NULL;
956 static GHashTable *dom_tab;
957 const gchar *last_dot = NULL;
958 const gchar *prelast_dot = NULL;
959 const gchar *last_tld_char = NULL;
961 /* the informative part of the email address (describing the name
962 * of the email address owner) may contain quoted parts. the
963 * closure stack stores the last encountered quotes. */
964 gchar closure_stack[128];
965 gchar *ptr = closure_stack;
967 g_return_val_if_fail(start != NULL, FALSE);
968 g_return_val_if_fail(scanpos != NULL, FALSE);
969 g_return_val_if_fail(bp != NULL, FALSE);
970 g_return_val_if_fail(ep != NULL, FALSE);
973 dom_tab = create_domain_tab();
974 g_return_val_if_fail(dom_tab, FALSE);
976 /* scan start of address */
977 for (bp_ = scanpos - 1;
978 bp_ >= start && IS_RFC822_CHAR(*(const guchar *)bp_); bp_--)
981 /* TODO: should start with an alnum? */
983 for (; bp_ < scanpos && !IS_ASCII_ALNUM(*(const guchar *)bp_); bp_++)
986 if (bp_ != scanpos) {
987 /* scan end of address */
988 for (ep_ = scanpos + 1;
989 *ep_ && IS_RFC822_CHAR(*(const guchar *)ep_); ep_++)
991 prelast_dot = last_dot;
993 if (*(last_dot + 1) == '.') {
994 if (prelast_dot == NULL)
996 last_dot = prelast_dot;
1001 /* TODO: really should terminate with an alnum? */
1002 for (; ep_ > scanpos && !IS_ASCII_ALNUM(*(const guchar *)ep_);
1007 if (last_dot == NULL)
1009 if (last_dot >= ep_)
1010 last_dot = prelast_dot;
1011 if (last_dot == NULL || (scanpos + 1 >= last_dot))
1015 for (last_tld_char = last_dot; last_tld_char < ep_; last_tld_char++)
1016 if (*last_tld_char == '?')
1019 if (is_toplvl_domain(dom_tab, last_dot, last_tld_char))
1026 if (!result) return FALSE;
1028 if (*ep_ && *(bp_ - 1) == '"' && *(ep_) == '"'
1029 && *(ep_ + 1) == ' ' && *(ep_ + 2) == '<'
1030 && IS_RFC822_CHAR(*(ep_ + 3))) {
1031 /* this informative part with an @ in it is
1032 * followed by the email address */
1035 /* go to matching '>' (or next non-rfc822 char, like \n) */
1036 for (; *ep_ != '>' && *ep != '\0' && IS_RFC822_CHAR(*ep_); ep_++)
1039 /* include the bracket */
1040 if (*ep_ == '>') ep_++;
1042 /* include the leading quote */
1050 /* skip if it's between quotes "'alfons@proteus.demon.nl'" <alfons@proteus.demon.nl> */
1051 if (bp_ - 1 > start && IS_QUOTE(*(bp_ - 1)) && IS_QUOTE(*ep_))
1054 /* see if this is <bracketed>; in this case we also scan for the informative part. */
1055 if (bp_ - 1 <= start || *(bp_ - 1) != '<' || *ep_ != '>')
1058 #define FULL_STACK() ((size_t) (ptr - closure_stack) >= sizeof closure_stack)
1059 #define IN_STACK() (ptr > closure_stack)
1060 /* has underrun check */
1061 #define POP_STACK() if(IN_STACK()) --ptr
1062 /* has overrun check */
1063 #define PUSH_STACK(c) if(!FULL_STACK()) *ptr++ = (c); else return TRUE
1064 /* has underrun check */
1065 #define PEEK_STACK() (IN_STACK() ? *(ptr - 1) : 0)
1069 /* scan for the informative part. */
1070 for (bp_ -= 2; bp_ >= start; bp_--) {
1071 /* if closure on the stack keep scanning */
1072 if (PEEK_STACK() == *bp_) {
1076 if (*bp_ == '\'' || *bp_ == '"') {
1081 /* if nothing in the closure stack, do the special conditions
1082 * the following if..else expression simply checks whether
1083 * a token is acceptable. if not acceptable, the clause
1084 * should terminate the loop with a 'break' */
1085 if (!PEEK_STACK()) {
1087 && (((bp_ - 1) >= start) && isalnum(*(bp_ - 1)))
1088 && (((bp_ + 1) < ep_) && isalnum(*(bp_ + 1)))) {
1089 /* hyphens are allowed, but only in
1091 } else if (!strchr(",;:=?./+<>!&\r\n\t", *bp_)) {
1092 /* but anything not being a punctiation
1095 break; /* anything else is rejected */
1108 /* scan forward (should start with an alnum) */
1109 for (; *bp_ != '<' && isspace(*bp_) && *bp_ != '"'; bp_++)
1119 #undef IS_ASCII_ALNUM
1120 #undef IS_RFC822_CHAR
1122 static gchar *make_email_string(const gchar *bp, const gchar *ep)
1124 /* returns a mailto: URI; mailto: is also used to detect the
1125 * uri type later on in the button_pressed signal handler */
1129 tmp = g_strndup(bp, ep - bp);
1130 result = g_strconcat("mailto:", tmp, NULL);
1136 static gchar *make_http_string(const gchar *bp, const gchar *ep)
1138 /* returns an http: URI; */
1142 tmp = g_strndup(bp, ep - bp);
1143 result = g_strconcat("http://", tmp, NULL);
1149 #define ADD_TXT_POS(bp_, ep_, pti_) \
1150 if ((last->next = alloca(sizeof(struct txtpos))) != NULL) { \
1151 last = last->next; \
1152 last->bp = (bp_); last->ep = (ep_); last->pti = (pti_); \
1153 last->next = NULL; \
1155 g_warning("alloc error scanning URIs\n"); \
1156 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, \
1162 /* textview_make_clickable_parts() - colorizes clickable parts */
1163 static void textview_make_clickable_parts(TextView *textview,
1164 const gchar *fg_tag,
1165 const gchar *uri_tag,
1166 const gchar *linebuf)
1168 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1169 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1171 gchar *mybuf = g_strdup(linebuf);
1173 /* parse table - in order of priority */
1175 const gchar *needle; /* token */
1177 /* token search function */
1178 gchar *(*search) (const gchar *haystack,
1179 const gchar *needle);
1180 /* part parsing function */
1181 gboolean (*parse) (const gchar *start,
1182 const gchar *scanpos,
1185 /* part to URI function */
1186 gchar *(*build_uri) (const gchar *bp,
1190 static struct table parser[] = {
1191 {"http://", strcasestr, get_uri_part, make_uri_string},
1192 {"https://", strcasestr, get_uri_part, make_uri_string},
1193 {"ftp://", strcasestr, get_uri_part, make_uri_string},
1194 {"www.", strcasestr, get_uri_part, make_http_string},
1195 {"mailto:", strcasestr, get_uri_part, make_uri_string},
1196 {"@", strcasestr, get_email_part, make_email_string}
1198 const gint PARSE_ELEMS = sizeof parser / sizeof parser[0];
1201 const gchar *walk, *bp, *ep;
1204 const gchar *bp, *ep; /* text position */
1205 gint pti; /* index in parse table */
1206 struct txtpos *next; /* next */
1207 } head = {NULL, NULL, 0, NULL}, *last = &head;
1209 if (!g_utf8_validate(linebuf, -1, NULL)) {
1210 mybuf = g_malloc(strlen(linebuf)*2 +1);
1211 conv_localetodisp(mybuf, strlen(linebuf)*2 +1, linebuf);
1214 gtk_text_buffer_get_end_iter(buffer, &iter);
1216 /* parse for clickable parts, and build a list of begin and end positions */
1217 for (walk = mybuf, n = 0;;) {
1218 gint last_index = PARSE_ELEMS;
1219 gchar *scanpos = NULL;
1221 /* FIXME: this looks phony. scanning for anything in the parse table */
1222 for (n = 0; n < PARSE_ELEMS; n++) {
1225 tmp = parser[n].search(walk, parser[n].needle);
1227 if (scanpos == NULL || tmp < scanpos) {
1235 /* check if URI can be parsed */
1236 if (parser[last_index].parse(walk, scanpos, &bp, &ep)
1237 && (size_t) (ep - bp - 1) > strlen(parser[last_index].needle)) {
1238 ADD_TXT_POS(bp, ep, last_index);
1242 strlen(parser[last_index].needle);
1247 /* colorize this line */
1249 const gchar *normal_text = mybuf;
1252 for (last = head.next; last != NULL;
1253 normal_text = last->ep, last = last->next) {
1255 uri = g_new(RemoteURI, 1);
1256 if (last->bp - normal_text > 0)
1257 gtk_text_buffer_insert_with_tags_by_name
1260 last->bp - normal_text,
1262 uri->uri = parser[last->pti].build_uri(last->bp,
1264 uri->start = gtk_text_iter_get_offset(&iter);
1265 gtk_text_buffer_insert_with_tags_by_name
1266 (buffer, &iter, last->bp, last->ep - last->bp,
1267 uri_tag, fg_tag, NULL);
1268 uri->end = gtk_text_iter_get_offset(&iter);
1269 uri->filename = NULL;
1270 textview->uri_list =
1271 g_slist_append(textview->uri_list, uri);
1275 gtk_text_buffer_insert_with_tags_by_name
1276 (buffer, &iter, normal_text, -1, fg_tag, NULL);
1278 gtk_text_buffer_insert_with_tags_by_name
1279 (buffer, &iter, mybuf, -1, fg_tag, NULL);
1286 static void textview_write_line(TextView *textview, const gchar *str,
1287 CodeConverter *conv)
1290 GtkTextBuffer *buffer;
1292 gchar buf[BUFFSIZE];
1294 gint quotelevel = -1;
1295 gchar quote_tag_str[10];
1297 text = GTK_TEXT_VIEW(textview->text);
1298 buffer = gtk_text_view_get_buffer(text);
1299 gtk_text_buffer_get_end_iter(buffer, &iter);
1302 strncpy2(buf, str, sizeof(buf));
1303 else if (conv_convert(conv, buf, sizeof(buf), str) < 0)
1304 conv_localetodisp(buf, sizeof(buf), str);
1307 //if (prefs_common.conv_mb_alnum) conv_mb_alnum(buf);
1310 /* change color of quotation
1311 >, foo>, _> ... ok, <foo>, foo bar>, foo-> ... ng
1312 Up to 3 levels of quotations are detected, and each
1313 level is colored using a different color. */
1314 if (prefs_common.enable_color
1315 && line_has_quote_char(buf, prefs_common.quote_chars)) {
1316 quotelevel = get_quote_level(buf, prefs_common.quote_chars);
1318 /* set up the correct foreground color */
1319 if (quotelevel > 2) {
1320 /* recycle colors */
1321 if (prefs_common.recycle_quote_colors)
1328 if (quotelevel == -1)
1331 g_snprintf(quote_tag_str, sizeof(quote_tag_str),
1332 "quote%d", quotelevel);
1333 fg_color = quote_tag_str;
1336 if (prefs_common.enable_color && (strcmp(buf,"-- \n") == 0 || textview->is_in_signature)) {
1337 fg_color = "signature";
1338 textview->is_in_signature = TRUE;
1341 textview_make_clickable_parts(textview, fg_color, "link", buf);
1344 void textview_write_link(TextView *textview, const gchar *str,
1345 const gchar *uri, CodeConverter *conv)
1347 GdkColor *link_color = NULL;
1349 GtkTextBuffer *buffer;
1351 gchar buf[BUFFSIZE];
1355 if (!str || *str == '\0')
1360 text = GTK_TEXT_VIEW(textview->text);
1361 buffer = gtk_text_view_get_buffer(text);
1362 gtk_text_buffer_get_end_iter(buffer, &iter);
1365 strncpy2(buf, str, sizeof(buf));
1366 else if (conv_convert(conv, buf, sizeof(buf), str) < 0)
1367 conv_utf8todisp(buf, sizeof(buf), str);
1369 if (g_utf8_validate(buf, -1, NULL) == FALSE)
1374 gtk_text_buffer_get_end_iter(buffer, &iter);
1375 for (bufp = buf; *bufp != '\0'; bufp = g_utf8_next_char(bufp)) {
1378 ch = g_utf8_get_char(bufp);
1379 if (!g_unichar_isspace(ch))
1383 gtk_text_buffer_insert(buffer, &iter, buf, bufp - buf);
1385 if (prefs_common.enable_color) {
1386 link_color = &uri_color;
1388 r_uri = g_new(RemoteURI, 1);
1389 r_uri->uri = g_strdup(uri);
1390 r_uri->start = gtk_text_iter_get_offset(&iter);
1391 gtk_text_buffer_insert_with_tags_by_name
1392 (buffer, &iter, bufp, -1, "link", NULL);
1393 r_uri->end = gtk_text_iter_get_offset(&iter);
1394 r_uri->filename = NULL;
1395 textview->uri_list = g_slist_append(textview->uri_list, r_uri);
1398 void textview_clear(TextView *textview)
1400 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1401 GtkTextBuffer *buffer;
1403 buffer = gtk_text_view_get_buffer(text);
1404 gtk_text_buffer_set_text(buffer, "", -1);
1406 TEXTVIEW_STATUSBAR_POP(textview);
1407 textview_uri_list_remove_all(textview->uri_list);
1408 textview->uri_list = NULL;
1410 textview->body_pos = 0;
1413 void textview_destroy(TextView *textview)
1415 textview_uri_list_remove_all(textview->uri_list);
1416 textview->uri_list = NULL;
1421 void textview_set_all_headers(TextView *textview, gboolean all_headers)
1423 textview->show_all_headers = all_headers;
1426 void textview_set_font(TextView *textview, const gchar *codeset)
1428 if (prefs_common.textfont) {
1429 PangoFontDescription *font_desc = NULL;
1431 font_desc = pango_font_description_from_string
1432 (prefs_common.textfont);
1434 gtk_widget_modify_font(textview->text, font_desc);
1435 pango_font_description_free(font_desc);
1438 gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(textview->text),
1439 prefs_common.line_space / 2);
1440 gtk_text_view_set_pixels_below_lines(GTK_TEXT_VIEW(textview->text),
1441 prefs_common.line_space / 2);
1444 void textview_set_text(TextView *textview, const gchar *text)
1447 GtkTextBuffer *buffer;
1449 g_return_if_fail(textview != NULL);
1450 g_return_if_fail(text != NULL);
1452 textview_clear(textview);
1454 view = GTK_TEXT_VIEW(textview->text);
1455 buffer = gtk_text_view_get_buffer(view);
1456 gtk_text_buffer_set_text(buffer, text, strlen(text));
1472 H_ORGANIZATION = 11,
1475 void textview_set_position(TextView *textview, gint pos)
1477 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1478 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1481 gtk_text_buffer_get_iter_at_offset(buffer, &iter, pos);
1482 gtk_text_buffer_place_cursor(buffer, &iter);
1483 gtk_text_view_scroll_to_iter(text, &iter, 0.0, FALSE, 0.0, 0.0);
1486 static GPtrArray *textview_scan_header(TextView *textview, FILE *fp)
1488 gchar buf[BUFFSIZE];
1489 GPtrArray *headers, *sorted_headers;
1490 GSList *disphdr_list;
1494 g_return_val_if_fail(fp != NULL, NULL);
1496 if (textview->show_all_headers)
1497 return procheader_get_header_array_asis(fp);
1499 if (!prefs_common.display_header) {
1500 while (fgets(buf, sizeof(buf), fp) != NULL)
1501 if (buf[0] == '\r' || buf[0] == '\n') break;
1505 headers = procheader_get_header_array_asis(fp);
1507 sorted_headers = g_ptr_array_new();
1509 for (disphdr_list = prefs_common.disphdr_list; disphdr_list != NULL;
1510 disphdr_list = disphdr_list->next) {
1511 DisplayHeaderProp *dp =
1512 (DisplayHeaderProp *)disphdr_list->data;
1514 for (i = 0; i < headers->len; i++) {
1515 header = g_ptr_array_index(headers, i);
1517 if (procheader_headername_equal(header->name,
1520 procheader_header_free(header);
1522 g_ptr_array_add(sorted_headers, header);
1524 g_ptr_array_remove_index(headers, i);
1530 if (prefs_common.show_other_header) {
1531 for (i = 0; i < headers->len; i++) {
1532 header = g_ptr_array_index(headers, i);
1533 g_ptr_array_add(sorted_headers, header);
1535 g_ptr_array_free(headers, TRUE);
1537 procheader_header_array_destroy(headers);
1540 return sorted_headers;
1543 static void textview_show_header(TextView *textview, GPtrArray *headers)
1545 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1546 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1551 g_return_if_fail(headers != NULL);
1553 for (i = 0; i < headers->len; i++) {
1554 header = g_ptr_array_index(headers, i);
1555 g_return_if_fail(header->name != NULL);
1557 gtk_text_buffer_get_end_iter (buffer, &iter);
1558 gtk_text_buffer_insert_with_tags_by_name
1559 (buffer, &iter, header->name, -1,
1560 "header_title", "header", NULL);
1561 if (header->name[strlen(header->name) - 1] != ' ')
1562 gtk_text_buffer_insert_with_tags_by_name
1563 (buffer, &iter, " ", 1,
1564 "header_title", "header", NULL);
1566 if (procheader_headername_equal(header->name, "Subject") ||
1567 procheader_headername_equal(header->name, "From") ||
1568 procheader_headername_equal(header->name, "To") ||
1569 procheader_headername_equal(header->name, "Cc"))
1570 unfold_line(header->body);
1572 if (prefs_common.enable_color &&
1573 (procheader_headername_equal(header->name, "X-Mailer") ||
1574 procheader_headername_equal(header->name,
1576 strstr(header->body, "Sylpheed") != NULL) {
1577 gtk_text_buffer_get_end_iter (buffer, &iter);
1578 gtk_text_buffer_insert_with_tags_by_name
1579 (buffer, &iter, header->body, -1,
1580 "header", "emphasis", NULL);
1582 textview_make_clickable_parts(textview, "header", "link",
1584 gtk_text_buffer_get_end_iter (buffer, &iter);
1585 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, "\n", 1,
1590 gboolean textview_search_string(TextView *textview, const gchar *str,
1593 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1594 GtkTextBuffer *buffer;
1595 GtkTextIter iter, match_pos;
1599 g_return_val_if_fail(str != NULL, FALSE);
1601 buffer = gtk_text_view_get_buffer(text);
1603 len = g_utf8_strlen(str, -1);
1604 g_return_val_if_fail(len >= 0, FALSE);
1606 mark = gtk_text_buffer_get_insert(buffer);
1607 gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
1609 if (gtkut_text_buffer_find(buffer, &iter, str, case_sens,
1611 GtkTextIter end = match_pos;
1613 gtk_text_iter_forward_chars(&end, len);
1614 /* place "insert" at the last character */
1615 gtk_text_buffer_select_range(buffer, &end, &match_pos);
1616 gtk_text_view_scroll_to_mark(text, mark, 0.0, FALSE, 0.0, 0.0);
1623 gboolean textview_search_string_backward(TextView *textview, const gchar *str,
1626 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1627 GtkTextBuffer *buffer;
1628 GtkTextIter iter, match_pos;
1632 g_return_val_if_fail(str != NULL, FALSE);
1634 buffer = gtk_text_view_get_buffer(text);
1636 len = g_utf8_strlen(str, -1);
1637 g_return_val_if_fail(len >= 0, FALSE);
1639 mark = gtk_text_buffer_get_insert(buffer);
1640 gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
1642 if (gtkut_text_buffer_find_backward(buffer, &iter, str, case_sens,
1644 GtkTextIter end = match_pos;
1646 gtk_text_iter_forward_chars(&end, len);
1647 gtk_text_buffer_select_range(buffer, &match_pos, &end);
1648 gtk_text_view_scroll_to_mark(text, mark, 0.0, FALSE, 0.0, 0.0);
1655 void textview_scroll_one_line(TextView *textview, gboolean up)
1657 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1658 GtkAdjustment *vadj = text->vadjustment;
1661 if (prefs_common.enable_smooth_scroll) {
1662 textview_smooth_scroll_one_line(textview, up);
1667 upper = vadj->upper - vadj->page_size;
1668 if (vadj->value < upper) {
1669 vadj->value += vadj->step_increment;
1670 vadj->value = MIN(vadj->value, upper);
1671 g_signal_emit_by_name(G_OBJECT(vadj),
1672 "value_changed", 0);
1675 if (vadj->value > 0.0) {
1676 vadj->value -= vadj->step_increment;
1677 vadj->value = MAX(vadj->value, 0.0);
1678 g_signal_emit_by_name(G_OBJECT(vadj),
1679 "value_changed", 0);
1684 gboolean textview_scroll_page(TextView *textview, gboolean up)
1686 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1687 GtkAdjustment *vadj = text->vadjustment;
1691 if (prefs_common.enable_smooth_scroll)
1692 return textview_smooth_scroll_page(textview, up);
1694 if (prefs_common.scroll_halfpage)
1695 page_incr = vadj->page_increment / 2;
1697 page_incr = vadj->page_increment;
1700 upper = vadj->upper - vadj->page_size;
1701 if (vadj->value < upper) {
1702 vadj->value += page_incr;
1703 vadj->value = MIN(vadj->value, upper);
1704 g_signal_emit_by_name(G_OBJECT(vadj),
1705 "value_changed", 0);
1709 if (vadj->value > 0.0) {
1710 vadj->value -= page_incr;
1711 vadj->value = MAX(vadj->value, 0.0);
1712 g_signal_emit_by_name(G_OBJECT(vadj),
1713 "value_changed", 0);
1721 static void textview_smooth_scroll_do(TextView *textview,
1722 gfloat old_value, gfloat last_value,
1725 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1726 GtkAdjustment *vadj = text->vadjustment;
1731 if (old_value < last_value) {
1732 change_value = last_value - old_value;
1735 change_value = old_value - last_value;
1739 for (i = step; i <= change_value; i += step) {
1740 vadj->value = old_value + (up ? -i : i);
1741 g_signal_emit_by_name(G_OBJECT(vadj),
1742 "value_changed", 0);
1745 vadj->value = last_value;
1746 g_signal_emit_by_name(G_OBJECT(vadj), "value_changed", 0);
1748 gtk_widget_queue_draw(GTK_WIDGET(text));
1751 static void textview_smooth_scroll_one_line(TextView *textview, gboolean up)
1753 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1754 GtkAdjustment *vadj = text->vadjustment;
1760 upper = vadj->upper - vadj->page_size;
1761 if (vadj->value < upper) {
1762 old_value = vadj->value;
1763 last_value = vadj->value + vadj->step_increment;
1764 last_value = MIN(last_value, upper);
1766 textview_smooth_scroll_do(textview, old_value,
1768 prefs_common.scroll_step);
1771 if (vadj->value > 0.0) {
1772 old_value = vadj->value;
1773 last_value = vadj->value - vadj->step_increment;
1774 last_value = MAX(last_value, 0.0);
1776 textview_smooth_scroll_do(textview, old_value,
1778 prefs_common.scroll_step);
1783 static gboolean textview_smooth_scroll_page(TextView *textview, gboolean up)
1785 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1786 GtkAdjustment *vadj = text->vadjustment;
1792 if (prefs_common.scroll_halfpage)
1793 page_incr = vadj->page_increment / 2;
1795 page_incr = vadj->page_increment;
1798 upper = vadj->upper - vadj->page_size;
1799 if (vadj->value < upper) {
1800 old_value = vadj->value;
1801 last_value = vadj->value + page_incr;
1802 last_value = MIN(last_value, upper);
1804 textview_smooth_scroll_do(textview, old_value,
1806 prefs_common.scroll_step);
1810 if (vadj->value > 0.0) {
1811 old_value = vadj->value;
1812 last_value = vadj->value - page_incr;
1813 last_value = MAX(last_value, 0.0);
1815 textview_smooth_scroll_do(textview, old_value,
1817 prefs_common.scroll_step);
1825 #define KEY_PRESS_EVENT_STOP() \
1826 g_signal_stop_emission_by_name(G_OBJECT(widget), \
1829 static gint textview_key_pressed(GtkWidget *widget, GdkEventKey *event,
1832 SummaryView *summaryview = NULL;
1833 MessageView *messageview = textview->messageview;
1835 if (!event) return FALSE;
1836 if (messageview->mainwin)
1837 summaryview = messageview->mainwin->summaryview;
1839 switch (event->keyval) {
1854 summary_pass_key_press_event(summaryview, event);
1856 textview_scroll_page
1859 (GDK_SHIFT_MASK|GDK_MOD1_MASK)) != 0);
1862 textview_scroll_page(textview, TRUE);
1865 textview_scroll_one_line
1866 (textview, (event->state &
1867 (GDK_SHIFT_MASK|GDK_MOD1_MASK)) != 0);
1871 summary_pass_key_press_event(summaryview, event);
1876 if ((event->state & (GDK_MOD1_MASK|GDK_CONTROL_MASK)) == 0) {
1877 KEY_PRESS_EVENT_STOP();
1878 mimeview_pass_key_press_event(messageview->mimeview,
1882 /* possible fall through */
1885 event->window != messageview->mainwin->window->window) {
1886 GdkEventKey tmpev = *event;
1888 tmpev.window = messageview->mainwin->window->window;
1889 KEY_PRESS_EVENT_STOP();
1890 gtk_widget_event(messageview->mainwin->window,
1891 (GdkEvent *)&tmpev);
1899 static gboolean textview_motion_notify(GtkWidget *widget,
1900 GdkEventMotion *event,
1903 textview_uri_update(textview, event->x, event->y);
1904 gdk_window_get_pointer(widget->window, NULL, NULL, NULL);
1909 static gboolean textview_leave_notify(GtkWidget *widget,
1910 GdkEventCrossing *event,
1913 textview_uri_update(textview, -1, -1);
1918 static gboolean textview_visibility_notify(GtkWidget *widget,
1919 GdkEventVisibility *event,
1925 window = gtk_text_view_get_window(GTK_TEXT_VIEW(widget),
1926 GTK_TEXT_WINDOW_TEXT);
1928 /* check if occurred for the text window part */
1929 if (window != event->window)
1932 gdk_window_get_pointer(widget->window, &wx, &wy, NULL);
1933 textview_uri_update(textview, wx, wy);
1938 static void textview_uri_update(TextView *textview, gint x, gint y)
1940 GtkTextBuffer *buffer;
1941 GtkTextIter start_iter, end_iter;
1942 RemoteURI *uri = NULL;
1944 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text));
1946 if (x != -1 && y != -1) {
1952 gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(textview->text),
1953 GTK_TEXT_WINDOW_WIDGET,
1955 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(textview->text),
1958 tags = gtk_text_iter_get_tags(&iter);
1959 for (cur = tags; cur != NULL; cur = cur->next) {
1960 GtkTextTag *tag = cur->data;
1963 g_object_get(G_OBJECT(tag), "name", &name, NULL);
1964 if (!strcmp(name, "link")
1965 && textview_get_uri_range(textview, &iter, tag,
1966 &start_iter, &end_iter))
1967 uri = textview_get_uri_from_range(textview,
1979 if (uri != textview->uri_hover) {
1982 if (textview->uri_hover)
1983 gtk_text_buffer_remove_tag_by_name(buffer,
1985 &textview->uri_hover_start_iter,
1986 &textview->uri_hover_end_iter);
1988 textview->uri_hover = uri;
1990 textview->uri_hover_start_iter = start_iter;
1991 textview->uri_hover_end_iter = end_iter;
1994 window = gtk_text_view_get_window(GTK_TEXT_VIEW(textview->text),
1995 GTK_TEXT_WINDOW_TEXT);
1996 gdk_window_set_cursor(window, uri ? hand_cursor : text_cursor);
1998 TEXTVIEW_STATUSBAR_POP(textview);
2003 gtk_text_buffer_apply_tag_by_name(buffer,
2008 trimmed_uri = trim_string(uri->uri, 60);
2009 TEXTVIEW_STATUSBAR_PUSH(textview, trimmed_uri);
2010 g_free(trimmed_uri);
2015 static gboolean textview_get_uri_range(TextView *textview,
2018 GtkTextIter *start_iter,
2019 GtkTextIter *end_iter)
2021 GtkTextIter _start_iter, _end_iter;
2024 if (!gtk_text_iter_forward_to_tag_toggle(&_end_iter, tag)) {
2025 debug_print("Can't find end");
2029 _start_iter = _end_iter;
2030 if (!gtk_text_iter_backward_to_tag_toggle(&_start_iter, tag)) {
2031 debug_print("Can't find start.");
2035 *start_iter = _start_iter;
2036 *end_iter = _end_iter;
2041 static RemoteURI *textview_get_uri_from_range(TextView *textview,
2044 GtkTextIter *start_iter,
2045 GtkTextIter *end_iter)
2047 gint start_pos, end_pos, cur_pos;
2048 RemoteURI *uri = NULL;
2051 start_pos = gtk_text_iter_get_offset(start_iter);
2052 end_pos = gtk_text_iter_get_offset(end_iter);
2053 cur_pos = gtk_text_iter_get_offset(iter);
2055 for (cur = textview->uri_list; cur != NULL; cur = cur->next) {
2056 RemoteURI *uri_ = (RemoteURI *)cur->data;
2057 if (start_pos == uri_->start &&
2058 end_pos == uri_->end) {
2061 } else if (start_pos == uri_->start ||
2062 end_pos == uri_->end) {
2063 /* in case of contiguous links, textview_get_uri_range
2064 * returns a broader range (start of 1st link to end
2066 * In that case, correct link is the one covering
2069 if (uri_->start <= cur_pos && cur_pos <= uri_->end) {
2079 static RemoteURI *textview_get_uri(TextView *textview,
2083 GtkTextIter start_iter, end_iter;
2084 RemoteURI *uri = NULL;
2086 if (textview_get_uri_range(textview, iter, tag, &start_iter,
2088 uri = textview_get_uri_from_range(textview, iter, tag,
2089 &start_iter, &end_iter);
2094 static gboolean textview_uri_button_pressed(GtkTextTag *tag, GObject *obj,
2095 GdkEvent *event, GtkTextIter *iter,
2098 GdkEventButton *bevent;
2099 RemoteURI *uri = NULL;
2104 if (event->type != GDK_BUTTON_PRESS && event->type != GDK_2BUTTON_PRESS
2105 && event->type != GDK_MOTION_NOTIFY)
2108 uri = textview_get_uri(textview, iter, tag);
2112 bevent = (GdkEventButton *) event;
2114 /* doubleclick: open compose / add address / browser */
2115 if ((event->type == GDK_BUTTON_PRESS && bevent->button == 1) ||
2116 bevent->button == 2 || bevent->button == 3) {
2117 if (!g_ascii_strncasecmp(uri->uri, "mailto:", 7)) {
2118 if (bevent->button == 3) {
2120 G_OBJECT(textview->mail_popup_menu),
2121 "menu_button", uri);
2122 gtk_menu_popup(GTK_MENU(textview->mail_popup_menu),
2123 NULL, NULL, NULL, NULL,
2124 bevent->button, bevent->time);
2126 PrefsAccount *account = NULL;
2128 if (textview->messageview && textview->messageview->msginfo &&
2129 textview->messageview->msginfo->folder) {
2130 FolderItem *folder_item;
2132 folder_item = textview->messageview->msginfo->folder;
2133 if (folder_item->prefs && folder_item->prefs->enable_default_account)
2134 account = account_find_from_id(folder_item->prefs->default_account);
2136 compose_new(account, uri->uri + 7, NULL);
2139 } else if (g_ascii_strncasecmp(uri->uri, "file:", 5)) {
2140 if (bevent->button == 1 &&
2141 textview_uri_security_check(textview, uri) == TRUE)
2143 prefs_common.uri_cmd);
2144 else if (bevent->button == 3) {
2146 G_OBJECT(textview->link_popup_menu),
2147 "menu_button", uri);
2148 gtk_menu_popup(GTK_MENU(textview->link_popup_menu),
2149 NULL, NULL, NULL, NULL,
2150 bevent->button, bevent->time);
2154 if (bevent->button == 3) {
2156 G_OBJECT(textview->file_popup_menu),
2157 "menu_button", uri);
2158 gtk_menu_popup(GTK_MENU(textview->file_popup_menu),
2159 NULL, NULL, NULL, NULL,
2160 bevent->button, bevent->time);
2170 *\brief Check to see if a web URL has been disguised as a different
2171 * URL (possible with HTML email).
2173 *\param uri The uri to check
2175 *\param textview The TextView the URL is contained in
2177 *\return gboolean TRUE if the URL is ok, or if the user chose to open
2178 * it anyway, otherwise FALSE
2180 static gboolean textview_uri_security_check(TextView *textview, RemoteURI *uri)
2183 gboolean retval = TRUE;
2184 GtkTextBuffer *buffer;
2185 GtkTextIter start, end;
2187 if (is_uri_string(uri->uri) == FALSE)
2190 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text));
2192 gtk_text_buffer_get_iter_at_offset(buffer, &start, uri->start);
2193 gtk_text_buffer_get_iter_at_offset(buffer, &end, uri->end);
2195 visible_str = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
2197 if (visible_str == NULL)
2200 if (strcmp(visible_str, uri->uri) != 0 && is_uri_string(visible_str)) {
2202 gchar *visible_uri_path;
2204 uri_path = get_uri_path(uri->uri);
2205 visible_uri_path = get_uri_path(visible_str);
2206 if (strcmp(uri_path, visible_uri_path) != 0)
2210 if (retval == FALSE) {
2214 msg = g_strdup_printf(_("The real URL (%s) is different from\n"
2215 "the apparent URL (%s).\n"
2218 uri->uri, visible_str);
2219 aval = alertpanel_full(_("Fake URL warning"), msg,
2220 GTK_STOCK_YES, GTK_STOCK_NO, NULL, FALSE,
2221 NULL, ALERT_WARNING, G_ALERTALTERNATE);
2223 if (aval == G_ALERTDEFAULT)
2227 g_free(visible_str);
2232 static void textview_uri_list_remove_all(GSList *uri_list)
2236 for (cur = uri_list; cur != NULL; cur = cur->next) {
2238 g_free(((RemoteURI *)cur->data)->uri);
2239 g_free(((RemoteURI *)cur->data)->filename);
2244 g_slist_free(uri_list);
2247 static void open_uri_cb (TextView *textview, guint action, void *data)
2249 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->link_popup_menu),
2254 if (textview_uri_security_check(textview, uri) == TRUE)
2256 prefs_common.uri_cmd);
2257 g_object_set_data(G_OBJECT(textview->link_popup_menu), "menu_button",
2261 static void save_file_cb (TextView *textview, guint action, void *data)
2263 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->file_popup_menu),
2265 gchar *filename = NULL;
2266 gchar *filepath = NULL;
2267 gchar *filedir = NULL;
2268 gchar *tmp_filename = NULL;
2272 if (uri->filename == NULL)
2275 filename = g_strdup(uri->filename);
2277 if (!g_utf8_validate(filename, -1, NULL)) {
2278 gchar *tmp = conv_filename_to_utf8(filename);
2283 subst_for_filename(filename);
2285 if (prefs_common.attach_save_dir)
2286 filepath = g_strconcat(prefs_common.attach_save_dir,
2287 G_DIR_SEPARATOR_S, filename, NULL);
2289 filepath = g_strdup(filename);
2293 filename = filesel_select_file_save(_("Save as"), filepath);
2299 if (is_file_exist(filename)) {
2303 res = g_strdup_printf(_("Overwrite existing file '%s'?"),
2305 aval = alertpanel(_("Overwrite"), res, GTK_STOCK_OK,
2306 GTK_STOCK_CANCEL, NULL);
2308 if (G_ALERTDEFAULT != aval)
2312 tmp_filename = g_filename_from_uri(uri->uri, NULL, NULL);
2313 copy_file(tmp_filename, filename, FALSE);
2314 g_free(tmp_filename);
2316 filedir = g_path_get_dirname(filename);
2317 if (filedir && strcmp(filedir, ".")) {
2318 if (prefs_common.attach_save_dir)
2319 g_free(prefs_common.attach_save_dir);
2320 prefs_common.attach_save_dir = g_strdup(filedir);
2326 g_object_set_data(G_OBJECT(textview->file_popup_menu), "menu_button",
2330 static void copy_uri_cb (TextView *textview, guint action, void *data)
2332 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->link_popup_menu),
2337 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), uri->uri, -1);
2338 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), uri->uri, -1);
2339 g_object_set_data(G_OBJECT(textview->link_popup_menu), "menu_button",
2343 static void add_uri_to_addrbook_cb (TextView *textview, guint action, void *data)
2345 gchar *fromname, *fromaddress;
2346 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu),
2352 fromaddress = g_strdup(uri->uri + 7);
2353 /* Hiroyuki: please put this function in utils.c! */
2354 fromname = procheader_get_fromname(fromaddress);
2355 extract_address(fromaddress);
2356 g_message("adding from textview %s <%s>", fromname, fromaddress);
2357 /* Add to address book - Match */
2358 addressbook_add_contact( fromname, fromaddress, NULL );
2360 g_free(fromaddress);
2364 static void mail_to_uri_cb (TextView *textview, guint action, void *data)
2366 PrefsAccount *account = NULL;
2367 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu),
2372 if (textview->messageview && textview->messageview->msginfo &&
2373 textview->messageview->msginfo->folder) {
2374 FolderItem *folder_item;
2376 folder_item = textview->messageview->msginfo->folder;
2377 if (folder_item->prefs && folder_item->prefs->enable_default_account)
2378 account = account_find_from_id(folder_item->prefs->default_account);
2380 compose_new(account, uri->uri + 7, NULL);
2383 static void copy_mail_to_uri_cb (TextView *textview, guint action, void *data)
2385 RemoteURI *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu),
2390 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), uri->uri +7, -1);
2391 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), uri->uri +7, -1);
2392 g_object_set_data(G_OBJECT(textview->mail_popup_menu), "menu_button",