ce2ad8d90dbf4ac7c4249f0cd11c34e158c29af3
[claws.git] / src / textview.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999,2000 Hiroyuki Yamamoto
4  *
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.
9  *
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.
14  *
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.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #  include "config.h"
22 #endif
23
24 #include "defs.h"
25
26 #include <glib.h>
27 #include <gdk/gdk.h>
28 #include <gdk/gdkkeysyms.h>
29 #include <gtk/gtkvbox.h>
30 #include <gtk/gtkscrolledwindow.h>
31 #include <gtk/gtktext.h>
32 #include <gtk/gtksignal.h>
33 #include <stdio.h>
34 #include <ctype.h>
35 #include <string.h>
36 #include <stdlib.h>
37
38 #include "intl.h"
39 #include "main.h"
40 #include "summaryview.h"
41 #include "procheader.h"
42 #include "prefs_common.h"
43 #include "codeconv.h"
44 #include "utils.h"
45 #include "gtkutils.h"
46 #include "procmime.h"
47 #include "html.h"
48 #include "compose.h"
49
50 #define FONT_LOAD(font, s) \
51 { \
52         gchar *fontstr, *p; \
53  \
54         Xstrdup_a(fontstr, s, ); \
55         if ((p = strchr(fontstr, ',')) != NULL) *p = '\0'; \
56         font = gdk_font_load(fontstr); \
57 }
58
59 typedef struct _RemoteURI       RemoteURI;
60
61 struct _RemoteURI
62 {
63         gchar *uri;
64
65         guint start;
66         guint end;
67 };
68
69 static GdkColor quote_colors[3] = {
70         {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
71         {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
72         {(gulong)0, (gushort)0, (gushort)0, (gushort)0}
73 };
74
75 static GdkColor uri_color = {
76         (gulong)0,
77         (gushort)0,
78         (gushort)0,
79         (gushort)0
80 };
81
82 static GdkColor emphasis_color = {
83         (gulong)0,
84         (gushort)0,
85         (gushort)0,
86         (gushort)0xcfff
87 };
88
89 static GdkColor error_color = {
90         (gulong)0,
91         (gushort)0xefff,
92         (gushort)0,
93         (gushort)0
94 };
95
96 static GdkFont *spacingfont;
97
98 static void textview_show_html          (TextView       *textview,
99                                          FILE           *fp,
100                                          CodeConverter  *conv);
101 static void textview_write_line         (TextView       *textview,
102                                          const gchar    *str,
103                                          CodeConverter  *conv);
104 static GPtrArray *textview_scan_header  (TextView       *textview,
105                                          FILE           *fp);
106 static void textview_show_header        (TextView       *textview,
107                                          GPtrArray      *headers);
108
109 static void textview_key_pressed        (GtkWidget      *widget,
110                                          GdkEventKey    *event,
111                                          TextView       *textview);
112 static void textview_button_pressed     (GtkWidget      *widget,
113                                          GdkEventButton *event,
114                                          TextView       *textview);
115
116 static void textview_uri_list_remove_all(GSList         *uri_list);
117
118 static void textview_smooth_scroll_do           (TextView       *textview,
119                                                  gfloat          old_value,
120                                                  gfloat          last_value,
121                                                  gint            step);
122 static void textview_smooth_scroll_one_line     (TextView       *textview,
123                                                  gboolean        up);
124 static gboolean textview_smooth_scroll_page     (TextView       *textview,
125                                                  gboolean        up);
126
127
128 TextView *textview_create(void)
129 {
130         TextView *textview;
131         GtkWidget *vbox;
132         GtkWidget *scrolledwin_sb;
133         GtkWidget *scrolledwin_mb;
134         GtkWidget *text_sb;
135         GtkWidget *text_mb;
136
137         debug_print(_("Creating text view...\n"));
138         textview = g_new0(TextView, 1);
139
140         scrolledwin_sb = gtk_scrolled_window_new(NULL, NULL);
141         scrolledwin_mb = gtk_scrolled_window_new(NULL, NULL);
142         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin_sb),
143                                        GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
144         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin_mb),
145                                        GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
146         gtk_widget_set_usize(scrolledwin_sb, prefs_common.mainview_width, -1);
147         gtk_widget_set_usize(scrolledwin_mb, prefs_common.mainview_width, -1);
148
149         /* create GtkText widgets for single-byte and multi-byte character */
150         text_sb = gtk_text_new(NULL, NULL);
151         text_mb = gtk_text_new(NULL, NULL);
152         gtk_widget_show(text_sb);
153         gtk_widget_show(text_mb);
154         gtk_text_set_word_wrap(GTK_TEXT(text_sb), TRUE);
155         gtk_text_set_word_wrap(GTK_TEXT(text_mb), TRUE);
156         gtk_widget_ensure_style(text_sb);
157         gtk_widget_ensure_style(text_mb);
158         if (text_sb->style && text_sb->style->font->type == GDK_FONT_FONTSET) {
159                 GtkStyle *style;
160
161                 style = gtk_style_copy(text_sb->style);
162                 gdk_font_unref(style->font);
163                 FONT_LOAD(style->font, NORMAL_FONT);
164                 gtk_widget_set_style(text_sb, style);
165         }
166         if (text_mb->style && text_mb->style->font->type == GDK_FONT_FONT) {
167                 GtkStyle *style;
168
169                 style = gtk_style_copy(text_mb->style);
170                 gdk_font_unref(style->font);
171                 style->font = gdk_fontset_load(NORMAL_FONT);
172                 gtk_widget_set_style(text_mb, style);
173         }
174         gtk_widget_ref(scrolledwin_sb);
175         gtk_widget_ref(scrolledwin_mb);
176
177         gtk_container_add(GTK_CONTAINER(scrolledwin_sb), text_sb);
178         gtk_container_add(GTK_CONTAINER(scrolledwin_mb), text_mb);
179         gtk_signal_connect(GTK_OBJECT(text_sb), "key_press_event",
180                            GTK_SIGNAL_FUNC(textview_key_pressed),
181                            textview);
182         gtk_signal_connect(GTK_OBJECT(text_sb), "button_press_event",
183                            GTK_SIGNAL_FUNC(textview_button_pressed),
184                            textview);
185         gtk_signal_connect(GTK_OBJECT(text_mb), "key_press_event",
186                            GTK_SIGNAL_FUNC(textview_key_pressed),
187                            textview);
188         gtk_signal_connect(GTK_OBJECT(text_mb), "button_press_event",
189                            GTK_SIGNAL_FUNC(textview_button_pressed),
190                            textview);
191
192         gtk_widget_show(scrolledwin_sb);
193         gtk_widget_show(scrolledwin_mb);
194
195         vbox = gtk_vbox_new(FALSE, 0);
196         gtk_box_pack_start(GTK_BOX(vbox), scrolledwin_sb, TRUE, TRUE, 0);
197
198         textview->vbox           = vbox;
199         textview->scrolledwin    = scrolledwin_sb;
200         textview->scrolledwin_sb = scrolledwin_sb;
201         textview->scrolledwin_mb = scrolledwin_mb;
202         textview->text           = text_sb;
203         textview->text_sb        = text_sb;
204         textview->text_mb        = text_mb;
205         textview->text_is_mb     = FALSE;
206         textview->uri_list       = NULL;
207
208         return textview;
209 }
210
211 void textview_init(TextView *textview)
212 {
213         gtkut_widget_disable_theme_engine(textview->text_sb);
214         gtkut_widget_disable_theme_engine(textview->text_mb);
215         textview_update_message_colors();
216         textview_set_font(textview, NULL);
217 }
218
219 void textview_update_message_colors(void)
220 {
221         GdkColor black = {0, 0, 0, 0};
222
223         if (prefs_common.enable_color) {
224                 /* grab the quote colors, converting from an int to a GdkColor */
225                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level1_col,
226                                                &quote_colors[0]);
227                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level2_col,
228                                                &quote_colors[1]);
229                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level3_col,
230                                                &quote_colors[2]);
231                 gtkut_convert_int_to_gdk_color(prefs_common.uri_col,
232                                                &uri_color);
233         } else {
234                 quote_colors[0] = quote_colors[1] = quote_colors[2] = 
235                         uri_color = emphasis_color = black;
236         }
237 }
238
239 void textview_show_message(TextView *textview, MimeInfo *mimeinfo,
240                            const gchar *file)
241 {
242         FILE *fp;
243
244         if ((fp = fopen(file, "r")) == NULL) {
245                 FILE_OP_ERROR(file, "fopen");
246                 return;
247         }
248
249         textview_show_part(textview, mimeinfo, fp);
250
251         fclose(fp);
252 }
253
254 void textview_show_part(TextView *textview, MimeInfo *mimeinfo, FILE *fp)
255 {
256         GtkText *text;
257         gchar buf[BUFFSIZE];
258         const gchar *boundary = NULL;
259         gint boundary_len = 0;
260         const gchar *charset = NULL;
261         FILE *tmpfp;
262         GPtrArray *headers = NULL;
263         CodeConverter *conv;
264
265         g_return_if_fail(mimeinfo != NULL);
266         g_return_if_fail(fp != NULL);
267
268         if (mimeinfo->mime_type == MIME_MULTIPART) {
269                 if (mimeinfo->sub) {
270                         mimeinfo = mimeinfo->sub;
271                         if (fseek(fp, mimeinfo->fpos, SEEK_SET) < 0) {
272                                 perror("fseek");
273                                 return;
274                         }
275                 } else
276                         return;
277         }
278         if (mimeinfo->parent && mimeinfo->parent->boundary) {
279                 boundary = mimeinfo->parent->boundary;
280                 boundary_len = strlen(boundary);
281         }
282
283         if (!boundary && mimeinfo->mime_type == MIME_TEXT) {
284                 if (fseek(fp, mimeinfo->fpos, SEEK_SET) < 0)
285                         perror("fseek");
286                 headers = textview_scan_header(textview, fp);
287         } else {
288                 if (mimeinfo->mime_type == MIME_TEXT && mimeinfo->parent) {
289                         glong fpos;
290
291                         if ((fpos = ftell(fp)) < 0)
292                                 perror("ftell");
293                         else if (fseek(fp, mimeinfo->parent->fpos, SEEK_SET)
294                                  < 0)
295                                 perror("fseek");
296                         else {
297                                 headers = textview_scan_header(textview, fp);
298                                 if (fseek(fp, fpos, SEEK_SET) < 0)
299                                         perror("fseek");
300                         }
301                 }
302                 while (fgets(buf, sizeof(buf), fp) != NULL)
303                         if (buf[0] == '\r' || buf[0] == '\n') break;
304         }
305
306         /* display attached RFC822 single text message */
307         if (mimeinfo->parent && mimeinfo->mime_type == MIME_MESSAGE_RFC822) {
308                 if (!mimeinfo->sub || mimeinfo->sub->children) return;
309                 headers = textview_scan_header(textview, fp);
310                 mimeinfo = mimeinfo->sub;
311         } else if (!mimeinfo->parent &&
312                    mimeinfo->mime_type == MIME_MESSAGE_RFC822) {
313                 if (!mimeinfo->sub) return;
314                 headers = textview_scan_header(textview, fp);
315                 mimeinfo = mimeinfo->sub;
316         }
317
318         if (prefs_common.force_charset)
319                 charset = prefs_common.force_charset;
320         else if (mimeinfo->charset)
321                 charset = mimeinfo->charset;
322         textview_set_font(textview, charset);
323
324         conv = conv_code_converter_new(charset);
325
326         textview_clear(textview);
327         text = GTK_TEXT(textview->text);
328         gtk_text_freeze(text);
329
330         if (headers) {
331                 gint i;
332
333                 textview_show_header(textview, headers);
334                 for (i = 0; i < headers->len; i++) {
335                         Header *header = g_ptr_array_index(headers, i);
336                         g_free(header->body);
337                         g_free(header->name);
338                         g_free(header);
339                 }
340                 g_ptr_array_free(headers, TRUE);
341         }
342
343         tmpfp = procmime_decode_content(NULL, fp, mimeinfo);
344         if (tmpfp) {
345                 if (mimeinfo->mime_type == MIME_TEXT_HTML)
346                         textview_show_html(textview, tmpfp, conv);
347                 else
348                         while (fgets(buf, sizeof(buf), tmpfp) != NULL)
349                                 textview_write_line(textview, buf, conv);
350                 fclose(tmpfp);
351         }
352
353         conv_code_converter_destroy(conv);
354
355         gtk_text_thaw(text);
356 }
357
358 void textview_show_mime_part(TextView *textview, MimeInfo *partinfo)
359 {
360         GtkText *text;
361
362         if (!partinfo) return;
363
364         textview_set_font(textview, NULL);
365         text = GTK_TEXT(textview->text);
366         textview_clear(textview);
367
368         gtk_text_freeze(text);
369
370         gtk_text_insert
371                 (text, textview->msgfont, NULL, NULL,
372                  _("To save this part, pop up the context menu with\n"), -1);
373         gtk_text_insert
374                 (text, textview->msgfont, NULL, NULL,
375                  _("right click and select `Save as...', or press `y' key.\n\n"), -1);
376
377         gtk_text_insert
378                 (text, textview->msgfont, NULL, NULL,
379                  _("To display this part as a text message, select\n"), -1);
380         gtk_text_insert
381                 (text, textview->msgfont, NULL, NULL,
382                  _("`Display as text', or press `t' key.\n\n"), -1);
383
384         gtk_text_insert
385                 (text, textview->msgfont, NULL, NULL,
386                  _("To open this part with external program, select `Open',\n"), -1);
387         gtk_text_insert
388                 (text, textview->msgfont, NULL, NULL,
389                  _("or double-click, or click the center button, or press `l' key."), -1);
390
391         gtk_text_thaw(text);
392 }
393
394 #if USE_GPGME
395 void textview_show_signature_part(TextView *textview, MimeInfo *partinfo)
396 {
397         GtkText *text;
398
399         if (!partinfo) return;
400
401         textview_set_font(textview, NULL);
402         text = GTK_TEXT(textview->text);
403         textview_clear(textview);
404
405         gtk_text_freeze(text);
406
407         if (partinfo->sigstatus_full == NULL) {
408                 gtk_text_insert
409                         (text, textview->msgfont, NULL, NULL,
410                          _("This signature has not been checked yet.\n"), -1);
411                 gtk_text_insert
412                         (text, textview->msgfont, NULL, NULL,
413                          _("To check it, pop up the context menu with\n"), -1);
414                 gtk_text_insert
415                         (text, textview->msgfont, NULL, NULL,
416                          _("right click and select `Check signature'.\n"), -1);
417         } else {
418                 gtk_text_insert(text, textview->msgfont, NULL, NULL,
419                                 partinfo->sigstatus_full, -1);
420         }
421
422         gtk_text_thaw(text);
423 }
424 #endif /* USE_GPGME */
425
426 static void textview_show_html(TextView *textview, FILE *fp,
427                                CodeConverter *conv)
428 {
429         HTMLParser *parser;
430         gchar *str;
431
432         parser = html_parser_new(fp, conv);
433         g_return_if_fail(parser != NULL);
434
435         while ((str = html_parse(parser)) != NULL) {
436                 textview_write_line(textview, str, NULL);
437         }
438         html_parser_destroy(parser);
439 }
440
441 /* get_uri_part() - retrieves a URI starting from scanpos.
442                     Returns TRUE if succesful */
443 static gboolean get_uri_part(const gchar *start, const gchar *scanpos,
444                              const gchar **bp, const gchar **ep)
445 {
446         const gchar *ep_;
447
448         g_return_val_if_fail(start != NULL, FALSE);
449         g_return_val_if_fail(scanpos != NULL, FALSE);
450         g_return_val_if_fail(bp != NULL, FALSE);
451         g_return_val_if_fail(ep != NULL, FALSE);
452
453         *bp = scanpos;
454
455         /* find end point of URI */
456         for (ep_ = scanpos; *ep_ != '\0'; ep_++) {
457                 if (!isgraph(*ep_) || !isascii(*ep_) || strchr("()<>\"", *ep_))
458                         break;
459         }
460
461         /* no punctuation at end of string */
462
463         /* FIXME: this stripping of trailing punctuations may bite with other URIs.
464          * should pass some URI type to this function and decide on that whether
465          * to perform punctuation stripping */
466
467 #define IS_REAL_PUNCT(ch) \
468         (ispunct(ch) && ((ch) != '/')) 
469
470         for (; ep_ - 1 > scanpos + 1 && IS_REAL_PUNCT(*(ep_ - 1)); ep_--)
471                 ;
472
473 #undef IS_REAL_PUNCT
474
475         *ep = ep_;
476
477         return TRUE;            
478 }
479
480 static gchar *make_uri_string(const gchar *bp, const gchar *ep)
481 {
482         return g_strndup(bp, ep - bp);
483 }
484
485 /* valid mail address characters */
486 #define IS_RFC822_CHAR(ch) \
487         (isascii(ch) && \
488          (ch) > 32   && \
489          (ch) != 127 && \
490          !isspace(ch) && \
491          !strchr("()<>\"", (ch)))
492
493 /* get_email_part() - retrieves an email address. Returns TRUE if succesful */
494 static gboolean get_email_part(const gchar *start, const gchar *scanpos,
495                                const gchar **bp, const gchar **ep)
496 {
497         /* more complex than the uri part because we need to scan back and forward starting from
498          * the scan position. */
499         gboolean result = FALSE;
500         const gchar *bp_;
501         const gchar *ep_;
502
503         g_return_val_if_fail(start != NULL, FALSE);
504         g_return_val_if_fail(scanpos != NULL, FALSE);
505         g_return_val_if_fail(bp != NULL, FALSE);
506         g_return_val_if_fail(ep != NULL, FALSE);
507
508         /* scan start of address */
509         for (bp_ = scanpos - 1; bp_ >= start && IS_RFC822_CHAR(*bp_); bp_--)
510                 ;
511
512         /* TODO: should start with an alnum? */
513         bp_++;
514         for (; bp_ < scanpos && !isalnum(*bp_); bp_++)
515                 ;
516
517         if (bp_ != scanpos) {
518                 /* scan end of address */
519                 for (ep_ = scanpos + 1; *ep_ && IS_RFC822_CHAR(*ep_); ep_++)
520                         ;
521
522                 /* TODO: really should terminate with an alnum? */
523                 for (; ep_ > scanpos  && !isalnum(*ep_); --ep_)
524                         ;
525                 ep_++;
526
527                 if (ep_ > scanpos + 1) {
528                         *ep = ep_;
529                         *bp = bp_;
530                         result = TRUE;
531                 }
532         }
533
534         return result;
535 }
536
537 #undef IS_RFC822_CHAR
538
539 static gchar *make_email_string(const gchar *bp, const gchar *ep)
540 {
541         /* returns a mailto: URI; mailto: is also used to detect the
542          * uri type later on in the button_pressed signal handler */
543         gchar *tmp;
544         gchar *result;
545
546         tmp = g_strndup(bp, ep - bp);
547         result = g_strconcat("mailto:", tmp, NULL);
548         g_free(tmp);
549
550         return result;
551 }
552
553 #define ADD_TXT_POS(bp_, ep_, pti_) \
554         if ((last->next = alloca(sizeof(struct txtpos))) != NULL) { \
555                 last = last->next; \
556                 last->bp = (bp_); last->ep = (ep_); last->pti = (pti_); \
557                 last->next = NULL; \
558         } else { \
559                 g_warning("alloc error scanning URIs\n"); \
560                 gtk_text_insert(text, textview->msgfont, fg_color, NULL, \
561                                 linebuf, -1); \
562                 return; \
563         }
564
565 /* textview_make_clickable_parts() - colorizes clickable parts */
566 static void textview_make_clickable_parts(TextView *textview, GtkText *text,
567                                           GdkColor *fg_color,
568                                           GdkColor *uri_color,
569                                           const gchar *linebuf)
570 {
571         /* parse table - in order of priority */
572         struct table {
573                 const gchar *needle; /* token */
574
575                 /* token search function */
576                 gchar    *(*search)     (const gchar *haystack,
577                                          const gchar *needle);
578                 /* part parsing function */
579                 gboolean  (*parse)      (const gchar *start,
580                                          const gchar *scanpos,
581                                          const gchar **bp_,
582                                          const gchar **ep_);
583                 /* part to URI function */
584                 gchar    *(*build_uri)  (const gchar *bp,
585                                          const gchar *ep);
586         };
587
588         static struct table parser[] = {
589                 {"http://",  strcasestr, get_uri_part,   make_uri_string},
590                 {"https://", strcasestr, get_uri_part,   make_uri_string},
591                 {"ftp://",   strcasestr, get_uri_part,   make_uri_string},
592                 {"mailto:",  strcasestr, get_uri_part,   make_uri_string},
593                 {"@",        strcasestr, get_email_part, make_email_string}
594         };
595         const gint PARSE_ELEMS = sizeof parser / sizeof parser[0];
596
597         gint  n;
598         const gchar *walk, *bp, *ep;
599
600         struct txtpos {
601                 const gchar     *bp, *ep;       /* text position */
602                 gint             pti;           /* index in parse table */
603                 struct txtpos   *next;          /* next */
604         } head = {NULL, NULL, 0,  NULL}, *last = &head;
605
606         /* parse for clickable parts, and build a list of begin and end positions  */
607         for (walk = linebuf, n = 0;;) {
608                 gint last_index = PARSE_ELEMS;
609                 gchar *scanpos = NULL;
610
611                 /* FIXME: this looks phony. scanning for anything in the parse table */
612                 for (n = 0; n < PARSE_ELEMS; n++) {
613                         gchar *tmp;
614
615                         tmp = parser[n].search(walk, parser[n].needle);
616                         if (tmp) {
617                                 if (scanpos == NULL || tmp < scanpos) {
618                                         scanpos = tmp;
619                                         last_index = n;
620                                 }
621                         }                                       
622                 }
623
624                 if (scanpos) {
625                         /* check if URI can be parsed */
626                         if (parser[last_index].parse(linebuf, scanpos, &bp, &ep)
627                             && (ep - bp - 1) > strlen(parser[n].needle)) {
628                                         ADD_TXT_POS(bp, ep, last_index);
629                                         walk = ep;
630                         } else
631                                 walk = scanpos +
632                                         strlen(parser[last_index].needle);
633                 } else
634                         break;
635         }
636
637         /* colorize this line */
638         if (head.next) {
639                 const gchar *normal_text = linebuf;
640
641                 /* insert URIs */
642                 for (last = head.next; last != NULL;
643                      normal_text = last->ep, last = last->next) {
644                         RemoteURI *uri;
645
646                         uri = g_new(RemoteURI, 1);
647                         if (last->bp - normal_text > 0)
648                                 gtk_text_insert(text, textview->msgfont,
649                                                 fg_color, NULL,
650                                                 normal_text,
651                                                 last->bp - normal_text);
652                         uri->uri = parser[last->pti].build_uri(last->bp, last->ep);
653                         uri->start = gtk_text_get_point(text);
654                         gtk_text_insert(text, textview->msgfont, uri_color,
655                                         NULL, last->bp, last->ep - last->bp);
656                         uri->end = gtk_text_get_point(text);
657                         textview->uri_list =
658                                 g_slist_append(textview->uri_list, uri);
659                 }
660
661                 if (*normal_text)
662                         gtk_text_insert(text, textview->msgfont, fg_color,
663                                         NULL, normal_text, -1);
664         } else
665                 gtk_text_insert(text, textview->msgfont, fg_color, NULL,
666                                 linebuf, -1);
667 }
668
669 #undef ADD_TXT_POS
670
671 static void textview_write_line(TextView *textview, const gchar *str,
672                                 CodeConverter *conv)
673 {
674         GtkText *text = GTK_TEXT(textview->text);
675         gchar buf[BUFFSIZE];
676         size_t len;
677         GdkColor *fg_color;
678         gint quotelevel = -1;
679
680         if (!conv)
681                 strncpy2(buf, str, sizeof(buf));
682         else if (conv_convert(conv, buf, sizeof(buf), str) < 0) {
683                 gtk_text_insert(text, textview->msgfont,
684                                 prefs_common.enable_color
685                                 ? &error_color : NULL, NULL,
686                                 "*** Warning: code conversion failed ***\n",
687                                 -1);
688                 return;
689         }
690
691         len = strlen(buf);
692         if (len > 1 && buf[len - 1] == '\n' && buf[len - 2] == '\r') {
693                 buf[len - 2] = '\n';
694                 buf[len - 1] = '\0';
695         }
696         if (prefs_common.conv_mb_alnum) conv_mb_alnum(buf);
697         fg_color = NULL;
698
699         /* change color of quotation
700            >, foo>, _> ... ok, <foo>, foo bar>, foo-> ... ng
701            Up to 3 levels of quotations are detected, and each
702            level is colored using a different color. */
703         if (prefs_common.enable_color && strchr(buf, '>')) {
704                 quotelevel = get_quote_level(buf);
705
706                 /* set up the correct foreground color */
707                 if (quotelevel > 2) {
708                         /* recycle colors */
709                         if (prefs_common.recycle_quote_colors)
710                                 quotelevel %= 3;
711                         else
712                                 quotelevel = 2;
713                 }
714         }
715
716         if (quotelevel == -1)
717                 fg_color = NULL;
718         else
719                 fg_color = &quote_colors[quotelevel];
720
721         if (prefs_common.head_space && spacingfont && buf[0] != '\n')
722                 gtk_text_insert(text, spacingfont, NULL, NULL, " ", 1);
723
724         if (prefs_common.enable_color)
725                 textview_make_clickable_parts(textview, text,
726                                               fg_color, &uri_color, buf);
727         else
728                 gtk_text_insert(text, textview->msgfont, fg_color, NULL,
729                                 buf, -1);
730 }
731
732 void textview_clear(TextView *textview)
733 {
734         GtkText *text = GTK_TEXT(textview->text);
735
736         gtk_text_freeze(text);
737         gtk_text_backward_delete(text, gtk_text_get_length(text));
738         gtk_text_thaw(text);
739
740         textview_uri_list_remove_all(textview->uri_list);
741         textview->uri_list = NULL;
742 }
743
744 void textview_destroy(TextView *textview)
745 {
746         textview_uri_list_remove_all(textview->uri_list);
747         textview->uri_list = NULL;
748
749         if (!textview->scrolledwin_sb->parent)
750                 gtk_widget_destroy(textview->scrolledwin_sb);
751         if (!textview->scrolledwin_mb->parent)
752                 gtk_widget_destroy(textview->scrolledwin_mb);
753
754         if (textview->msgfont) {
755                 textview->msgfont->ascent = textview->prev_ascent;
756                 textview->msgfont->descent = textview->prev_descent;
757                 gdk_font_unref(textview->msgfont);
758         }
759         if (textview->boldfont)
760                 gdk_font_unref(textview->boldfont);
761
762         g_free(textview);
763 }
764
765 void textview_set_font(TextView *textview, const gchar *codeset)
766 {
767         gboolean use_fontset = TRUE;
768
769         /* In multi-byte mode, GtkText can't display 8bit characters
770            correctly, so it must be single-byte mode. */
771         if (MB_CUR_MAX > 1) {
772                 if (codeset) {
773                         if (!strncasecmp(codeset, "ISO-8859-", 9) ||
774                             !strncasecmp(codeset, "KOI8-", 5)     ||
775                             !strncasecmp(codeset, "CP", 2)        ||
776                             !strncasecmp(codeset, "WINDOWS-", 8)  ||
777                             !strcasecmp(codeset, "BALTIC"))
778                                 use_fontset = FALSE;
779                 }
780         } else
781                 use_fontset = FALSE;
782
783         if (textview->text_is_mb && !use_fontset) {
784                 GtkWidget *parent;
785
786                 parent = textview->scrolledwin_mb->parent;
787                 gtk_editable_select_region
788                         (GTK_EDITABLE(textview->text_mb), 0, 0);
789                 gtk_container_remove(GTK_CONTAINER(parent),
790                                      textview->scrolledwin_mb);
791                 gtk_container_add(GTK_CONTAINER(parent),
792                                   textview->scrolledwin_sb);
793
794                 textview->text = textview->text_sb;
795                 textview->text_is_mb = FALSE;
796         } else if (!textview->text_is_mb && use_fontset) {
797                 GtkWidget *parent;
798
799                 parent = textview->scrolledwin_sb->parent;
800                 gtk_editable_select_region
801                         (GTK_EDITABLE(textview->text_sb), 0, 0);
802                 gtk_container_remove(GTK_CONTAINER(parent),
803                                      textview->scrolledwin_sb);
804                 gtk_container_add(GTK_CONTAINER(parent),
805                                   textview->scrolledwin_mb);
806
807                 textview->text = textview->text_mb;
808                 textview->text_is_mb = TRUE;
809         }
810
811         if (prefs_common.textfont) {
812                 if (textview->msgfont) {
813                         textview->msgfont->ascent = textview->prev_ascent;
814                         textview->msgfont->descent = textview->prev_descent;
815                         gdk_font_unref(textview->msgfont);
816                         textview->msgfont = NULL;
817                 }
818                 if (use_fontset)
819                         textview->msgfont =
820                                 gdk_fontset_load(prefs_common.textfont);
821                 else {
822                         if (MB_CUR_MAX > 1) {
823                                 FONT_LOAD(textview->msgfont,
824                                           "-*-courier-medium-r-normal--14-*-*-*-*-*-iso8859-1");
825                         } else {
826                                 FONT_LOAD(textview->msgfont,
827                                           prefs_common.textfont);
828                         }
829                 }
830
831                 if (textview->msgfont) {
832                         gint ascent, descent;
833
834                         textview->prev_ascent = textview->msgfont->ascent;
835                         textview->prev_descent = textview->msgfont->descent;
836                         descent = prefs_common.line_space / 2;
837                         ascent  = prefs_common.line_space - descent;
838                         textview->msgfont->ascent  += ascent;
839                         textview->msgfont->descent += descent;
840                 }
841         }
842
843         if (!textview->boldfont)
844                 FONT_LOAD(textview->boldfont, BOLD_FONT);
845         if (!spacingfont)
846                 spacingfont = gdk_font_load("-*-*-medium-r-normal--6-*");
847 }
848
849 enum
850 {
851         H_DATE          = 0,
852         H_FROM          = 1,
853         H_TO            = 2,
854         H_NEWSGROUPS    = 3,
855         H_SUBJECT       = 4,
856         H_CC            = 5,
857         H_REPLY_TO      = 6,
858         H_FOLLOWUP_TO   = 7,
859         H_X_MAILER      = 8,
860         H_X_NEWSREADER  = 9,
861         H_USER_AGENT    = 10,
862         H_ORGANIZATION  = 11,
863 };
864
865 static GPtrArray *textview_scan_header(TextView *textview, FILE *fp)
866 {
867         static HeaderEntry hentry[] = {{"Date:",         NULL, FALSE},
868                                        {"From:",         NULL, TRUE},
869                                        {"To:",           NULL, FALSE},
870                                        {"Newsgroups:",   NULL, FALSE},
871                                        {"Subject:",      NULL, TRUE},
872                                        {"Cc:",           NULL, FALSE},
873                                        {"Reply-To:",     NULL, FALSE},
874                                        {"Followup-To:",  NULL, FALSE},
875                                        {"X-Mailer:",     NULL, TRUE},
876                                        {"X-Newsreader:", NULL, TRUE},
877                                        {"User-Agent:",   NULL, TRUE},
878                                        {"Organization:", NULL, TRUE},
879                                        {NULL,            NULL, FALSE}};
880         gchar buf[BUFFSIZE], tmp[BUFFSIZE];
881         gint hnum;
882         HeaderEntry *hp;
883         GPtrArray *headers;
884
885         g_return_val_if_fail(fp != NULL, NULL);
886
887         if (!prefs_common.display_header) {
888                 while (fgets(buf, sizeof(buf), fp) != NULL)
889                         if (buf[0] == '\r' || buf[0] == '\n') break;
890                 return NULL;
891         }
892
893         headers = g_ptr_array_new();
894
895         while ((hnum = procheader_get_one_field(buf, sizeof(buf), fp, hentry))
896                != -1) {
897                 Header *header;
898
899                 hp = hentry + hnum;
900
901                 header = g_new(Header, 1);
902                 header->name = g_strndup(buf, strlen(hp->name));
903                 conv_unmime_header(tmp, sizeof(tmp), buf + strlen(hp->name),
904                                    NULL);
905                 header->body = g_strdup(tmp);
906
907                 g_ptr_array_add(headers, header);
908         }
909
910         return headers;
911 }
912
913 static void textview_show_header(TextView *textview, GPtrArray *headers)
914 {
915         GtkText *text = GTK_TEXT(textview->text);
916         Header *header;
917         gint i;
918
919         g_return_if_fail(headers != NULL);
920
921         gtk_text_freeze(text);
922
923         for (i = 0; i < headers->len; i++) {
924                 header = g_ptr_array_index(headers, i);
925                 g_return_if_fail(header->name != NULL);
926
927                 gtk_text_insert(text, textview->boldfont, NULL, NULL,
928                                 header->name, -1);
929                 if (prefs_common.enable_color &&
930                     (strncmp(header->name, "X-Mailer", 8) == 0 ||
931                      strncmp(header->name, "X-Newsreader", 12) == 0) &&
932                     strstr(header->body, "Sylpheed") != NULL)
933                         gtk_text_insert(text, NULL, &emphasis_color, NULL,
934                                         header->body, -1);
935                 else
936                         gtk_text_insert(text, NULL, NULL, NULL,
937                                         header->body, -1);
938                 gtk_text_insert(text, textview->msgfont, NULL, NULL, "\n", 1);
939         }
940
941         gtk_text_insert(text, textview->msgfont, NULL, NULL, "\n", 1);
942         gtk_text_thaw(text);
943 }
944
945 void textview_scroll_one_line(TextView *textview, gboolean up)
946 {
947         GtkText *text = GTK_TEXT(textview->text);
948         gfloat upper;
949
950         if (prefs_common.enable_smooth_scroll) {
951                 textview_smooth_scroll_one_line(textview, up);
952                 return;
953         }
954
955         if (!up) {
956                 upper = text->vadj->upper - text->vadj->page_size;
957                 if (text->vadj->value < upper) {
958                         text->vadj->value +=
959                                 text->vadj->step_increment * 4;
960                         text->vadj->value =
961                                 MIN(text->vadj->value, upper);
962                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
963                                                 "value_changed");
964                 }
965         } else {
966                 if (text->vadj->value > 0.0) {
967                         text->vadj->value -=
968                                 text->vadj->step_increment * 4;
969                         text->vadj->value =
970                                 MAX(text->vadj->value, 0.0);
971                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
972                                                 "value_changed");
973                 }
974         }
975 }
976
977 gboolean textview_scroll_page(TextView *textview, gboolean up)
978 {
979         GtkText *text = GTK_TEXT(textview->text);
980         gfloat upper;
981         gfloat page_incr;
982
983         if (prefs_common.enable_smooth_scroll)
984                 return textview_smooth_scroll_page(textview, up);
985
986         if (prefs_common.scroll_halfpage)
987                 page_incr = text->vadj->page_increment / 2;
988         else
989                 page_incr = text->vadj->page_increment;
990
991         if (!up) {
992                 upper = text->vadj->upper - text->vadj->page_size;
993                 if (text->vadj->value < upper) {
994                         text->vadj->value += page_incr;
995                         text->vadj->value = MIN(text->vadj->value, upper);
996                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
997                                                 "value_changed");
998                 } else
999                         return FALSE;
1000         } else {
1001                 if (text->vadj->value > 0.0) {
1002                         text->vadj->value -= page_incr;
1003                         text->vadj->value = MAX(text->vadj->value, 0.0);
1004                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1005                                                 "value_changed");
1006                 } else
1007                         return FALSE;
1008         }
1009
1010         return TRUE;
1011 }
1012
1013 static void textview_smooth_scroll_do(TextView *textview,
1014                                       gfloat old_value, gfloat last_value,
1015                                       gint step)
1016 {
1017         GtkText *text = GTK_TEXT(textview->text);
1018         gint change_value;
1019         gboolean up;
1020         gint i;
1021
1022         if (old_value < last_value) {
1023                 change_value = last_value - old_value;
1024                 up = FALSE;
1025         } else {
1026                 change_value = old_value - last_value;
1027                 up = TRUE;
1028         }
1029
1030         gdk_key_repeat_disable();
1031
1032         for (i = step; i <= change_value; i += step) {
1033                 text->vadj->value = old_value + (up ? -i : i);
1034                 gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1035                                         "value_changed");
1036         }
1037
1038         text->vadj->value = last_value;
1039         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj), "value_changed");
1040
1041         gdk_key_repeat_restore();
1042 }
1043
1044 static void textview_smooth_scroll_one_line(TextView *textview, gboolean up)
1045 {
1046         GtkText *text = GTK_TEXT(textview->text);
1047         gfloat upper;
1048         gfloat old_value;
1049         gfloat last_value;
1050
1051         if (!up) {
1052                 upper = text->vadj->upper - text->vadj->page_size;
1053                 if (text->vadj->value < upper) {
1054                         old_value = text->vadj->value;
1055                         last_value = text->vadj->value +
1056                                 text->vadj->step_increment * 4;
1057                         last_value = MIN(last_value, upper);
1058
1059                         textview_smooth_scroll_do(textview, old_value,
1060                                                   last_value,
1061                                                   prefs_common.scroll_step);
1062                 }
1063         } else {
1064                 if (text->vadj->value > 0.0) {
1065                         old_value = text->vadj->value;
1066                         last_value = text->vadj->value -
1067                                 text->vadj->step_increment * 4;
1068                         last_value = MAX(last_value, 0.0);
1069
1070                         textview_smooth_scroll_do(textview, old_value,
1071                                                   last_value,
1072                                                   prefs_common.scroll_step);
1073                 }
1074         }
1075 }
1076
1077 static gboolean textview_smooth_scroll_page(TextView *textview, gboolean up)
1078 {
1079         GtkText *text = GTK_TEXT(textview->text);
1080         gfloat upper;
1081         gfloat page_incr;
1082         gfloat old_value;
1083         gfloat last_value;
1084
1085         if (prefs_common.scroll_halfpage)
1086                 page_incr = text->vadj->page_increment / 2;
1087         else
1088                 page_incr = text->vadj->page_increment;
1089
1090         if (!up) {
1091                 upper = text->vadj->upper - text->vadj->page_size;
1092                 if (text->vadj->value < upper) {
1093                         old_value = text->vadj->value;
1094                         last_value = text->vadj->value + page_incr;
1095                         last_value = MIN(last_value, upper);
1096
1097                         textview_smooth_scroll_do(textview, old_value,
1098                                                   last_value,
1099                                                   prefs_common.scroll_step);
1100                 } else
1101                         return FALSE;
1102         } else {
1103                 if (text->vadj->value > 0.0) {
1104                         old_value = text->vadj->value;
1105                         last_value = text->vadj->value - page_incr;
1106                         last_value = MAX(last_value, 0.0);
1107
1108                         textview_smooth_scroll_do(textview, old_value,
1109                                                   last_value,
1110                                                   prefs_common.scroll_step);
1111                 } else
1112                         return FALSE;
1113         }
1114
1115         return TRUE;
1116 }
1117
1118 static void textview_key_pressed(GtkWidget *widget, GdkEventKey *event,
1119                                  TextView *textview)
1120 {
1121         SummaryView *summaryview = NULL;
1122
1123         if (!event) return;
1124         if (textview->messageview->mainwin)
1125                 summaryview = textview->messageview->mainwin->summaryview;
1126
1127         switch (event->keyval) {
1128         case GDK_Tab:
1129         case GDK_Home:
1130         case GDK_Left:
1131         case GDK_Up:
1132         case GDK_Right:
1133         case GDK_Down:
1134         case GDK_Page_Up:
1135         case GDK_Page_Down:
1136         case GDK_End:
1137         case GDK_Control_L:
1138         case GDK_Control_R:
1139                 break;
1140         case GDK_space:
1141                 if (summaryview)
1142                         summary_pass_key_press_event(summaryview, event);
1143                 else
1144                         textview_scroll_page(textview, FALSE);
1145                 break;
1146         case GDK_BackSpace:
1147         case GDK_Delete:
1148                 textview_scroll_page(textview, TRUE);
1149                 break;
1150         case GDK_Return:
1151                 textview_scroll_one_line(textview,
1152                                          (event->state & GDK_MOD1_MASK) != 0);
1153                 break;
1154         default:
1155                 if (summaryview)
1156                         summary_pass_key_press_event(summaryview, event);
1157                 break;
1158         }
1159 }
1160
1161 static void textview_button_pressed(GtkWidget *widget, GdkEventButton *event,
1162                                     TextView *textview)
1163 {
1164         if (event &&
1165             ((event->button == 1 && event->type == GDK_2BUTTON_PRESS) ||
1166              event->button == 2)) {
1167                 GSList *cur;
1168                 guint current_pos;
1169
1170                 current_pos = GTK_EDITABLE(textview->text)->current_pos;
1171
1172                 for (cur = textview->uri_list; cur != NULL; cur = cur->next) {
1173                         RemoteURI *uri = (RemoteURI *)cur->data;
1174
1175                         if (current_pos >= uri->start &&
1176                             current_pos <  uri->end) {
1177                                 if (!strncasecmp(uri->uri, "mailto:", 7))
1178                                         compose_new_with_recipient
1179                                                 (NULL, uri->uri + 7);
1180                                 else
1181                                         open_uri(uri->uri,
1182                                                  prefs_common.uri_cmd);
1183                         }                               
1184                 }
1185         }
1186 }
1187
1188 static void textview_uri_list_remove_all(GSList *uri_list)
1189 {
1190         GSList *cur;
1191
1192         for (cur = uri_list; cur != NULL; cur = cur->next) {
1193                 if (cur->data) {
1194                         g_free(((RemoteURI *)cur->data)->uri);
1195                         g_free(cur->data);
1196                 }
1197         }
1198
1199         g_slist_free(uri_list);
1200 }