xref: /haiku/src/apps/mail/Content.cpp (revision 632e56d8e514ba6ac41f582ce580e51a3cd8922e)
1 /*
2 Open Tracker License
3 
4 Terms and Conditions
5 
6 Copyright (c) 1991-2001, Be Incorporated. All rights reserved.
7 
8 Permission is hereby granted, free of charge, to any person obtaining a copy of
9 this software and associated documentation files (the "Software"), to deal in
10 the Software without restriction, including without limitation the rights to
11 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
12 of the Software, and to permit persons to whom the Software is furnished to do
13 so, subject to the following conditions:
14 
15 The above copyright notice and this permission notice applies to all licensees
16 and shall be included in all copies or substantial portions of the Software.
17 
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF TITLE, MERCHANTABILITY,
20 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 BE INCORPORATED BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
22 AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF, OR IN
23 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24 
25 Except as contained in this notice, the name of Be Incorporated shall not be
26 used in advertising or otherwise to promote the sale, use or other dealings in
27 this Software without prior written authorization from Be Incorporated.
28 
29 BeMail(TM), Tracker(TM), Be(R), BeOS(R), and BeIA(TM) are trademarks or
30 registered trademarks of Be Incorporated in the United States and other
31 countries. Other brand product names are registered trademarks or trademarks
32 of their respective holders. All rights reserved.
33 */
34 
35 
36 #include <ctype.h>
37 #include <stdio.h>
38 #include <stdlib.h>
39 #include <strings.h>
40 
41 #include <Alert.h>
42 #include <Beep.h>
43 #include <Clipboard.h>
44 #include <ControlLook.h>
45 #include <Debug.h>
46 #include <E-mail.h>
47 #include <Input.h>
48 #include <Locale.h>
49 #include <MenuItem.h>
50 #include <Mime.h>
51 #include <NodeInfo.h>
52 #include <NodeMonitor.h>
53 #include <Path.h>
54 #include <PopUpMenu.h>
55 #include <Region.h>
56 #include <Roster.h>
57 #include <ScrollView.h>
58 #include <TextView.h>
59 #include <UTF8.h>
60 
61 #include <MailMessage.h>
62 #include <MailAttachment.h>
63 #include <mail_util.h>
64 
65 #include "MailApp.h"
66 #include "MailSupport.h"
67 #include "MailWindow.h"
68 #include "Messages.h"
69 #include "Content.h"
70 #include "Utilities.h"
71 #include "FieldMsg.h"
72 #include "Words.h"
73 
74 
75 #define DEBUG_SPELLCHECK 0
76 #if DEBUG_SPELLCHECK
77 #	define DSPELL(x) x
78 #else
79 #	define DSPELL(x) ;
80 #endif
81 
82 
83 #define B_TRANSLATION_CONTEXT "Mail"
84 
85 
86 const rgb_color kNormalTextColor = {0, 0, 0, 255};
87 const rgb_color kSpellTextColor = {255, 0, 0, 255};
88 const rgb_color kHyperLinkColor = {0, 0, 255, 255};
89 const rgb_color kHeaderColor = {72, 72, 72, 255};
90 
91 const rgb_color kQuoteColors[] = {
92 	{0, 0, 0x80, 0},		// 3rd, 6th, ... quote level color (blue)
93 	{0, 0x80, 0, 0},		// 1st, 4th, ... quote level color (green)
94 	{0x80, 0, 0, 0}			// 2nd, ... (red)
95 };
96 const int32 kNumQuoteColors = 3;
97 
98 const rgb_color kDiffColors[] = {
99 	{0xb0, 0, 0, 0},		// '-', red
100 	{0, 0x90, 0, 0},		// '+', green
101 	{0x6a, 0x6a, 0x6a, 0}	// '@@', dark grey
102 };
103 
104 void Unicode2UTF8(int32 c, char **out);
105 
106 
107 inline bool
108 IsInitialUTF8Byte(uchar b)
109 {
110 	return ((b & 0xC0) != 0x80);
111 }
112 
113 
114 void
115 Unicode2UTF8(int32 c, char **out)
116 {
117 	char *s = *out;
118 
119 	ASSERT(c < 0x200000);
120 
121 	if (c < 0x80)
122 		*(s++) = c;
123 	else if (c < 0x800) {
124 		*(s++) = 0xc0 | (c >> 6);
125 		*(s++) = 0x80 | (c & 0x3f);
126 	} else if (c < 0x10000) {
127 		*(s++) = 0xe0 | (c >> 12);
128 		*(s++) = 0x80 | ((c >> 6) & 0x3f);
129 		*(s++) = 0x80 | (c & 0x3f);
130 	} else if (c < 0x200000) {
131 		*(s++) = 0xf0 | (c >> 18);
132 		*(s++) = 0x80 | ((c >> 12) & 0x3f);
133 		*(s++) = 0x80 | ((c >> 6) & 0x3f);
134 		*(s++) = 0x80 | (c & 0x3f);
135 	}
136 	*out = s;
137 }
138 
139 
140 static bool
141 FilterHTMLTag(int32 &first, char **t, char *end)
142 {
143 	const char *newlineTags[] = {
144 		"br", "/p", "/div", "/table", "/tr",
145 		NULL};
146 
147 	char *a = *t;
148 
149 	// check for some common entities (in ISO-Latin-1)
150 	if (first == '&') {
151 		// filter out and convert decimal values
152 		if (a[1] == '#' && sscanf(a + 2, "%" B_SCNd32 ";", &first) == 1) {
153 			t[0] += strchr(a, ';') - a;
154 			return false;
155 		}
156 
157 		const struct { const char *name; int32 code; } entities[] = {
158 			// this list is sorted alphabetically to be binary searchable
159 			// the current implementation doesn't do this, though
160 
161 			// "name" is the entity name,
162 			// "code" is the corresponding unicode
163 			{"AElig;",	0x00c6},
164 			{"Aacute;",	0x00c1},
165 			{"Acirc;",	0x00c2},
166 			{"Agrave;",	0x00c0},
167 			{"Aring;",	0x00c5},
168 			{"Atilde;",	0x00c3},
169 			{"Auml;",	0x00c4},
170 			{"Ccedil;",	0x00c7},
171 			{"Eacute;",	0x00c9},
172 			{"Ecirc;",	0x00ca},
173 			{"Egrave;",	0x00c8},
174 			{"Euml;",	0x00cb},
175 			{"Iacute;", 0x00cd},
176 			{"Icirc;",	0x00ce},
177 			{"Igrave;", 0x00cc},
178 			{"Iuml;",	0x00cf},
179 			{"Ntilde;",	0x00d1},
180 			{"Oacute;", 0x00d3},
181 			{"Ocirc;",	0x00d4},
182 			{"Ograve;", 0x00d2},
183 			{"Ouml;",	0x00d6},
184 			{"Uacute;", 0x00da},
185 			{"Ucirc;",	0x00db},
186 			{"Ugrave;", 0x00d9},
187 			{"Uuml;",	0x00dc},
188 			{"aacute;", 0x00e1},
189 			{"acirc;",	0x00e2},
190 			{"aelig;",	0x00e6},
191 			{"agrave;", 0x00e0},
192 			{"amp;",	'&'},
193 			{"aring;",	0x00e5},
194 			{"atilde;", 0x00e3},
195 			{"auml;",	0x00e4},
196 			{"ccedil;",	0x00e7},
197 			{"copy;",	0x00a9},
198 			{"eacute;",	0x00e9},
199 			{"ecirc;",	0x00ea},
200 			{"egrave;",	0x00e8},
201 			{"euml;",	0x00eb},
202 			{"gt;",		'>'},
203 			{"iacute;", 0x00ed},
204 			{"icirc;",	0x00ee},
205 			{"igrave;", 0x00ec},
206 			{"iuml;",	0x00ef},
207 			{"lt;",		'<'},
208 			{"nbsp;",	' '},
209 			{"ntilde;",	0x00f1},
210 			{"oacute;", 0x00f3},
211 			{"ocirc;",	0x00f4},
212 			{"ograve;", 0x00f2},
213 			{"ouml;",	0x00f6},
214 			{"quot;",	'"'},
215 			{"szlig;",	0x00f6},
216 			{"uacute;", 0x00fa},
217 			{"ucirc;",	0x00fb},
218 			{"ugrave;", 0x00f9},
219 			{"uuml;",	0x00fc},
220 			{NULL, 0}
221 		};
222 
223 		for (int32 i = 0; entities[i].name; i++) {
224 			// entities are case-sensitive
225 			int32 length = strlen(entities[i].name);
226 			if (!strncmp(a + 1, entities[i].name, length)) {
227 				t[0] += length;	// note that the '&' is included here
228 				first = entities[i].code;
229 				return false;
230 			}
231 		}
232 	}
233 
234 	// no tag to filter
235 	if (first != '<')
236 		return false;
237 
238 	a++;
239 
240 	// is the tag one of the newline tags?
241 
242 	bool newline = false;
243 	for (int i = 0; newlineTags[i]; i++) {
244 		int length = strlen(newlineTags[i]);
245 		if (!strncasecmp(a, (char *)newlineTags[i], length) && !isalnum(a[length])) {
246 			newline = true;
247 			break;
248 		}
249 	}
250 
251 	// oh, it's not, so skip it!
252 
253 	if (!strncasecmp(a, "head", 4)) {	// skip "head" completely
254 		for (; a[0] && a < end; a++) {
255 			// Find the end of the HEAD section, or the start of the BODY,
256 			// which happens for some malformed spam.
257 			if (strncasecmp (a, "</head", 6) == 0 ||
258 				strncasecmp (a, "<body", 5) == 0)
259 				break;
260 		}
261 	}
262 
263 	// skip until tag end
264 	while (a[0] && a[0] != '>' && a < end)
265 		a++;
266 
267 	t[0] = a;
268 
269 	if (newline) {
270 		first = '\n';
271 		return false;
272 	}
273 
274 	return true;
275 }
276 
277 
278 /** Returns the type and length of the URL in the string if it is one.
279  *	If the "url" string is specified, it will fill it with the complete
280  *	URL that might differ from the one in the text itself (i.e. because
281  *	of an prepended "http://").
282  */
283 
284 static uint8
285 CheckForURL(const char *string, size_t &urlLength, BString *url = NULL)
286 {
287 	const char *urlPrefixes[] = {
288 		"http://",
289 		"ftp://",
290 		"shttp://",
291 		"https://",
292 		"finger://",
293 		"telnet://",
294 		"gopher://",
295 		"news://",
296 		"nntp://",
297 		"file://",
298 		NULL
299 	};
300 
301 	//
302 	//	Search for URL prefix
303 	//
304 	uint8 type = 0;
305 	for (const char **prefix = urlPrefixes; *prefix != 0; prefix++) {
306 		if (!cistrncmp(string, *prefix, strlen(*prefix))) {
307 			type = TYPE_URL;
308 			break;
309 		}
310 	}
311 
312 	//
313 	//	Not a URL? check for "mailto:" or "www."
314 	//
315 	if (type == 0 && !cistrncmp(string, "mailto:", 7))
316 		type = TYPE_MAILTO;
317 	if (type == 0 && !strncmp(string, "www.", 4)) {
318 		// this type will be special cased later (and a http:// is added
319 		// for the enclosure address)
320 		type = TYPE_URL;
321 	}
322 	if (type == 0) {
323 		// check for valid eMail addresses
324 		const char *at = strchr(string, '@');
325 		if (at) {
326 			const char *pos = string;
327 			bool readName = false;
328 			for (; pos < at; pos++) {
329 				// ToDo: are these all allowed characters?
330 				if (!isalnum(pos[0]) && pos[0] != '_' && pos[0] != '.' && pos[0] != '-')
331 					break;
332 
333 				readName = true;
334 			}
335 			if (pos == at && readName)
336 				type = TYPE_MAILTO;
337 		}
338 	}
339 
340 	if (type == 0)
341 		return 0;
342 
343 	int32 index = strcspn(string, " \t<>)\"\\,\r\n");
344 
345 	// filter out some punctuation marks if they are the last character
346 	char suffix = string[index - 1];
347 	if (suffix == '.'
348 		|| suffix == ','
349 		|| suffix == '?'
350 		|| suffix == '!'
351 		|| suffix == ':'
352 		|| suffix == ';')
353 		index--;
354 
355 	if (type == TYPE_URL) {
356 		char *parenthesis = NULL;
357 
358 		// filter out a trailing ')' if there is no left parenthesis before
359 		if (string[index - 1] == ')') {
360 			char *parenthesis = strchr(string, '(');
361 			if (parenthesis == NULL || parenthesis > string + index)
362 				index--;
363 		}
364 
365 		// filter out a trailing ']' if there is no left bracket before
366 		if (parenthesis == NULL && string[index - 1] == ']') {
367 			char *parenthesis = strchr(string, '[');
368 			if (parenthesis == NULL || parenthesis > string + index)
369 				index--;
370 		}
371 	}
372 
373 	if (url != NULL) {
374 		// copy the address to the specified string
375 		if (type == TYPE_URL && string[0] == 'w') {
376 			// URL starts with "www.", so add the protocol to it
377 			url->SetTo("http://");
378 			url->Append(string, index);
379 		} else if (type == TYPE_MAILTO && cistrncmp(string, "mailto:", 7)) {
380 			// eMail address had no "mailto:" prefix
381 			url->SetTo("mailto:");
382 			url->Append(string, index);
383 		} else
384 			url->SetTo(string, index);
385 	}
386 	urlLength = index;
387 
388 	return type;
389 }
390 
391 
392 static void
393 CopyQuotes(const char *text, size_t length, char *outText, size_t &outLength)
394 {
395 	// count qoute level (to be able to wrap quotes correctly)
396 
397 	const char *quote = QUOTE;
398 	int32 level = 0;
399 	for (size_t i = 0; i < length; i++) {
400 		if (text[i] == quote[0])
401 			level++;
402 		else if (text[i] != ' ' && text[i] != '\t')
403 			break;
404 	}
405 
406 	// if there are too much quotes, try to preserve the quote color level
407 	if (level > 10)
408 		level = kNumQuoteColors * 3 + (level % kNumQuoteColors);
409 
410 	// copy the quotes to outText
411 
412 	const int32 quoteLength = strlen(QUOTE);
413 	outLength = 0;
414 	while (level-- > 0) {
415 		strcpy(outText + outLength, QUOTE);
416 		outLength += quoteLength;
417 	}
418 }
419 
420 
421 int32
422 diff_mode(char c)
423 {
424 	if (c == '+')
425 		return 2;
426 	if (c == '-')
427 		return 1;
428 	if (c == '@')
429 		return 3;
430 	if (c == ' ')
431 		return 0;
432 
433 	// everything else ends the diff mode
434 	return -1;
435 }
436 
437 
438 bool
439 is_quote_char(char c)
440 {
441 	return c == '>' || c == '|';
442 }
443 
444 
445 /*!	Fills the specified text_run_array with the correct values for the
446 	specified text.
447 	If "view" is NULL, it will assume that "line" lies on a line break,
448 	if not, it will correctly retrieve the number of quotes the current
449 	line already has.
450 */
451 void
452 FillInQuoteTextRuns(BTextView* view, quote_context* context, const char* line,
453 	int32 length, const BFont& font, text_run_array* style, int32 maxStyles)
454 {
455 	text_run* runs = style->runs;
456 	int32 index = style->count;
457 	bool begin;
458 	int32 pos = 0;
459 	int32 diffMode = 0;
460 	bool inDiff = false;
461 	bool wasDiff = false;
462 	int32 level = 0;
463 
464 	// get index to the beginning of the current line
465 
466 	if (context != NULL) {
467 		level = context->level;
468 		diffMode = context->diff_mode;
469 		begin = context->begin;
470 		inDiff = context->in_diff;
471 		wasDiff = context->was_diff;
472 	} else if (view != NULL) {
473 		int32 start, end;
474 		view->GetSelection(&end, &end);
475 
476 		begin = view->TextLength() == 0
477 			|| view->ByteAt(view->TextLength() - 1) == '\n';
478 
479 		// the following line works only reliable when text wrapping is set to
480 		// off; so the complicated version actually used here is necessary:
481 		// start = view->OffsetAt(view->CurrentLine());
482 
483 		const char *text = view->Text();
484 
485 		if (!begin) {
486 			// if the text is not the start of a new line, go back
487 			// to the first character in the current line
488 			for (start = end; start > 0; start--) {
489 				if (text[start - 1] == '\n')
490 					break;
491 			}
492 		}
493 
494 		// get number of nested qoutes for current line
495 
496 		if (!begin && start < end) {
497 			begin = true;
498 				// if there was no text in this line, there may come
499 				// more nested quotes
500 
501 			diffMode = diff_mode(text[start]);
502 			if (diffMode == 0) {
503 				for (int32 i = start; i < end; i++) {
504 					if (is_quote_char(text[i]))
505 						level++;
506 					else if (text[i] != ' ' && text[i] != '\t') {
507 						begin = false;
508 						break;
509 					}
510 				}
511 			} else
512 				inDiff = true;
513 
514 			if (begin) {
515 				// skip leading spaces (tabs & newlines aren't allowed here)
516 				while (line[pos] == ' ')
517 					pos++;
518 			}
519 		}
520 	} else
521 		begin = true;
522 
523 	// set styles for all qoute levels in the text to be inserted
524 
525 	for (int32 pos = 0; pos < length;) {
526 		int32 next;
527 		if (begin && is_quote_char(line[pos])) {
528 			begin = false;
529 
530 			while (pos < length && line[pos] != '\n') {
531 				// insert style for each quote level
532 				level++;
533 
534 				bool search = true;
535 				for (next = pos + 1; next < length; next++) {
536 					if ((search && is_quote_char(line[next]))
537 						|| line[next] == '\n')
538 						break;
539 					else if (search && line[next] != ' ' && line[next] != '\t')
540 						search = false;
541 				}
542 
543 				runs[index].offset = pos;
544 				runs[index].font = font;
545 				runs[index].color = level > 0
546 					? kQuoteColors[level % kNumQuoteColors] : kNormalTextColor;
547 
548 				pos = next;
549 				if (++index >= maxStyles)
550 					break;
551 			}
552 		} else {
553 			if (begin) {
554 				if (!inDiff) {
555 					inDiff = !strncmp(&line[pos], "--- ", 4);
556 					wasDiff = false;
557 				}
558 				if (inDiff) {
559 					diffMode = diff_mode(line[pos]);
560 					if (diffMode < 0) {
561 						inDiff = false;
562 						wasDiff = true;
563 					}
564 				}
565 			}
566 
567 			runs[index].offset = pos;
568 			runs[index].font = font;
569 			if (wasDiff)
570 				runs[index].color = kDiffColors[diff_mode('@') - 1];
571 			else if (diffMode <= 0) {
572 				runs[index].color = level > 0
573 					? kQuoteColors[level % kNumQuoteColors] : kNormalTextColor;
574 			} else
575 				runs[index].color = kDiffColors[diffMode - 1];
576 
577 			begin = false;
578 
579 			for (next = pos; next < length; next++) {
580 				if (line[next] == '\n') {
581 					begin = true;
582 					wasDiff = false;
583 					break;
584 				}
585 			}
586 
587 			pos = next;
588 			index++;
589 		}
590 
591 		if (pos < length)
592 			begin = line[pos] == '\n';
593 
594 		if (begin) {
595 			pos++;
596 			level = 0;
597 			wasDiff = false;
598 
599 			// skip one leading space (tabs & newlines aren't allowed here)
600 			if (!inDiff && pos < length && line[pos] == ' ')
601 				pos++;
602 		}
603 
604 		if (index >= maxStyles)
605 			break;
606 	}
607 	style->count = index;
608 
609 	if (context) {
610 		// update context for next run
611 		context->level = level;
612 		context->diff_mode = diffMode;
613 		context->begin = begin;
614 		context->in_diff = inDiff;
615 		context->was_diff = wasDiff;
616 	}
617 }
618 
619 
620 //	#pragma mark -
621 
622 
623 TextRunArray::TextRunArray(size_t entries)
624 	:
625 	fNumEntries(entries)
626 {
627 	fArray = (text_run_array *)malloc(sizeof(int32) + sizeof(text_run) * entries);
628 	if (fArray != NULL)
629 		fArray->count = 0;
630 }
631 
632 
633 TextRunArray::~TextRunArray()
634 {
635 	free(fArray);
636 }
637 
638 
639 //	#pragma mark -
640 
641 
642 TContentView::TContentView(bool incoming, BFont* font,
643 	bool showHeader, bool coloredQuotes)
644 	:
645 	BView("m_content", B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE),
646 	fFocus(false),
647 	fIncoming(incoming)
648 {
649 	SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
650 
651 	BGroupLayout* layout = new BGroupLayout(B_VERTICAL, 0);
652 	SetLayout(layout);
653 
654 	fTextView = new TTextView(fIncoming, this, font, showHeader,
655 		coloredQuotes);
656 
657 	BScrollView* scrollView = new BScrollView("", fTextView, 0, true, true);
658 	scrollView->SetBorders(BControlLook::B_TOP_BORDER);
659 	AddChild(scrollView);
660 }
661 
662 
663 void
664 TContentView::FindString(const char *str)
665 {
666 	int32	finish;
667 	int32	pass = 0;
668 	int32	start = 0;
669 
670 	if (str == NULL)
671 		return;
672 
673 	//
674 	//	Start from current selection or from the beginning of the pool
675 	//
676 	const char *text = fTextView->Text();
677 	int32 count = fTextView->TextLength();
678 	fTextView->GetSelection(&start, &finish);
679 	if (start != finish)
680 		start = finish;
681 	if (!count || text == NULL)
682 		return;
683 
684 	//
685 	//	Do the find
686 	//
687 	while (pass < 2) {
688 		long found = -1;
689 		char lc = tolower(str[0]);
690 		char uc = toupper(str[0]);
691 		for (long i = start; i < count; i++) {
692 			if (text[i] == lc || text[i] == uc) {
693 				const char *s = str;
694 				const char *t = text + i;
695 				while (*s && (tolower(*s) == tolower(*t))) {
696 					s++;
697 					t++;
698 				}
699 				if (*s == 0) {
700 					found = i;
701 					break;
702 				}
703 			}
704 		}
705 
706 		//
707 		//	Select the text if it worked
708 		//
709 		if (found != -1) {
710 			Window()->Activate();
711 			fTextView->Select(found, found + strlen(str));
712 			fTextView->ScrollToSelection();
713 			fTextView->MakeFocus(true);
714 			return;
715 		}
716 		else if (start) {
717 			start = 0;
718 			text = fTextView->Text();
719 			count = fTextView->TextLength();
720 			pass++;
721 		} else {
722 			beep();
723 			return;
724 		}
725 	}
726 }
727 
728 
729 void
730 TContentView::Focus(bool focus)
731 {
732 	if (fFocus != focus) {
733 		fFocus = focus;
734 		Draw(Frame());
735 	}
736 }
737 
738 
739 void
740 TContentView::MessageReceived(BMessage *msg)
741 {
742 	switch (msg->what) {
743 		case CHANGE_FONT:
744 		{
745 			BFont *font;
746 			msg->FindPointer("font", (void **)&font);
747 			fTextView->UpdateFont(font);
748 			fTextView->Invalidate(Bounds());
749 			break;
750 		}
751 
752 		case M_QUOTE:
753 		{
754 			int32 start, finish;
755 			fTextView->GetSelection(&start, &finish);
756 			fTextView->AddQuote(start, finish);
757 			break;
758 		}
759 		case M_REMOVE_QUOTE:
760 		{
761 			int32 start, finish;
762 			fTextView->GetSelection(&start, &finish);
763 			fTextView->RemoveQuote(start, finish);
764 			break;
765 		}
766 
767 		case M_SIGNATURE:
768 		{
769 			if (fTextView->IsReaderThreadRunning()) {
770 				// Do not add the signature until the reader thread
771 				// is finished. Resubmit the message for later processing
772 				Window()->PostMessage(msg);
773 				break;
774 			}
775 
776 			entry_ref ref;
777 			msg->FindRef("ref", &ref);
778 
779 			BFile file(&ref, B_READ_ONLY);
780 			if (file.InitCheck() == B_OK) {
781 				int32 start, finish;
782 				fTextView->GetSelection(&start, &finish);
783 
784 				off_t size;
785 				file.GetSize(&size);
786 				if (size > 32768)	// safety against corrupt signatures
787 					break;
788 
789 				char *signature = (char *)malloc(size);
790 				if (signature == NULL)
791 					break;
792 				ssize_t bytesRead = file.Read(signature, size);
793 				if (bytesRead < B_OK) {
794 					free (signature);
795 					break;
796 				}
797 
798 				const char *text = fTextView->Text();
799 				int32 length = fTextView->TextLength();
800 
801 				// reserve some empty lines before the signature
802 				const char* newLines = "\n\n\n\n";
803 				if (length && text[length - 1] == '\n')
804 					newLines++;
805 
806 				fTextView->Select(length, length);
807 				fTextView->Insert(newLines, strlen(newLines));
808 				length += strlen(newLines);
809 
810 				// append the signature
811 				fTextView->Select(length, length);
812 				fTextView->Insert(signature, bytesRead);
813 				fTextView->Select(length, length + bytesRead);
814 				fTextView->ScrollToSelection();
815 
816 				// set the editing cursor position
817 				fTextView->Select(length - 2 , length - 2);
818 				fTextView->ScrollToSelection();
819 				free (signature);
820 			} else {
821 				beep();
822 				BAlert* alert = new BAlert("",
823 					B_TRANSLATE("An error occurred trying to open this "
824 						"signature."), B_TRANSLATE("Sorry"));
825 				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
826 				alert->Go();
827 			}
828 			break;
829 		}
830 
831 		case M_FIND:
832 			FindString(msg->FindString("findthis"));
833 			break;
834 
835 		default:
836 			BView::MessageReceived(msg);
837 	}
838 }
839 
840 
841 //	#pragma mark -
842 
843 
844 TTextView::TTextView(bool incoming, TContentView *view,
845 	BFont *font, bool showHeader, bool coloredQuotes)
846 	:
847 	BTextView("", B_WILL_DRAW | B_NAVIGABLE),
848 
849 	fHeader(showHeader),
850 	fColoredQuotes(coloredQuotes),
851 	fReady(false),
852 	fYankBuffer(NULL),
853 	fLastPosition(-1),
854 	fMail(NULL),
855 	fFont(font),
856 	fParent(view),
857 	fStopLoading(false),
858 	fThread(0),
859 	fPanel(NULL),
860 	fIncoming(incoming),
861 	fSpellCheck(false),
862 	fRaw(false),
863 	fCursor(false),
864 	fFirstSpellMark(NULL)
865 {
866 	fStopSem = create_sem(1, "reader_sem");
867 	SetStylable(true);
868 	SetInsets(4, 4, 4, 4);
869 		// TODO: have some font size related value here
870 		// (ideally the same as in BTextControl, etc. from BControlLook)
871 
872 	fEnclosures = new BList();
873 
874 	// Enclosure pop up menu
875 	fEnclosureMenu = new BPopUpMenu("Enclosure", false, false);
876 	fEnclosureMenu->SetFont(be_plain_font);
877 	fEnclosureMenu->AddItem(new BMenuItem(
878 		B_TRANSLATE("Save attachment" B_UTF8_ELLIPSIS),	new BMessage(M_SAVE)));
879 	fEnclosureMenu->AddItem(new BMenuItem(B_TRANSLATE("Open attachment"),
880 		new BMessage(M_OPEN)));
881 
882 	// Hyperlink pop up menu
883 	fLinkMenu = new BPopUpMenu("Link", false, false);
884 	fLinkMenu->SetFont(be_plain_font);
885 	fLinkMenu->AddItem(new BMenuItem(B_TRANSLATE("Open this link"),
886 		new BMessage(M_OPEN)));
887 	fLinkMenu->AddItem(new BMenuItem(B_TRANSLATE("Copy link location"),
888 		new BMessage(M_COPY)));
889 
890 	SetDoesUndo(true);
891 
892 	//Undo function
893 	fUndoBuffer.On();
894 	fInputMethodUndoBuffer.On();
895 	fUndoState.replaced = false;
896 	fUndoState.deleted = false;
897 	fInputMethodUndoState.active = false;
898 	fInputMethodUndoState.replace = false;
899 }
900 
901 
902 TTextView::~TTextView()
903 {
904 	ClearList();
905 	delete fPanel;
906 
907 	if (fYankBuffer)
908 		free(fYankBuffer);
909 
910 	delete_sem(fStopSem);
911 }
912 
913 
914 void
915 TTextView::UpdateFont(const BFont* newFont)
916 {
917 	fFont = *newFont;
918 
919 	// update the text run array safely with new font
920 	text_run_array *runArray = RunArray(0, INT32_MAX);
921 	for (int i = 0; i < runArray->count; i++)
922 		runArray->runs[i].font = *newFont;
923 
924 	SetRunArray(0, INT32_MAX, runArray);
925 	FreeRunArray(runArray);
926 }
927 
928 
929 void
930 TTextView::AttachedToWindow()
931 {
932 	BTextView::AttachedToWindow();
933 	fFont.SetSpacing(B_FIXED_SPACING);
934 	SetFontAndColor(&fFont);
935 
936 	if (fMail != NULL) {
937 		LoadMessage(fMail, false, NULL);
938 		if (fIncoming)
939 			MakeEditable(false);
940 	}
941 }
942 
943 
944 void
945 TTextView::KeyDown(const char *key, int32 count)
946 {
947 	char		raw;
948 	int32		end;
949 	int32 		start;
950 	uint32		mods;
951 	BMessage	*msg;
952 	int32		textLen = TextLength();
953 
954 	msg = Window()->CurrentMessage();
955 	mods = msg->FindInt32("modifiers");
956 
957 	switch (key[0]) {
958 		case B_HOME:
959 			if (IsSelectable()) {
960 				if (IsEditable())
961 					BTextView::KeyDown(key, count);
962 				else {
963 					// scroll to the beginning
964 					Select(0, 0);
965 					ScrollToSelection();
966 				}
967 			}
968 			break;
969 
970 		case B_END:
971 			if (IsSelectable()) {
972 				if (IsEditable())
973 					BTextView::KeyDown(key, count);
974 				else {
975 					// scroll to the end
976 					int32 length = TextLength();
977 					Select(length, length);
978 					ScrollToSelection();
979 				}
980 			}
981 			break;
982 
983 		case 0x02:						// ^b - back 1 char
984 			if (IsSelectable()) {
985 				GetSelection(&start, &end);
986 				while (!IsInitialUTF8Byte(ByteAt(--start))) {
987 					if (start < 0) {
988 						start = 0;
989 						break;
990 					}
991 				}
992 				if (start >= 0) {
993 					Select(start, start);
994 					ScrollToSelection();
995 				}
996 			}
997 			break;
998 
999 		case B_DELETE:
1000 			if (IsSelectable()) {
1001 				if ((key[0] == B_DELETE) || (mods & B_CONTROL_KEY)) {
1002 					// ^d
1003 					if (IsEditable()) {
1004 						GetSelection(&start, &end);
1005 						if (start != end)
1006 							Delete();
1007 						else {
1008 							for (end = start + 1; !IsInitialUTF8Byte(ByteAt(end)); end++) {
1009 								if (end > textLen) {
1010 									end = textLen;
1011 									break;
1012 								}
1013 							}
1014 							Select(start, end);
1015 							Delete();
1016 						}
1017 					}
1018 				}
1019 				else
1020 					Select(textLen, textLen);
1021 				ScrollToSelection();
1022 			}
1023 			break;
1024 
1025 		case 0x05:						// ^e - end of line
1026 			if (IsSelectable() && (mods & B_CONTROL_KEY)) {
1027 				if (CurrentLine() == CountLines() - 1)
1028 					Select(TextLength(), TextLength());
1029 				else {
1030 					GoToLine(CurrentLine() + 1);
1031 					GetSelection(&start, &end);
1032 					Select(start - 1, start - 1);
1033 				}
1034 			}
1035 			break;
1036 
1037 		case 0x06:						// ^f - forward 1 char
1038 			if (IsSelectable()) {
1039 				GetSelection(&start, &end);
1040 				if (end > start)
1041 					start = end;
1042 				else {
1043 					for (end = start + 1; !IsInitialUTF8Byte(ByteAt(end));
1044 						end++) {
1045 						if (end > textLen) {
1046 							end = textLen;
1047 							break;
1048 						}
1049 					}
1050 					start = end;
1051 				}
1052 				Select(start, start);
1053 				ScrollToSelection();
1054 			}
1055 			break;
1056 
1057 		case 0x0e:						// ^n - next line
1058 			if (IsSelectable()) {
1059 				raw = B_DOWN_ARROW;
1060 				BTextView::KeyDown(&raw, 1);
1061 			}
1062 			break;
1063 
1064 		case 0x0f:						// ^o - open line
1065 			if (IsEditable()) {
1066 				GetSelection(&start, &end);
1067 				Delete();
1068 
1069 				char newLine = '\n';
1070 				Insert(&newLine, 1);
1071 				Select(start, start);
1072 				ScrollToSelection();
1073 			}
1074 			break;
1075 
1076 		case B_PAGE_UP:
1077 			if (mods & B_CONTROL_KEY) {	// ^k kill text from cursor to e-o-line
1078 				if (IsEditable()) {
1079 					GetSelection(&start, &end);
1080 					if ((start != fLastPosition) && (fYankBuffer)) {
1081 						free(fYankBuffer);
1082 						fYankBuffer = NULL;
1083 					}
1084 					fLastPosition = start;
1085 					if (CurrentLine() < CountLines() - 1) {
1086 						GoToLine(CurrentLine() + 1);
1087 						GetSelection(&end, &end);
1088 						end--;
1089 					}
1090 					else
1091 						end = TextLength();
1092 					if (end < start)
1093 						break;
1094 					if (start == end)
1095 						end++;
1096 					Select(start, end);
1097 					if (fYankBuffer) {
1098 						fYankBuffer = (char *)realloc(fYankBuffer,
1099 									 strlen(fYankBuffer) + (end - start) + 1);
1100 						GetText(start, end - start,
1101 							    &fYankBuffer[strlen(fYankBuffer)]);
1102 					} else {
1103 						fYankBuffer = (char *)malloc(end - start + 1);
1104 						GetText(start, end - start, fYankBuffer);
1105 					}
1106 					Delete();
1107 					ScrollToSelection();
1108 				}
1109 				break;
1110 			}
1111 
1112 			BTextView::KeyDown(key, count);
1113 			break;
1114 
1115 		case 0x10:						// ^p goto previous line
1116 			if (IsSelectable()) {
1117 				raw = B_UP_ARROW;
1118 				BTextView::KeyDown(&raw, 1);
1119 			}
1120 			break;
1121 
1122 		case 0x19:						// ^y yank text
1123 			if (IsEditable() && fYankBuffer) {
1124 				Delete();
1125 				Insert(fYankBuffer);
1126 				ScrollToSelection();
1127 			}
1128 			break;
1129 
1130 		default:
1131 			BTextView::KeyDown(key, count);
1132 	}
1133 }
1134 
1135 
1136 void
1137 TTextView::MakeFocus(bool focus)
1138 {
1139 	if (!focus) {
1140 		// ToDo: can someone please translate this? Otherwise I will remove it - axeld.
1141 		// MakeFocus(false) は、IM も Inactive になり、そのまま確定される。
1142 		// しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED)
1143 		// を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。
1144 		fInputMethodUndoState.active = false;
1145 		// fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら(確定)正規のバッファへ追加
1146 		if (fInputMethodUndoBuffer.CountItems() > 0) {
1147 			KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1);
1148 			if (item->History == K_INSERTED) {
1149 				fUndoBuffer.MakeNewUndoItem();
1150 				fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset, item->History, item->CursorPos);
1151 				fUndoBuffer.MakeNewUndoItem();
1152 			}
1153 			fInputMethodUndoBuffer.MakeEmpty();
1154 		}
1155 	}
1156 	BTextView::MakeFocus(focus);
1157 
1158 	fParent->Focus(focus);
1159 }
1160 
1161 
1162 void
1163 TTextView::MessageReceived(BMessage *msg)
1164 {
1165 	switch (msg->what) {
1166 		case B_SIMPLE_DATA:
1167 		{
1168 			if (fIncoming)
1169 				break;
1170 
1171 			BMessage message(REFS_RECEIVED);
1172 			bool isEnclosure = false;
1173 			bool inserted = false;
1174 
1175 			off_t len = 0;
1176 			int32 end;
1177 			int32 start;
1178 
1179 			int32 index = 0;
1180 			entry_ref ref;
1181 			while (msg->FindRef("refs", index++, &ref) == B_OK) {
1182 				BFile file(&ref, B_READ_ONLY);
1183 				if (file.InitCheck() == B_OK) {
1184 					BNodeInfo node(&file);
1185 					char type[B_FILE_NAME_LENGTH];
1186 					node.GetType(type);
1187 
1188 					off_t size = 0;
1189 					file.GetSize(&size);
1190 
1191 					if (!strncasecmp(type, "text/", 5) && size > 0) {
1192 						len += size;
1193 						char *text = (char *)malloc(size);
1194 						if (text == NULL) {
1195 							puts("no memory!");
1196 							return;
1197 						}
1198 						if (file.Read(text, size) < B_OK) {
1199 							puts("could not read from file");
1200 							free(text);
1201 							continue;
1202 						}
1203 						if (!inserted) {
1204 							GetSelection(&start, &end);
1205 							Delete();
1206 							inserted = true;
1207 						}
1208 
1209 						int32 offset = 0;
1210 						for (int32 loop = 0; loop < size; loop++) {
1211 							if (text[loop] == '\n') {
1212 								Insert(&text[offset], loop - offset + 1);
1213 								offset = loop + 1;
1214 							} else if (text[loop] == '\r') {
1215 								text[loop] = '\n';
1216 								Insert(&text[offset], loop - offset + 1);
1217 								if ((loop + 1 < size)
1218 										&& (text[loop + 1] == '\n'))
1219 									loop++;
1220 								offset = loop + 1;
1221 							}
1222 						}
1223 						free(text);
1224 					} else {
1225 						isEnclosure = true;
1226 						message.AddRef("refs", &ref);
1227 					}
1228 				}
1229 			}
1230 
1231 			if (index == 1) {
1232 				// message doesn't contain any refs - maybe the parent class likes it
1233 				BTextView::MessageReceived(msg);
1234 				break;
1235 			}
1236 
1237 			if (inserted)
1238 				Select(start, start + len);
1239 			if (isEnclosure)
1240 				Window()->PostMessage(&message, Window());
1241 			break;
1242 		}
1243 
1244 		case M_HEADER:
1245 			msg->FindBool("header", &fHeader);
1246 			SetText(NULL);
1247 			LoadMessage(fMail, false, NULL);
1248 			break;
1249 
1250 		case M_RAW:
1251 			StopLoad();
1252 
1253 			msg->FindBool("raw", &fRaw);
1254 			SetText(NULL);
1255 			LoadMessage(fMail, false, NULL);
1256 			break;
1257 
1258 		case M_SELECT:
1259 			if (IsSelectable())
1260 				Select(0, TextLength());
1261 			break;
1262 
1263 		case M_SAVE:
1264 			Save(msg);
1265 			break;
1266 
1267 		case B_NODE_MONITOR:
1268 		{
1269 			int32 opcode;
1270 			if (msg->FindInt32("opcode", &opcode) == B_NO_ERROR) {
1271 				dev_t device;
1272 				if (msg->FindInt32("device", &device) < B_OK)
1273 					break;
1274 				ino_t inode;
1275 				if (msg->FindInt64("node", &inode) < B_OK)
1276 					break;
1277 
1278 				hyper_text *enclosure;
1279 				for (int32 index = 0;
1280 						(enclosure = (hyper_text *)fEnclosures->ItemAt(index++)) != NULL;) {
1281 					if (device == enclosure->node.device
1282 						&& inode == enclosure->node.node) {
1283 						if (opcode == B_ENTRY_REMOVED) {
1284 							enclosure->saved = false;
1285 							enclosure->have_ref = false;
1286 						} else if (opcode == B_ENTRY_MOVED) {
1287 							enclosure->ref.device = device;
1288 							msg->FindInt64("to directory", &enclosure->ref.directory);
1289 
1290 							const char *name;
1291 							msg->FindString("name", &name);
1292 							enclosure->ref.set_name(name);
1293 						}
1294 						break;
1295 					}
1296 				}
1297 			}
1298 			break;
1299 		}
1300 
1301 		//
1302 		// Tracker has responded to a BMessage that was dragged out of
1303 		// this email message.  It has created a file for us, we just have to
1304 		// put the stuff in it.
1305 		//
1306 		case B_COPY_TARGET:
1307 		{
1308 			BMessage data;
1309 			if (msg->FindMessage("be:originator-data", &data) == B_OK) {
1310 				entry_ref directory;
1311 				const char *name;
1312 				hyper_text *enclosure;
1313 
1314 				if (data.FindPointer("enclosure", (void **)&enclosure) == B_OK
1315 					&& msg->FindString("name", &name) == B_OK
1316 					&& msg->FindRef("directory", &directory) == B_OK) {
1317 					switch (enclosure->type) {
1318 						case TYPE_ENCLOSURE:
1319 						case TYPE_BE_ENCLOSURE:
1320 						{
1321 							//
1322 							//	Enclosure.  Decode the data and write it out.
1323 							//
1324 							BMessage saveMsg(M_SAVE);
1325 							saveMsg.AddString("name", name);
1326 							saveMsg.AddRef("directory", &directory);
1327 							saveMsg.AddPointer("enclosure", enclosure);
1328 							Save(&saveMsg, false);
1329 							break;
1330 						}
1331 
1332 						case TYPE_URL:
1333 						{
1334 							const char *replyType;
1335 							if (msg->FindString("be:filetypes", &replyType) != B_OK)
1336 								// drag recipient didn't ask for any specific type,
1337 								// create a bookmark file as default
1338 								replyType = "application/x-vnd.Be-bookmark";
1339 
1340 							BDirectory dir(&directory);
1341 							BFile file(&dir, name, B_READ_WRITE);
1342 							if (file.InitCheck() == B_OK) {
1343 								if (strcmp(replyType, "application/x-vnd.Be-bookmark") == 0) {
1344 									// we got a request to create a bookmark, stuff
1345 									// it with the url attribute
1346 									file.WriteAttr("META:url", B_STRING_TYPE, 0,
1347 													enclosure->name, strlen(enclosure->name) + 1);
1348 								} else if (strcasecmp(replyType, "text/plain") == 0) {
1349 									// create a plain text file, stuff it with
1350 									// the url as text
1351 									file.Write(enclosure->name, strlen(enclosure->name));
1352 								}
1353 
1354 								BNodeInfo fileInfo(&file);
1355 								fileInfo.SetType(replyType);
1356 							}
1357 							break;
1358 						}
1359 
1360 						case TYPE_MAILTO:
1361 						{
1362 							//
1363 							//	Add some attributes to the already created
1364 							//	person file.  Strip out the 'mailto:' if
1365 							//  possible.
1366 							//
1367 							char *addrStart = enclosure->name;
1368 							while (true) {
1369 								if (*addrStart == ':') {
1370 									addrStart++;
1371 									break;
1372 								}
1373 
1374 								if (*addrStart == '\0') {
1375 									addrStart = enclosure->name;
1376 									break;
1377 								}
1378 
1379 								addrStart++;
1380 							}
1381 
1382 							const char *replyType;
1383 							if (msg->FindString("be:filetypes", &replyType) != B_OK)
1384 								// drag recipient didn't ask for any specific type,
1385 								// create a bookmark file as default
1386 								replyType = "application/x-vnd.Be-bookmark";
1387 
1388 							BDirectory dir(&directory);
1389 							BFile file(&dir, name, B_READ_WRITE);
1390 							if (file.InitCheck() == B_OK) {
1391 								if (!strcmp(replyType, "application/x-person")) {
1392 									// we got a request to create a bookmark, stuff
1393 									// it with the address attribute
1394 									file.WriteAttr("META:email", B_STRING_TYPE, 0,
1395 									  addrStart, strlen(enclosure->name) + 1);
1396 								} else if (!strcasecmp(replyType, "text/plain")) {
1397 									// create a plain text file, stuff it with the
1398 									// email as text
1399 									file.Write(addrStart, strlen(addrStart));
1400 								}
1401 
1402 								BNodeInfo fileInfo(&file);
1403 								fileInfo.SetType(replyType);
1404 							}
1405 							break;
1406 						}
1407 					}
1408 				} else {
1409 					//
1410 					// Assume this is handled by BTextView...
1411 					// (Probably drag clipping.)
1412 					//
1413 					BTextView::MessageReceived(msg);
1414 				}
1415 			}
1416 			break;
1417 		}
1418 
1419 		case B_INPUT_METHOD_EVENT:
1420 		{
1421 			int32 im_op;
1422 			if (msg->FindInt32("be:opcode", &im_op) == B_OK){
1423 				switch (im_op) {
1424 					case B_INPUT_METHOD_STARTED:
1425 						fInputMethodUndoState.replace = true;
1426 						fInputMethodUndoState.active = true;
1427 						break;
1428 					case B_INPUT_METHOD_STOPPED:
1429 						fInputMethodUndoState.active = false;
1430 						if (fInputMethodUndoBuffer.CountItems() > 0) {
1431 							KUndoItem *undo = fInputMethodUndoBuffer.ItemAt(
1432 								fInputMethodUndoBuffer.CountItems() - 1);
1433 							if (undo->History == K_INSERTED){
1434 								fUndoBuffer.MakeNewUndoItem();
1435 								fUndoBuffer.AddUndo(undo->RedoText, undo->Length,
1436 									undo->Offset, undo->History, undo->CursorPos);
1437 								fUndoBuffer.MakeNewUndoItem();
1438 							}
1439 							fInputMethodUndoBuffer.MakeEmpty();
1440 						}
1441 						break;
1442 					case B_INPUT_METHOD_CHANGED:
1443 						fInputMethodUndoState.active = true;
1444 						break;
1445 					case B_INPUT_METHOD_LOCATION_REQUEST:
1446 						fInputMethodUndoState.active = true;
1447 						break;
1448 				}
1449 			}
1450 			BTextView::MessageReceived(msg);
1451 			break;
1452 		}
1453 
1454 		case M_REDO:
1455 			Redo();
1456 			break;
1457 
1458 		default:
1459 			BTextView::MessageReceived(msg);
1460 	}
1461 }
1462 
1463 
1464 void
1465 TTextView::MouseDown(BPoint where)
1466 {
1467 	if (IsEditable()) {
1468 		BPoint point;
1469 		uint32 buttons;
1470 		GetMouse(&point, &buttons);
1471 		if (gDictCount && (buttons == B_SECONDARY_MOUSE_BUTTON)) {
1472 			int32 offset, start, end, length;
1473 			const char *text = Text();
1474 			offset = OffsetAt(where);
1475 			if (isalpha(text[offset])) {
1476 				length = TextLength();
1477 
1478 				//Find start and end of word
1479 				//FindSpellBoundry(length, offset, &start, &end);
1480 
1481 				char c;
1482 				bool isAlpha, isApost, isCap;
1483 				int32 first;
1484 
1485 				for (first = offset;
1486 					(first >= 0) && (((c = text[first]) == '\'') || isalpha(c));
1487 					first--) {}
1488 				isCap = isupper(text[++first]);
1489 
1490 				for (start = offset, c = text[start], isAlpha = isalpha(c), isApost = (c=='\'');
1491 					(start >= 0) && (isAlpha || (isApost
1492 					&& (((c = text[start+1]) != 's') || !isCap) && isalpha(c)
1493 					&& isalpha(text[start-1])));
1494 					start--, c = text[start], isAlpha = isalpha(c), isApost = (c == '\'')) {}
1495 				start++;
1496 
1497 				for (end = offset, c = text[end], isAlpha = isalpha(c), isApost = (c == '\'');
1498 					(end < length) && (isAlpha || (isApost
1499 					&& (((c = text[end + 1]) != 's') || !isCap) && isalpha(c)));
1500 					end++, c = text[end], isAlpha = isalpha(c), isApost = (c == '\'')) {}
1501 
1502 				length = end - start;
1503 				BString srcWord;
1504 				srcWord.SetTo(text + start, length);
1505 
1506 				bool		foundWord = false;
1507 				BList 		matches;
1508 				BString 	*string;
1509 
1510 				BMenuItem *menuItem;
1511 				BPopUpMenu menu("Words", false, false);
1512 
1513 				for (int32 i = 0; i < gDictCount; i++)
1514 					gWords[i]->FindBestMatches(&matches,
1515 						srcWord.String());
1516 
1517 				if (matches.CountItems()) {
1518 					sort_word_list(&matches, srcWord.String());
1519 					for (int32 i = 0; (string = (BString *)matches.ItemAt(i)) != NULL; i++) {
1520 						menu.AddItem((menuItem = new BMenuItem(string->String(), NULL)));
1521 						if (!strcasecmp(string->String(), srcWord.String())) {
1522 							menuItem->SetEnabled(false);
1523 							foundWord = true;
1524 						}
1525 						delete string;
1526 					}
1527 				} else {
1528 					menuItem = new BMenuItem(B_TRANSLATE("No matches"), NULL);
1529 					menuItem->SetEnabled(false);
1530 					menu.AddItem(menuItem);
1531 				}
1532 
1533 				BMenuItem *addItem = NULL;
1534 				if (!foundWord && gUserDict >= 0) {
1535 					menu.AddSeparatorItem();
1536 					addItem = new BMenuItem(B_TRANSLATE("Add"), NULL);
1537 					menu.AddItem(addItem);
1538 				}
1539 
1540 				point = ConvertToScreen(where);
1541 				if ((menuItem = menu.Go(point, false, false)) != NULL) {
1542 					if (menuItem == addItem) {
1543 						BString newItem(srcWord.String());
1544 						newItem << "\n";
1545 						gWords[gUserDict]->InitIndex();
1546 						gExactWords[gUserDict]->InitIndex();
1547 						gUserDictFile->Write(newItem.String(), newItem.Length());
1548 						gWords[gUserDict]->BuildIndex();
1549 						gExactWords[gUserDict]->BuildIndex();
1550 
1551 						if (fSpellCheck)
1552 							CheckSpelling(0, TextLength());
1553 					} else {
1554 						int32 len = strlen(menuItem->Label());
1555 						Select(start, start);
1556 						Delete(start, end);
1557 						Insert(start, menuItem->Label(), len);
1558 						Select(start+len, start+len);
1559 					}
1560 				}
1561 			}
1562 			return;
1563 		} else if (fSpellCheck && IsEditable()) {
1564 			int32 start, end;
1565 
1566 			GetSelection(&start, &end);
1567 			FindSpellBoundry(1, start, &start, &end);
1568 			CheckSpelling(start, end);
1569 		}
1570 	} else {
1571 		// is not editable, look for enclosures/links
1572 
1573 		int32 clickOffset = OffsetAt(where);
1574 		int32 items = fEnclosures->CountItems();
1575 		for (int32 loop = 0; loop < items; loop++) {
1576 			hyper_text *enclosure = (hyper_text*) fEnclosures->ItemAt(loop);
1577 			if (clickOffset < enclosure->text_start || clickOffset >= enclosure->text_end)
1578 				continue;
1579 
1580 			//
1581 			// The user is clicking on this attachment
1582 			//
1583 
1584 			int32 start;
1585 			int32 finish;
1586 			Select(enclosure->text_start, enclosure->text_end);
1587 			GetSelection(&start, &finish);
1588 			Window()->UpdateIfNeeded();
1589 
1590 			bool drag = false;
1591 			bool held = false;
1592 			uint32 buttons = 0;
1593 			if (Window()->CurrentMessage()) {
1594 				Window()->CurrentMessage()->FindInt32("buttons",
1595 				  (int32 *) &buttons);
1596 			}
1597 
1598 			//
1599 			// If this is the primary button, wait to see if the user is going
1600 			// to single click, hold, or drag.
1601 			//
1602 			if (buttons != B_SECONDARY_MOUSE_BUTTON) {
1603 				BPoint point = where;
1604 				bigtime_t popupDelay;
1605 				get_click_speed(&popupDelay);
1606 				popupDelay *= 2;
1607 				popupDelay += system_time();
1608 				while (buttons && abs((int)(point.x - where.x)) < 4
1609 					&& abs((int)(point.y - where.y)) < 4
1610 					&& system_time() < popupDelay) {
1611 					snooze(10000);
1612 					GetMouse(&point, &buttons);
1613 				}
1614 
1615 				if (system_time() < popupDelay) {
1616 					//
1617 					// The user either dragged this or released the button.
1618 					// check if it was dragged.
1619 					//
1620 					if (!(abs((int)(point.x - where.x)) < 4
1621 						&& abs((int)(point.y - where.y)) < 4) && buttons)
1622 						drag = true;
1623 				} else  {
1624 					//
1625 					//	The user held the button down.
1626 					//
1627 					held = true;
1628 				}
1629 			}
1630 
1631 			//
1632 			//	If the user has right clicked on this menu,
1633 			// 	or held the button down on it for a while,
1634 			//	pop up a context menu.
1635 			//
1636 			if (buttons == B_SECONDARY_MOUSE_BUTTON || held) {
1637 				//
1638 				// Right mouse click... Display a menu
1639 				//
1640 				BPoint point = where;
1641 				ConvertToScreen(&point);
1642 
1643 				BMenuItem *item;
1644 				if ((enclosure->type != TYPE_ENCLOSURE)
1645 					&& (enclosure->type != TYPE_BE_ENCLOSURE))
1646 					item = fLinkMenu->Go(point, true);
1647 				else
1648 					item = fEnclosureMenu->Go(point, true);
1649 
1650 				BMessage *msg;
1651 				if (item && (msg = item->Message()) != NULL) {
1652 					if (msg->what == M_SAVE) {
1653 						if (fPanel)
1654 							fPanel->SetEnclosure(enclosure);
1655 						else {
1656 							fPanel = new TSavePanel(enclosure, this);
1657 							fPanel->Window()->Show();
1658 						}
1659 					} else if (msg->what == M_COPY) {
1660 						// copy link location to clipboard
1661 
1662 						if (be_clipboard->Lock()) {
1663 							be_clipboard->Clear();
1664 
1665 							BMessage *clip;
1666 							if ((clip = be_clipboard->Data()) != NULL) {
1667 								clip->AddData("text/plain", B_MIME_TYPE,
1668 									enclosure->name, strlen(enclosure->name));
1669 								be_clipboard->Commit();
1670 							}
1671 							be_clipboard->Unlock();
1672 						}
1673 					} else
1674 						Open(enclosure);
1675 				}
1676 			} else {
1677 				//
1678 				// Left button.  If the user single clicks, open this link.
1679 				// Otherwise, initiate a drag.
1680 				//
1681 				if (drag) {
1682 					BMessage dragMessage(B_SIMPLE_DATA);
1683 					dragMessage.AddInt32("be:actions", B_COPY_TARGET);
1684 					dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1685 					switch (enclosure->type) {
1686 						case TYPE_BE_ENCLOSURE:
1687 						case TYPE_ENCLOSURE:
1688 							//
1689 							// Attachment.  The type is specified in the message.
1690 							//
1691 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1692 							dragMessage.AddString("be:filetypes",
1693 								enclosure->content_type ? enclosure->content_type : "");
1694 							dragMessage.AddString("be:clip_name", enclosure->name);
1695 							break;
1696 
1697 						case TYPE_URL:
1698 							//
1699 							// URL.  The user can drag it into the tracker to
1700 							// create a bookmark file.
1701 							//
1702 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1703 							dragMessage.AddString("be:filetypes",
1704 							  "application/x-vnd.Be-bookmark");
1705 							dragMessage.AddString("be:filetypes", "text/plain");
1706 							dragMessage.AddString("be:clip_name", "Bookmark");
1707 
1708 							dragMessage.AddString("be:url", enclosure->name);
1709 							break;
1710 
1711 						case TYPE_MAILTO:
1712 							//
1713 							// Mailto address.  The user can drag it into the
1714 							// tracker to create a people file.
1715 							//
1716 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1717 							dragMessage.AddString("be:filetypes",
1718 							  "application/x-person");
1719 							dragMessage.AddString("be:filetypes", "text/plain");
1720 							dragMessage.AddString("be:clip_name", "Person");
1721 
1722 							dragMessage.AddString("be:email", enclosure->name);
1723 							break;
1724 
1725 						default:
1726 							//
1727 							// Otherwise it doesn't have a type that I know how
1728 							// to save.  It won't have any types and if any
1729 							// program wants to accept it, more power to them.
1730 							// (tracker won't.)
1731 							//
1732 							dragMessage.AddString("be:clip_name", "Hyperlink");
1733 					}
1734 
1735 					BMessage data;
1736 					data.AddPointer("enclosure", enclosure);
1737 					dragMessage.AddMessage("be:originator-data", &data);
1738 
1739 					BRegion selectRegion;
1740 					GetTextRegion(start, finish, &selectRegion);
1741 					DragMessage(&dragMessage, selectRegion.Frame(), this);
1742 				} else {
1743 					//
1744 					//	User Single clicked on the attachment.  Open it.
1745 					//
1746 					Open(enclosure);
1747 				}
1748 			}
1749 			return;
1750 		}
1751 	}
1752 	BTextView::MouseDown(where);
1753 }
1754 
1755 
1756 void
1757 TTextView::MouseMoved(BPoint where, uint32 code, const BMessage *msg)
1758 {
1759 	int32 start = OffsetAt(where);
1760 
1761 	for (int32 loop = fEnclosures->CountItems(); loop-- > 0;) {
1762 		hyper_text *enclosure = (hyper_text *)fEnclosures->ItemAt(loop);
1763 		if ((start >= enclosure->text_start) && (start < enclosure->text_end)) {
1764 			if (!fCursor)
1765 				SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
1766 			fCursor = true;
1767 			return;
1768 		}
1769 	}
1770 
1771 	if (fCursor) {
1772 		SetViewCursor(B_CURSOR_I_BEAM);
1773 		fCursor = false;
1774 	}
1775 
1776 	BTextView::MouseMoved(where, code, msg);
1777 }
1778 
1779 
1780 void
1781 TTextView::ClearList()
1782 {
1783 	hyper_text *enclosure;
1784 	while ((enclosure = (hyper_text *)fEnclosures->FirstItem()) != NULL) {
1785 		fEnclosures->RemoveItem(enclosure);
1786 
1787 		if (enclosure->name)
1788 			free(enclosure->name);
1789 		if (enclosure->content_type)
1790 			free(enclosure->content_type);
1791 		if (enclosure->encoding)
1792 			free(enclosure->encoding);
1793 		if (enclosure->have_ref && !enclosure->saved) {
1794 			BEntry entry(&enclosure->ref);
1795 			entry.Remove();
1796 		}
1797 
1798 		watch_node(&enclosure->node, B_STOP_WATCHING, this);
1799 		free(enclosure);
1800 	}
1801 }
1802 
1803 
1804 void
1805 TTextView::LoadMessage(BEmailMessage *mail, bool quoteIt, const char *text)
1806 {
1807 	StopLoad();
1808 
1809 	fMail = mail;
1810 
1811 	ClearList();
1812 
1813 	MakeSelectable(true);
1814 	MakeEditable(false);
1815 	if (text)
1816 		Insert(text, strlen(text));
1817 
1818 	//attr_info attrInfo;
1819 	TTextView::Reader *reader = new TTextView::Reader(fHeader, fRaw, quoteIt, fIncoming,
1820 				text != NULL, true,
1821 				// I removed the following, because I absolutely can't imagine why it's
1822 				// there (the mail kit should be able to deal with non-compliant mails)
1823 				// -- axeld.
1824 				// fFile->GetAttrInfo(B_MAIL_ATTR_MIME, &attrInfo) == B_OK,
1825 				this, mail, fEnclosures, fStopSem);
1826 
1827 	resume_thread(fThread = spawn_thread(Reader::Run, "reader", B_NORMAL_PRIORITY, reader));
1828 }
1829 
1830 
1831 void
1832 TTextView::Open(hyper_text *enclosure)
1833 {
1834 	switch (enclosure->type) {
1835 		case TYPE_URL:
1836 		{
1837 			const struct {const char *urlType, *handler; } handlerTable[] = {
1838 				{"http",	B_URL_HTTP},
1839 				{"https",	B_URL_HTTPS},
1840 				{"ftp",		B_URL_FTP},
1841 				{"gopher",	B_URL_GOPHER},
1842 				{"mailto",	B_URL_MAILTO},
1843 				{"news",	B_URL_NEWS},
1844 				{"nntp",	B_URL_NNTP},
1845 				{"telnet",	B_URL_TELNET},
1846 				{"rlogin",	B_URL_RLOGIN},
1847 				{"tn3270",	B_URL_TN3270},
1848 				{"wais",	B_URL_WAIS},
1849 				{"file",	B_URL_FILE},
1850 				{NULL,		NULL}
1851 			};
1852 			const char *handlerToLaunch = NULL;
1853 
1854 			const char *colonPos = strchr(enclosure->name, ':');
1855 			if (colonPos) {
1856 				int urlTypeLength = colonPos - enclosure->name;
1857 
1858 				for (int32 index = 0; handlerTable[index].urlType; index++) {
1859 					if (!strncasecmp(enclosure->name,
1860 							handlerTable[index].urlType, urlTypeLength)) {
1861 						handlerToLaunch = handlerTable[index].handler;
1862 						break;
1863 					}
1864 				}
1865 			}
1866 			if (handlerToLaunch) {
1867 				entry_ref appRef;
1868 				if (be_roster->FindApp(handlerToLaunch, &appRef) != B_OK)
1869 					handlerToLaunch = NULL;
1870 			}
1871 			if (!handlerToLaunch)
1872 				handlerToLaunch = "application/x-vnd.Be-Bookmark";
1873 
1874 			status_t result = be_roster->Launch(handlerToLaunch, 1, &enclosure->name);
1875 			if (result != B_NO_ERROR && result != B_ALREADY_RUNNING) {
1876 				beep();
1877 				BAlert* alert = new BAlert("",
1878 					B_TRANSLATE("There is no installed handler for "
1879 						"URL links."), B_TRANSLATE("Sorry"));
1880 				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
1881 				alert->Go();
1882 			}
1883 			break;
1884 		}
1885 
1886 		case TYPE_MAILTO:
1887 			if (be_roster->Launch(B_MAIL_TYPE, 1, &enclosure->name) < B_OK) {
1888 				char *argv[] = {(char *)"Mail", enclosure->name};
1889 				be_app->ArgvReceived(2, argv);
1890 			}
1891 			break;
1892 
1893 		case TYPE_ENCLOSURE:
1894 		case TYPE_BE_ENCLOSURE:
1895 			if (!enclosure->have_ref) {
1896 				BPath path;
1897 				if (find_directory(B_SYSTEM_TEMP_DIRECTORY, &path) == B_NO_ERROR) {
1898 					BDirectory dir(path.Path());
1899 					if (dir.InitCheck() == B_NO_ERROR) {
1900 						char name[B_FILE_NAME_LENGTH];
1901 						char baseName[B_FILE_NAME_LENGTH];
1902 						strcpy(baseName, enclosure->name ? enclosure->name : "enclosure");
1903 						strcpy(name, baseName);
1904 						for (int32 index = 0; dir.Contains(name); index++)
1905 							sprintf(name, "%s_%" B_PRId32, baseName, index);
1906 
1907 						BEntry entry(path.Path());
1908 						entry_ref ref;
1909 						entry.GetRef(&ref);
1910 
1911 						BMessage save(M_SAVE);
1912 						save.AddRef("directory", &ref);
1913 						save.AddString("name", name);
1914 						save.AddPointer("enclosure", enclosure);
1915 						if (Save(&save) != B_NO_ERROR)
1916 							break;
1917 						enclosure->saved = false;
1918 					}
1919 				}
1920 			}
1921 
1922 			BMessenger tracker("application/x-vnd.Be-TRAK");
1923 			if (tracker.IsValid()) {
1924 				BMessage openMsg(B_REFS_RECEIVED);
1925 				openMsg.AddRef("refs", &enclosure->ref);
1926 				tracker.SendMessage(&openMsg);
1927 			}
1928 			break;
1929 	}
1930 }
1931 
1932 
1933 status_t
1934 TTextView::Save(BMessage *msg, bool makeNewFile)
1935 {
1936 	const char		*name;
1937 	entry_ref		ref;
1938 	BFile			file;
1939 	BPath			path;
1940 	hyper_text		*enclosure;
1941 	status_t		result = B_NO_ERROR;
1942 	char 			entry_name[B_FILE_NAME_LENGTH];
1943 
1944 	msg->FindString("name", &name);
1945 	msg->FindRef("directory", &ref);
1946 	msg->FindPointer("enclosure", (void **)&enclosure);
1947 
1948 	BDirectory dir;
1949 	dir.SetTo(&ref);
1950 	result = dir.InitCheck();
1951 
1952 	if (result == B_OK) {
1953 		if (makeNewFile) {
1954 			//
1955 			// Search for the file and delete it if it already exists.
1956 			// (It may not, that's ok.)
1957 			//
1958 			BEntry entry;
1959 			if (dir.FindEntry(name, &entry) == B_NO_ERROR)
1960 				entry.Remove();
1961 
1962 			if ((enclosure->have_ref) && (!enclosure->saved)) {
1963 				entry.SetTo(&enclosure->ref);
1964 
1965 				//
1966 				// Added true arg and entry_name so MoveTo clobbers as
1967 				// before. This may not be the correct behaviour, but
1968 				// it's the preserved behaviour.
1969 				//
1970 				entry.GetName(entry_name);
1971 				result = entry.MoveTo(&dir, entry_name, true);
1972 				if (result == B_NO_ERROR) {
1973 					entry.Rename(name);
1974 					entry.GetRef(&enclosure->ref);
1975 					entry.GetNodeRef(&enclosure->node);
1976 					enclosure->saved = true;
1977 					return result;
1978 				}
1979 			}
1980 
1981 			if (result == B_NO_ERROR) {
1982 				result = dir.CreateFile(name, &file);
1983 				if (result == B_NO_ERROR && enclosure->content_type) {
1984 					char type[B_MIME_TYPE_LENGTH];
1985 
1986 					if (!strcasecmp(enclosure->content_type, "message/rfc822"))
1987 						strcpy(type, "text/x-email");
1988 					else if (!strcasecmp(enclosure->content_type, "message/delivery-status"))
1989 						strcpy(type, "text/plain");
1990 					else
1991 						strcpy(type, enclosure->content_type);
1992 
1993 					BNodeInfo info(&file);
1994 					info.SetType(type);
1995 				}
1996 			}
1997 		} else {
1998 			//
1999 			// 	This file was dragged into the tracker or desktop.  The file
2000 			//	already exists.
2001 			//
2002 			result = file.SetTo(&dir, name, B_WRITE_ONLY);
2003 		}
2004 	}
2005 
2006 	if (enclosure->component == NULL)
2007 		result = B_ERROR;
2008 
2009 	if (result == B_NO_ERROR) {
2010 		//
2011 		// Write the data
2012 		//
2013 		enclosure->component->GetDecodedData(&file);
2014 
2015 		BEntry entry;
2016 		dir.FindEntry(name, &entry);
2017 		entry.GetRef(&enclosure->ref);
2018 		enclosure->have_ref = true;
2019 		enclosure->saved = true;
2020 		entry.GetPath(&path);
2021 		update_mime_info(path.Path(), false, true,
2022 			!cistrcmp("application/octet-stream", enclosure->content_type ? enclosure->content_type : B_EMPTY_STRING));
2023 		entry.GetNodeRef(&enclosure->node);
2024 		watch_node(&enclosure->node, B_WATCH_NAME, this);
2025 	}
2026 
2027 	if (result != B_NO_ERROR) {
2028 		beep();
2029 		BAlert* alert = new BAlert("", B_TRANSLATE("An error occurred trying to save "
2030 				"the attachment."),	B_TRANSLATE("Sorry"));
2031 		alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
2032 		alert->Go();
2033 	}
2034 
2035 	return result;
2036 }
2037 
2038 
2039 void
2040 TTextView::StopLoad()
2041 {
2042 	Window()->Unlock();
2043 
2044 	thread_info	info;
2045 	if (fThread != 0 && get_thread_info(fThread, &info) == B_NO_ERROR) {
2046 		fStopLoading = true;
2047 		acquire_sem(fStopSem);
2048 		int32 result;
2049 		wait_for_thread(fThread, &result);
2050 		fThread = 0;
2051 		release_sem(fStopSem);
2052 		fStopLoading = false;
2053 	}
2054 
2055 	Window()->Lock();
2056 }
2057 
2058 
2059 bool
2060 TTextView::IsReaderThreadRunning()
2061 {
2062 	if (fThread == 0)
2063 		return false;
2064 
2065 	thread_info info;
2066 	for (int i = 5; i > 0; i--, usleep(100000))
2067 		if (get_thread_info(fThread, &info) != B_OK)
2068 			return false;
2069 	return true;
2070 }
2071 
2072 
2073 void
2074 TTextView::AddAsContent(BEmailMessage *mail, bool wrap, uint32 charset, mail_encoding encoding)
2075 {
2076 	if (mail == NULL)
2077 		return;
2078 
2079 	int32 textLength = TextLength();
2080 	const char *text = Text();
2081 
2082 	BTextMailComponent *body = mail->Body();
2083 	if (body == NULL) {
2084 		if (mail->SetBody(body = new BTextMailComponent()) < B_OK)
2085 			return;
2086 	}
2087 	body->SetEncoding(encoding, charset);
2088 
2089 	// Just add the text as a whole if we can, or ...
2090 	if (!wrap) {
2091 		body->AppendText(text);
2092 		return;
2093 	}
2094 
2095 	// ... do word wrapping.
2096 
2097 	BWindow	*window = Window();
2098 	char *saveText = strdup(text);
2099 	BRect saveTextRect = TextRect();
2100 
2101 	// do this before we start messing with the fonts
2102 	// the user will never know...
2103 	window->DisableUpdates();
2104 	Hide();
2105 	BScrollBar *vScroller = ScrollBar(B_VERTICAL);
2106 	BScrollBar *hScroller = ScrollBar(B_HORIZONTAL);
2107 	if (vScroller != NULL)
2108 		vScroller->SetTarget((BView *)NULL);
2109 	if (hScroller != NULL)
2110 		hScroller->SetTarget((BView *)NULL);
2111 
2112 	// Temporarily set the font to a fixed width font for line wrapping
2113 	// calculations.  If the font doesn't have as many of the symbols as
2114 	// the preferred font, go back to using the user's preferred font.
2115 
2116 	bool *boolArray;
2117 	int missingCharactersFixedWidth = 0;
2118 	int missingCharactersPreferredFont = 0;
2119 	int32 numberOfCharacters;
2120 
2121 	numberOfCharacters = BString(text).CountChars();
2122 	if (numberOfCharacters > 0
2123 		&& (boolArray = (bool *)malloc(sizeof(bool) * numberOfCharacters)) != NULL) {
2124 		memset(boolArray, 0, sizeof (bool) * numberOfCharacters);
2125 		be_fixed_font->GetHasGlyphs(text, numberOfCharacters, boolArray);
2126 		for (int i = 0; i < numberOfCharacters; i++) {
2127 			if (!boolArray[i])
2128 				missingCharactersFixedWidth += 1;
2129 		}
2130 
2131 		memset(boolArray, 0, sizeof (bool) * numberOfCharacters);
2132 		fFont.GetHasGlyphs(text, numberOfCharacters, boolArray);
2133 		for (int i = 0; i < numberOfCharacters; i++) {
2134 			if (!boolArray[i])
2135 				missingCharactersPreferredFont += 1;
2136 		}
2137 
2138 		free(boolArray);
2139 	}
2140 
2141 	if (missingCharactersFixedWidth > missingCharactersPreferredFont)
2142 		SetFontAndColor(0, textLength, &fFont);
2143 	else // All things being equal, the fixed font is better for wrapping.
2144 		SetFontAndColor(0, textLength, be_fixed_font);
2145 
2146 	// calculate a text rect that is 72 columns wide
2147 	BRect newTextRect = saveTextRect;
2148 	newTextRect.right = newTextRect.left + be_fixed_font->StringWidth("m") * 72;
2149 	SetTextRect(newTextRect);
2150 
2151 	// hard-wrap, based on TextView's soft-wrapping
2152 	int32 numLines = CountLines();
2153 	bool spaceMoved = false;
2154 	char *content = (char *)malloc(textLength + numLines * 72);	// more we'll ever need
2155 	if (content != NULL) {
2156 		int32 contentLength = 0;
2157 
2158 		for (int32 i = 0; i < numLines; i++) {
2159 			int32 startOffset = OffsetAt(i);
2160 			if (spaceMoved) {
2161 				startOffset++;
2162 				spaceMoved = false;
2163 			}
2164 			int32 endOffset = OffsetAt(i + 1);
2165 			int32 lineLength = endOffset - startOffset;
2166 
2167 			// quick hack to not break URLs into several parts
2168 			for (int32 pos = startOffset; pos < endOffset; pos++) {
2169 				size_t urlLength;
2170 				uint8 type = CheckForURL(text + pos, urlLength);
2171 				if (type != 0)
2172 					pos += urlLength;
2173 
2174 				if (pos > endOffset) {
2175 					// find first break character after the URL
2176 					for (; text[pos]; pos++) {
2177 						if (isalnum(text[pos]) || isspace(text[pos]))
2178 							break;
2179 					}
2180 					if (text[pos] && isspace(text[pos]) && text[pos] != '\n')
2181 						pos++;
2182 
2183 					endOffset += pos - endOffset;
2184 					lineLength = endOffset - startOffset;
2185 
2186 					// insert a newline (and the same number of quotes) after the
2187 					// URL to make sure the rest of the text is properly wrapped
2188 
2189 					char buffer[64];
2190 					if (text[pos] == '\n')
2191 						buffer[0] = '\0';
2192 					else
2193 						strcpy(buffer, "\n");
2194 
2195 					size_t quoteLength;
2196 					CopyQuotes(text + startOffset, lineLength, buffer + strlen(buffer), quoteLength);
2197 
2198 					Insert(pos, buffer, strlen(buffer));
2199 					numLines = CountLines();
2200 					text = Text();
2201 					i++;
2202 				}
2203 			}
2204 			if (text[endOffset - 1] != ' '
2205 				&& text[endOffset - 1] != '\n'
2206 				&& text[endOffset] == ' ') {
2207 				// make sure spaces will be part of this line
2208 				endOffset++;
2209 				lineLength++;
2210 				spaceMoved = true;
2211 			}
2212 
2213 			memcpy(content + contentLength, text + startOffset, lineLength);
2214 			contentLength += lineLength;
2215 
2216 			// add a newline to every line except for the ones
2217 			// that already end in newlines, and the last line
2218 			if ((text[endOffset - 1] != '\n') && (i < numLines - 1)) {
2219 				content[contentLength++] = '\n';
2220 
2221 				// copy quote level of the first line
2222 				size_t quoteLength;
2223 				CopyQuotes(text + startOffset, lineLength, content + contentLength, quoteLength);
2224 				contentLength += quoteLength;
2225 			}
2226 		}
2227 		content[contentLength] = '\0';
2228 
2229 		body->AppendText(content);
2230 		free(content);
2231 	}
2232 
2233 	// reset the text rect and font
2234 	SetTextRect(saveTextRect);
2235 	SetText(saveText);
2236 	free(saveText);
2237 	SetFontAndColor(0, textLength, &fFont);
2238 
2239 	// should be OK to hook these back up now
2240 	if (vScroller != NULL)
2241 		vScroller->SetTarget(this);
2242 	if (hScroller != NULL)
2243 		hScroller->SetTarget(this);
2244 
2245 	Show();
2246 	window->EnableUpdates();
2247 }
2248 
2249 
2250 //	#pragma mark -
2251 
2252 
2253 TTextView::Reader::Reader(bool header, bool raw, bool quote, bool incoming,
2254 		bool stripHeader, bool mime, TTextView *view, BEmailMessage *mail,
2255 		BList *list, sem_id sem)
2256 	:
2257 	fHeader(header),
2258 	fRaw(raw),
2259 	fQuote(quote),
2260 	fIncoming(incoming),
2261 	fStripHeader(stripHeader),
2262 	fMime(mime),
2263 	fView(view),
2264 	fMail(mail),
2265 	fEnclosures(list),
2266 	fStopSem(sem)
2267 {
2268 }
2269 
2270 
2271 bool
2272 TTextView::Reader::ParseMail(BMailContainer *container,
2273 	BTextMailComponent *ignore)
2274 {
2275 	int32 count = 0;
2276 	for (int32 i = 0; i < container->CountComponents(); i++) {
2277 		if (fView->fStopLoading)
2278 			return false;
2279 
2280 		BMailComponent *component;
2281 		if ((component = container->GetComponent(i)) == NULL) {
2282 			if (fView->fStopLoading)
2283 				return false;
2284 
2285 			hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2286 			if (enclosure == NULL)
2287 				return false;
2288 
2289 			memset(enclosure, 0, sizeof(hyper_text));
2290 
2291 			enclosure->type = TYPE_ENCLOSURE;
2292 
2293 			const char *name = "\n<Attachment: could not handle>\n";
2294 
2295 			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
2296 			enclosure->text_start++;
2297 			enclosure->text_end += strlen(name) - 1;
2298 
2299 			Insert(name, strlen(name), true);
2300 			fEnclosures->AddItem(enclosure);
2301 			continue;
2302 		}
2303 
2304 		count++;
2305 		if (component == ignore)
2306 			continue;
2307 
2308 		if (component->ComponentType() == B_MAIL_MULTIPART_CONTAINER) {
2309 			BMIMEMultipartMailContainer *c = dynamic_cast<BMIMEMultipartMailContainer *>(container->GetComponent(i));
2310 			ASSERT(c != NULL);
2311 
2312 			if (!ParseMail(c, ignore))
2313 				count--;
2314 		} else if (fIncoming) {
2315 			hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2316 			if (enclosure == NULL)
2317 				return false;
2318 
2319 			memset(enclosure, 0, sizeof(hyper_text));
2320 
2321 			enclosure->type = TYPE_ENCLOSURE;
2322 			enclosure->component = component;
2323 
2324 			BString name;
2325 			char fileName[B_FILE_NAME_LENGTH];
2326 			strcpy(fileName, "untitled");
2327 			if (BMailAttachment *attachment = dynamic_cast <BMailAttachment *> (component))
2328 				attachment->FileName(fileName);
2329 
2330 			BPath path(fileName);
2331 			enclosure->name = strdup(path.Leaf());
2332 
2333 			BMimeType type;
2334 			component->MIMEType(&type);
2335 			enclosure->content_type = strdup(type.Type());
2336 
2337 			char typeDescription[B_MIME_TYPE_LENGTH];
2338 			if (type.GetShortDescription(typeDescription) != B_OK)
2339 				strcpy(typeDescription, type.Type() ? type.Type() : B_EMPTY_STRING);
2340 
2341 			name = "\n<";
2342 			name.Append(B_TRANSLATE_COMMENT("Enclosure: %name% (Type: %type%)",
2343 				"Don't translate the variables %name% and %type%."));
2344 			name.Append(">\n");
2345 			name.ReplaceFirst("%name%", enclosure->name);
2346 			name.ReplaceFirst("%type%", typeDescription);
2347 
2348 			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
2349 			enclosure->text_start++;
2350 			enclosure->text_end += strlen(name.String()) - 1;
2351 
2352 			Insert(name.String(), name.Length(), true);
2353 			fEnclosures->AddItem(enclosure);
2354 		}
2355 //			default:
2356 //			{
2357 //				PlainTextBodyComponent *body = dynamic_cast<PlainTextBodyComponent *>(container->GetComponent(i));
2358 //				const char *text;
2359 //				if (body && (text = body->Text()) != NULL)
2360 //					Insert(text, strlen(text), false);
2361 //			}
2362 	}
2363 	return count > 0;
2364 }
2365 
2366 
2367 bool
2368 TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader)
2369 {
2370 	char line[522];
2371 	int32 count = 0;
2372 
2373 	for (int32 loop = 0; loop < data_len; loop++) {
2374 		if (fView->fStopLoading)
2375 			return false;
2376 
2377 		if (fQuote && (!loop || (loop && data[loop - 1] == '\n'))) {
2378 			strcpy(&line[count], QUOTE);
2379 			count += strlen(QUOTE);
2380 		}
2381 		if (!fRaw && fIncoming && (loop < data_len - 7)) {
2382 			size_t urlLength;
2383 			BString url;
2384 			uint8 type = CheckForURL(data + loop, urlLength, &url);
2385 
2386 			if (type) {
2387 				if (!Insert(line, count, false, isHeader))
2388 					return false;
2389 				count = 0;
2390 
2391 				hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2392 				if (enclosure == NULL)
2393 					return false;
2394 
2395 				memset(enclosure, 0, sizeof(hyper_text));
2396 				fView->GetSelection(&enclosure->text_start,
2397 									&enclosure->text_end);
2398 				enclosure->type = type;
2399 				enclosure->name = strdup(url.String());
2400 				if (enclosure->name == NULL) {
2401 					free(enclosure);
2402 					return false;
2403 				}
2404 
2405 				Insert(&data[loop], urlLength, true, isHeader);
2406 				enclosure->text_end += urlLength;
2407 				loop += urlLength - 1;
2408 
2409 				fEnclosures->AddItem(enclosure);
2410 				continue;
2411 			}
2412 		}
2413 		if (!fRaw && fMime && data[loop] == '=') {
2414 			if ((loop) && (loop < data_len - 1) && (data[loop + 1] == '\r'))
2415 				loop += 2;
2416 			else
2417 				line[count++] = data[loop];
2418 		} else if (data[loop] != '\r')
2419 			line[count++] = data[loop];
2420 
2421 		if (count > 511 || (count && loop == data_len - 1)) {
2422 			if (!Insert(line, count, false, isHeader))
2423 				return false;
2424 			count = 0;
2425 		}
2426 	}
2427 	return true;
2428 }
2429 
2430 
2431 bool
2432 TTextView::Reader::Insert(const char *line, int32 count, bool isHyperLink,
2433 	bool isHeader)
2434 {
2435 	if (!count)
2436 		return true;
2437 
2438 	BFont font(fView->Font());
2439 	TextRunArray style(count / 8 + 8);
2440 
2441 	if (fView->fColoredQuotes && !isHeader && !isHyperLink) {
2442 		FillInQuoteTextRuns(fView, &fQuoteContext, line, count, font,
2443 			&style.Array(), style.MaxEntries());
2444 	} else {
2445 		text_run_array &array = style.Array();
2446 		array.count = 1;
2447 		array.runs[0].offset = 0;
2448 		if (isHeader) {
2449 			array.runs[0].color = isHyperLink ? kHyperLinkColor : kHeaderColor;
2450 			font.SetSize(font.Size() * 0.9);
2451 		} else {
2452 			array.runs[0].color = isHyperLink
2453 				? kHyperLinkColor : kNormalTextColor;
2454 		}
2455 		array.runs[0].font = font;
2456 	}
2457 
2458 	if (!fView->Window()->Lock())
2459 		return false;
2460 
2461 	fView->Insert(fView->TextLength(), line, count, &style.Array());
2462 
2463 	fView->Window()->Unlock();
2464 	return true;
2465 }
2466 
2467 
2468 status_t
2469 TTextView::Reader::Run(void *_this)
2470 {
2471 	Reader *reader = (Reader *)_this;
2472 	TTextView *view = reader->fView;
2473 	char *msg = NULL;
2474 	off_t size = 0;
2475 	int32 len = 0;
2476 
2477 	if (!reader->Lock())
2478 		return B_INTERRUPTED;
2479 
2480 	BFile *file = dynamic_cast<BFile *>(reader->fMail->Data());
2481 	if (file != NULL) {
2482 		len = header_len(file);
2483 
2484 		if (reader->fHeader)
2485 			size = len;
2486 		if (reader->fRaw || !reader->fMime)
2487 			file->GetSize(&size);
2488 
2489 		if (size != 0 && (msg = (char *)malloc(size)) == NULL)
2490 			goto done;
2491 		file->Seek(0, 0);
2492 
2493 		if (msg)
2494 			size = file->Read(msg, size);
2495 	}
2496 
2497 	// show the header?
2498 	if (reader->fHeader && len) {
2499 		// strip all headers except "From", "To", "Reply-To", "Subject", and "Date"
2500 	 	if (reader->fStripHeader) {
2501 		 	const char *header = msg;
2502 		 	char *buffer = NULL;
2503 
2504 			while (strncmp(header, "\r\n", 2)) {
2505 				const char *eol = header;
2506 				while ((eol = strstr(eol, "\r\n")) != NULL && isspace(eol[2]))
2507 					eol += 2;
2508 				if (eol == NULL)
2509 					break;
2510 
2511 				eol += 2;	// CR+LF belong to the line
2512 				size_t length = eol - header;
2513 
2514 		 		buffer = (char *)realloc(buffer, length + 1);
2515 		 		if (buffer == NULL)
2516 		 			goto done;
2517 
2518 		 		memcpy(buffer, header, length);
2519 
2520 				length = rfc2047_to_utf8(&buffer, &length, length);
2521 
2522 		 		if (!strncasecmp(header, "Reply-To: ", 10)
2523 		 			|| !strncasecmp(header, "To: ", 4)
2524 		 			|| !strncasecmp(header, "From: ", 6)
2525 		 			|| !strncasecmp(header, "Subject: ", 8)
2526 		 			|| !strncasecmp(header, "Date: ", 6))
2527 		 			reader->Process(buffer, length, true);
2528 
2529 		 		header = eol;
2530 	 		}
2531 		 	free(buffer);
2532 	 		reader->Process("\r\n", 2, true);
2533 	 	}
2534 	 	else if (!reader->Process(msg, len, true))
2535 			goto done;
2536 	}
2537 
2538 	if (reader->fRaw) {
2539 		if (!reader->Process((const char *)msg + len, size - len))
2540 			goto done;
2541 	} else {
2542 		//reader->fFile->Seek(0, 0);
2543 		//BEmailMessage *mail = new BEmailMessage(reader->fFile);
2544 		BEmailMessage *mail = reader->fMail;
2545 
2546 		// at first, insert the mail body
2547 		BTextMailComponent *body = NULL;
2548 		if (mail->BodyText() && !view->fStopLoading) {
2549 			char *bodyText = const_cast<char *>(mail->BodyText());
2550 			int32 bodyLength = strlen(bodyText);
2551 			body = mail->Body();
2552 			bool isHTML = false;
2553 
2554 			BMimeType type;
2555 			if (body->MIMEType(&type) == B_OK && type == "text/html") {
2556 				// strip out HTML tags
2557 				char *t = bodyText, *a, *end = bodyText + bodyLength;
2558 				bodyText = (char *)malloc(bodyLength + 1);
2559 				isHTML = true;
2560 
2561 				// TODO: is it correct to assume that the text is in Latin-1?
2562 				//		because if it isn't, the code below won't work correctly...
2563 
2564 				for (a = bodyText; t < end; t++) {
2565 					int32 c = *t;
2566 
2567 					// compact spaces
2568 					bool space = false;
2569 					while (c && (c == ' ' || c == '\t')) {
2570 						c = *(++t);
2571 						space = true;
2572 					}
2573 					if (space) {
2574 						c = ' ';
2575 						t--;
2576 					} else if (FilterHTMLTag(c, &t, end))	// the tag filter
2577 						continue;
2578 
2579 					Unicode2UTF8(c, &a);
2580 				}
2581 
2582 				*a = 0;
2583 				bodyLength = strlen(bodyText);
2584 				body = NULL;	// to add the HTML text as enclosure
2585 			}
2586 			if (!reader->Process(bodyText, bodyLength))
2587 				goto done;
2588 
2589 			if (isHTML)
2590 				free(bodyText);
2591 		}
2592 
2593 		if (!reader->ParseMail(mail, body))
2594 			goto done;
2595 
2596 		//reader->fView->fMail = mail;
2597 	}
2598 
2599 	if (!view->fStopLoading && view->Window()->Lock()) {
2600 		view->Select(0, 0);
2601 		view->MakeSelectable(true);
2602 		if (!reader->fIncoming)
2603 			view->MakeEditable(true);
2604 
2605 		view->Window()->Unlock();
2606 	}
2607 
2608 done:
2609 	// restore the reading position if available
2610 	view->Window()->PostMessage(M_READ_POS);
2611 
2612 	reader->Unlock();
2613 
2614 	delete reader;
2615 	free(msg);
2616 
2617 	return B_NO_ERROR;
2618 }
2619 
2620 
2621 status_t
2622 TTextView::Reader::Unlock()
2623 {
2624 	return release_sem(fStopSem);
2625 }
2626 
2627 
2628 bool
2629 TTextView::Reader::Lock()
2630 {
2631 	if (acquire_sem_etc(fStopSem, 1, B_TIMEOUT, 0) != B_NO_ERROR)
2632 		return false;
2633 
2634 	return true;
2635 }
2636 
2637 
2638 //====================================================================
2639 //	#pragma mark -
2640 
2641 
2642 TSavePanel::TSavePanel(hyper_text *enclosure, TTextView *view)
2643 	: BFilePanel(B_SAVE_PANEL)
2644 {
2645 	fEnclosure = enclosure;
2646 	fView = view;
2647 	if (enclosure->name)
2648 		SetSaveText(enclosure->name);
2649 }
2650 
2651 
2652 void
2653 TSavePanel::SendMessage(const BMessenger * /* messenger */, BMessage *msg)
2654 {
2655 	const char	*name = NULL;
2656 	BMessage	save(M_SAVE);
2657 	entry_ref	ref;
2658 
2659 	if ((!msg->FindRef("directory", &ref)) && (!msg->FindString("name", &name))) {
2660 		save.AddPointer("enclosure", fEnclosure);
2661 		save.AddString("name", name);
2662 		save.AddRef("directory", &ref);
2663 		fView->Window()->PostMessage(&save, fView);
2664 	}
2665 }
2666 
2667 
2668 void
2669 TSavePanel::SetEnclosure(hyper_text *enclosure)
2670 {
2671 	fEnclosure = enclosure;
2672 	if (enclosure->name)
2673 		SetSaveText(enclosure->name);
2674 	else
2675 		SetSaveText("");
2676 
2677 	if (!IsShowing())
2678 		Show();
2679 	Window()->Activate();
2680 }
2681 
2682 
2683 //--------------------------------------------------------------------
2684 //	#pragma mark -
2685 
2686 
2687 void
2688 TTextView::InsertText(const char *insertText, int32 length, int32 offset,
2689 	const text_run_array *runs)
2690 {
2691 	ContentChanged();
2692 
2693 	// Undo function
2694 
2695 	int32 cursorPos, dummy;
2696 	GetSelection(&cursorPos, &dummy);
2697 
2698 	if (fInputMethodUndoState.active) {
2699 		// IMアクティブ時は、一旦別のバッファへ記憶
2700 		fInputMethodUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
2701 		fInputMethodUndoState.replace = false;
2702 	} else {
2703 		if (fUndoState.replaced) {
2704 			fUndoBuffer.AddUndo(insertText, length, offset, K_REPLACED, cursorPos);
2705 		} else {
2706 			if (length == 1 && insertText[0] == 0x0a)
2707 				fUndoBuffer.MakeNewUndoItem();
2708 
2709 			fUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
2710 
2711 			if (length == 1 && insertText[0] == 0x0a)
2712 				fUndoBuffer.MakeNewUndoItem();
2713 		}
2714 	}
2715 
2716 	fUndoState.replaced = false;
2717 	fUndoState.deleted = false;
2718 
2719 	struct text_runs : text_run_array { text_run _runs[1]; } style;
2720 	if (runs == NULL && IsEditable()) {
2721 		style.count = 1;
2722 		style.runs[0].offset = 0;
2723 		style.runs[0].font = fFont;
2724 		style.runs[0].color = kNormalTextColor;
2725 		runs = &style;
2726 	}
2727 
2728 	BTextView::InsertText(insertText, length, offset, runs);
2729 
2730 	if (fSpellCheck && IsEditable())
2731 	{
2732 		UpdateSpellMarks(offset, length);
2733 
2734 		rgb_color color;
2735 		GetFontAndColor(offset - 1, NULL, &color);
2736 		const char *text = Text();
2737 
2738 		if (length > 1
2739 			|| isalpha(text[offset + 1])
2740 			|| (!isalpha(text[offset]) && text[offset] != '\'')
2741 			|| (color.red == kSpellTextColor.red
2742 				&& color.green == kSpellTextColor.green
2743 				&& color.blue == kSpellTextColor.blue))
2744 		{
2745 			int32 start, end;
2746 			FindSpellBoundry(length, offset, &start, &end);
2747 
2748 			DSPELL(printf("Offset %ld, start %ld, end %ld\n", offset, start, end));
2749 			DSPELL(printf("\t\"%10.10s...\"\n", text + start));
2750 
2751 			CheckSpelling(start, end);
2752 		}
2753 	}
2754 }
2755 
2756 
2757 void
2758 TTextView::DeleteText(int32 start, int32 finish)
2759 {
2760 	ContentChanged();
2761 
2762 	// Undo function
2763 	int32 cursorPos, dummy;
2764 	GetSelection(&cursorPos, &dummy);
2765 	if (fInputMethodUndoState.active) {
2766 		if (fInputMethodUndoState.replace) {
2767 			fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
2768 			fInputMethodUndoState.replace = false;
2769 		} else {
2770 			fInputMethodUndoBuffer.AddUndo(&Text()[start], finish - start, start,
2771 				K_DELETED, cursorPos);
2772 		}
2773 	} else
2774 		fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
2775 
2776 	fUndoState.deleted = true;
2777 	fUndoState.replaced = true;
2778 
2779 	BTextView::DeleteText(start, finish);
2780 	if (fSpellCheck && IsEditable()) {
2781 		UpdateSpellMarks(start, start - finish);
2782 
2783 		int32 s, e;
2784 		FindSpellBoundry(1, start, &s, &e);
2785 		CheckSpelling(s, e);
2786 	}
2787 }
2788 
2789 
2790 void
2791 TTextView::ContentChanged(void)
2792 {
2793 	BLooper *looper = Looper();
2794 	if (looper == NULL)
2795 		return;
2796 
2797 	BMessage msg(FIELD_CHANGED);
2798 	msg.AddInt32("bitmask", FIELD_BODY);
2799 	msg.AddPointer("source", this);
2800 	looper->PostMessage(&msg);
2801 }
2802 
2803 
2804 void
2805 TTextView::CheckSpelling(int32 start, int32 end, int32 flags)
2806 {
2807 	const char 	*text = Text();
2808 	const char 	*next, *endPtr, *word = NULL;
2809 	int32 		wordLength = 0, wordOffset;
2810 	int32		nextHighlight = start;
2811 	BString 	testWord;
2812 	bool		isCap = false;
2813 	bool		isAlpha;
2814 	bool		isApost;
2815 
2816 	for (next = text + start, endPtr = text + end; next <= endPtr; next++) {
2817 		//printf("next=%c\n", *next);
2818 		// ToDo: this has to be refined to other languages...
2819 		// Alpha signifies the start of a word
2820 		isAlpha = isalpha(*next);
2821 		isApost = (*next == '\'');
2822 		if (!word && isAlpha) {
2823 			//printf("Found word start\n");
2824 			word = next;
2825 			wordLength++;
2826 			isCap = isupper(*word);
2827 		} else if (word && (isAlpha || isApost) && !(isApost && !isalpha(next[1]))
2828 					&& !(isCap && isApost && (next[1] == 's'))) {
2829 			// Word continues check
2830 			wordLength++;
2831 			//printf("Word continues...\n");
2832 		} else if (word) {
2833 			// End of word reached
2834 
2835 			//printf("Word End\n");
2836 			// Don't check single characters
2837 			if (wordLength > 1) {
2838 				bool isUpper = true;
2839 
2840 				// Look for all uppercase
2841 				for (int32 i = 0; i < wordLength; i++) {
2842 					if (word[i] == '\'')
2843 						break;
2844 
2845 					if (islower(word[i])) {
2846 						isUpper = false;
2847 						break;
2848 					}
2849 				}
2850 
2851 				// Don't check all uppercase words
2852 				if (!isUpper) {
2853 					bool foundMatch = false;
2854 					wordOffset = word - text;
2855 					testWord.SetTo(word, wordLength);
2856 
2857 					testWord = testWord.ToLower();
2858 					DSPELL(printf("Testing: \"%s\"\n", testWord.String()));
2859 
2860 					int32 key = -1;
2861 					if (gDictCount)
2862 						key = gExactWords[0]->GetKey(testWord.String());
2863 
2864 					// Search all dictionaries
2865 					for (int32 i = 0; i < gDictCount; i++) {
2866 						if (gExactWords[i]->Lookup(key) >= 0) {
2867 							foundMatch = true;
2868 							break;
2869 						}
2870 					}
2871 
2872 					if (!foundMatch) {
2873 						if (flags & S_CLEAR_ERRORS)
2874 							RemoveSpellMark(nextHighlight, wordOffset);
2875 
2876 						if (flags & S_SHOW_ERRORS)
2877 							AddSpellMark(wordOffset, wordOffset + wordLength);
2878 					} else if (flags & S_CLEAR_ERRORS)
2879 						RemoveSpellMark(nextHighlight, wordOffset + wordLength);
2880 
2881 					nextHighlight = wordOffset + wordLength;
2882 				}
2883 			}
2884 			// Reset state to looking for word
2885 			word = NULL;
2886 			wordLength = 0;
2887 		}
2888 	}
2889 
2890 	if (nextHighlight <= end
2891 		&& (flags & S_CLEAR_ERRORS) != 0
2892 		&& nextHighlight < TextLength())
2893 		SetFontAndColor(nextHighlight, end, NULL, B_FONT_ALL, &kNormalTextColor);
2894 }
2895 
2896 
2897 void
2898 TTextView::FindSpellBoundry(int32 length, int32 offset, int32 *_start, int32 *_end)
2899 {
2900 	int32 start, end, textLength;
2901 	const char *text = Text();
2902 	textLength = TextLength();
2903 
2904 	for (start = offset - 1; start >= 0
2905 		&& (isalpha(text[start]) || text[start] == '\''); start--) {}
2906 
2907 	start++;
2908 
2909 	for (end = offset + length; end < textLength
2910 		&& (isalpha(text[end]) || text[end] == '\''); end++) {}
2911 
2912 	*_start = start;
2913 	*_end = end;
2914 }
2915 
2916 
2917 TTextView::spell_mark *
2918 TTextView::FindSpellMark(int32 start, int32 end, spell_mark **_previousMark)
2919 {
2920 	spell_mark *lastMark = NULL;
2921 
2922 	for (spell_mark *spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
2923 		if (spellMark->start < end && spellMark->end > start) {
2924 			if (_previousMark)
2925 				*_previousMark = lastMark;
2926 			return spellMark;
2927 		}
2928 
2929 		lastMark = spellMark;
2930 	}
2931 	return NULL;
2932 }
2933 
2934 
2935 void
2936 TTextView::UpdateSpellMarks(int32 offset, int32 length)
2937 {
2938 	DSPELL(printf("UpdateSpellMarks: offset = %ld, length = %ld\n", offset, length));
2939 
2940 	spell_mark *spellMark;
2941 	for (spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
2942 		DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));
2943 
2944 		if (spellMark->end < offset)
2945 			continue;
2946 
2947 		if (spellMark->start > offset)
2948 			spellMark->start += length;
2949 
2950 		spellMark->end += length;
2951 
2952 		DSPELL(printf("\t-> reset: %ld - %ld\n", spellMark->start, spellMark->end));
2953 	}
2954 }
2955 
2956 
2957 status_t
2958 TTextView::AddSpellMark(int32 start, int32 end)
2959 {
2960 	DSPELL(printf("AddSpellMark: start = %ld, end = %ld\n", start, end));
2961 
2962 	// check if there is already a mark for this passage
2963 	spell_mark *spellMark = FindSpellMark(start, end);
2964 	if (spellMark) {
2965 		if (spellMark->start == start && spellMark->end == end) {
2966 			DSPELL(printf("\tfound one\n"));
2967 			return B_OK;
2968 		}
2969 
2970 		DSPELL(printf("\tremove old one\n"));
2971 		RemoveSpellMark(start, end);
2972 	}
2973 
2974 	spellMark = (spell_mark *)malloc(sizeof(spell_mark));
2975 	if (spellMark == NULL)
2976 		return B_NO_MEMORY;
2977 
2978 	spellMark->start = start;
2979 	spellMark->end = end;
2980 	spellMark->style = RunArray(start, end);
2981 
2982 	// set the spell marks appearance
2983 	BFont font(fFont);
2984 	font.SetFace(B_BOLD_FACE | B_ITALIC_FACE);
2985 	SetFontAndColor(start, end, &font, B_FONT_ALL, &kSpellTextColor);
2986 
2987 	// add it to the queue
2988 	spellMark->next = fFirstSpellMark;
2989 	fFirstSpellMark = spellMark;
2990 
2991 	return B_OK;
2992 }
2993 
2994 
2995 bool
2996 TTextView::RemoveSpellMark(int32 start, int32 end)
2997 {
2998 	DSPELL(printf("RemoveSpellMark: start = %ld, end = %ld\n", start, end));
2999 
3000 	// find spell mark
3001 	spell_mark *lastMark = NULL;
3002 	spell_mark *spellMark = FindSpellMark(start, end, &lastMark);
3003 	if (spellMark == NULL) {
3004 		DSPELL(printf("\tnot found!\n"));
3005 		return false;
3006 	}
3007 
3008 	DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));
3009 
3010 	// dequeue the spell mark
3011 	if (lastMark)
3012 		lastMark->next = spellMark->next;
3013 	else
3014 		fFirstSpellMark = spellMark->next;
3015 
3016 	if (spellMark->start < start)
3017 		start = spellMark->start;
3018 	if (spellMark->end > end)
3019 		end = spellMark->end;
3020 
3021 	// reset old text run array
3022 	SetRunArray(start, end, spellMark->style);
3023 
3024 	free(spellMark->style);
3025 	free(spellMark);
3026 
3027 	return true;
3028 }
3029 
3030 
3031 void
3032 TTextView::RemoveSpellMarks()
3033 {
3034 	spell_mark *spellMark, *nextMark;
3035 
3036 	for (spellMark = fFirstSpellMark; spellMark; spellMark = nextMark) {
3037 		nextMark = spellMark->next;
3038 
3039 		// reset old text run array
3040 		SetRunArray(spellMark->start, spellMark->end, spellMark->style);
3041 
3042 		free(spellMark->style);
3043 		free(spellMark);
3044 	}
3045 
3046 	fFirstSpellMark = NULL;
3047 }
3048 
3049 
3050 void
3051 TTextView::EnableSpellCheck(bool enable)
3052 {
3053 	if (fSpellCheck == enable)
3054 		return;
3055 
3056 	fSpellCheck = enable;
3057 	int32 textLength = TextLength();
3058 	if (fSpellCheck) {
3059 		// work-around for a bug in the BTextView class
3060 		// which causes lots of flicker
3061 		int32 start, end;
3062 		GetSelection(&start, &end);
3063 		if (start != end)
3064 			Select(start, start);
3065 
3066 		CheckSpelling(0, textLength);
3067 
3068 		if (start != end)
3069 			Select(start, end);
3070 	}
3071 	else
3072 		RemoveSpellMarks();
3073 }
3074 
3075 
3076 void
3077 TTextView::WindowActivated(bool flag)
3078 {
3079 	if (!flag) {
3080 		// WindowActivated(false) は、IM も Inactive になり、そのまま確定される。
3081 		// しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED)
3082 		// を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。
3083 		// OpenBeOSで修正されることを願って暫定処置としている。
3084 		fInputMethodUndoState.active = false;
3085 		// fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら(確定)正規のバッファへ追加
3086 		if (fInputMethodUndoBuffer.CountItems() > 0) {
3087 			KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1);
3088 			if (item->History == K_INSERTED) {
3089 				fUndoBuffer.MakeNewUndoItem();
3090 				fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset,
3091 					item->History, item->CursorPos);
3092 				fUndoBuffer.MakeNewUndoItem();
3093 			}
3094 			fInputMethodUndoBuffer.MakeEmpty();
3095 		}
3096 	}
3097 	BTextView::WindowActivated(flag);
3098 }
3099 
3100 
3101 void
3102 TTextView::AddQuote(int32 start, int32 finish)
3103 {
3104 	BRect rect = Bounds();
3105 
3106 	int32 lineStart;
3107 	GoToLine(CurrentLine());
3108 	GetSelection(&lineStart, &lineStart);
3109 	lineStart = LineStart(lineStart);
3110 
3111 	// make sure that we're changing the whole last line, too
3112 	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
3113 	{
3114 		const char *text = Text();
3115 		while (text[lineEnd] && text[lineEnd] != '\n')
3116 			lineEnd++;
3117 	}
3118 	Select(lineStart, lineEnd);
3119 
3120 	int32 textLength = lineEnd - lineStart;
3121 	char *text = (char *)malloc(textLength + 1);
3122 	if (text == NULL)
3123 		return;
3124 
3125 	GetText(lineStart, textLength, text);
3126 
3127 	int32 quoteLength = strlen(QUOTE);
3128 	int32 targetLength = 0;
3129 	char *target = NULL;
3130 	int32 lastLine = 0;
3131 
3132 	for (int32 index = 0; index < textLength; index++) {
3133 		if (text[index] == '\n' || index == textLength - 1) {
3134 			// add quote to this line
3135 			int32 lineLength = index - lastLine + 1;
3136 
3137 			target = (char *)realloc(target, targetLength + lineLength + quoteLength);
3138 			if (target == NULL) {
3139 				// free the old buffer?
3140 				free(text);
3141 				return;
3142 			}
3143 
3144 			// copy the quote sign
3145 			memcpy(&target[targetLength], QUOTE, quoteLength);
3146 			targetLength += quoteLength;
3147 
3148 			// copy the rest of the line
3149 			memcpy(&target[targetLength], &text[lastLine], lineLength);
3150 			targetLength += lineLength;
3151 
3152 			lastLine = index + 1;
3153 		}
3154 	}
3155 
3156 	// replace with quoted text
3157 	free(text);
3158 	Delete();
3159 
3160 	if (fColoredQuotes) {
3161 		const BFont *font = Font();
3162 		TextRunArray style(targetLength / 8 + 8);
3163 
3164 		FillInQuoteTextRuns(NULL, NULL, target, targetLength, font,
3165 			&style.Array(), style.MaxEntries());
3166 		Insert(target, targetLength, &style.Array());
3167 	} else
3168 		Insert(target, targetLength);
3169 
3170 	free(target);
3171 
3172 	// redo the old selection (compute the new start if necessary)
3173 	Select(start + quoteLength, finish + (targetLength - textLength));
3174 
3175 	ScrollTo(rect.LeftTop());
3176 }
3177 
3178 
3179 void
3180 TTextView::RemoveQuote(int32 start, int32 finish)
3181 {
3182 	BRect rect = Bounds();
3183 
3184 	GoToLine(CurrentLine());
3185 	int32 lineStart;
3186 	GetSelection(&lineStart, &lineStart);
3187 	lineStart = LineStart(lineStart);
3188 
3189 	// make sure that we're changing the whole last line, too
3190 	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
3191 	const char *text = Text();
3192 	while (text[lineEnd] && text[lineEnd] != '\n')
3193 		lineEnd++;
3194 
3195 	Select(lineStart, lineEnd);
3196 
3197 	int32 length = lineEnd - lineStart;
3198 	char *target = (char *)malloc(length + 1);
3199 	if (target == NULL)
3200 		return;
3201 
3202 	int32 quoteLength = strlen(QUOTE);
3203 	int32 removed = 0;
3204 	text += lineStart;
3205 
3206 	for (int32 index = 0; index < length;) {
3207 		// find out the length of the current line
3208 		int32 lineLength = 0;
3209 		while (index + lineLength < length && text[lineLength] != '\n')
3210 			lineLength++;
3211 
3212 		// include the newline to be part of this line
3213 		if (text[lineLength] == '\n' && index + lineLength + 1 < length)
3214 			lineLength++;
3215 
3216 		if (!strncmp(text, QUOTE, quoteLength)) {
3217 			// remove quote
3218 			length -= quoteLength;
3219 			removed += quoteLength;
3220 
3221 			lineLength -= quoteLength;
3222 			text += quoteLength;
3223 		}
3224 
3225 		if (lineLength == 0) {
3226 			target[index] = '\0';
3227 			break;
3228 		}
3229 
3230 		memcpy(&target[index], text, lineLength);
3231 
3232 		text += lineLength;
3233 		index += lineLength;
3234 	}
3235 
3236 	if (removed) {
3237 		Delete();
3238 
3239 		if (fColoredQuotes) {
3240 			const BFont *font = Font();
3241 			TextRunArray style(length / 8 + 8);
3242 
3243 			FillInQuoteTextRuns(NULL, NULL, target, length, font,
3244 				&style.Array(), style.MaxEntries());
3245 			Insert(target, length, &style.Array());
3246 		} else
3247 			Insert(target, length);
3248 
3249 		// redo old selection
3250 		bool noSelection = start == finish;
3251 
3252 		if (start > lineStart + quoteLength)
3253 			start -= quoteLength;
3254 		else
3255 			start = lineStart;
3256 
3257 		if (noSelection)
3258 			finish = start;
3259 		else
3260 			finish -= removed;
3261 	}
3262 
3263 	free(target);
3264 
3265 	Select(start, finish);
3266 	ScrollTo(rect.LeftTop());
3267 }
3268 
3269 
3270 int32
3271 TTextView::LineStart(int32 offset)
3272 {
3273 	if (offset <= 0)
3274 		return 0;
3275 
3276 	while (offset > 0) {
3277 		offset = PreviousByte(offset);
3278 		if (ByteAt(offset) == B_ENTER)
3279 			return offset + 1;
3280 	}
3281 
3282 	return offset;
3283 }
3284 
3285 
3286 int32
3287 TTextView::PreviousByte(int32 offset) const
3288 {
3289 	if (offset <= 0)
3290 		return 0;
3291 
3292 	int32 count = 6;
3293 
3294 	for (--offset; offset > 0 && count; --offset, --count) {
3295 		if ((ByteAt(offset) & 0xC0) != 0x80)
3296 			break;
3297 	}
3298 
3299 	return count ? offset : 0;
3300 }
3301 
3302 
3303 void
3304 TTextView::Undo(BClipboard */*clipboard*/)
3305 {
3306 	if (fInputMethodUndoState.active)
3307 		return;
3308 
3309 	int32 length, offset, cursorPos;
3310 	undo_type history;
3311 	char *text;
3312 	status_t status;
3313 
3314 	status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
3315 	if (status == B_OK) {
3316 		fUndoBuffer.Off();
3317 
3318 		switch (history) {
3319 			case K_INSERTED:
3320 				BTextView::Delete(offset, offset + length);
3321 				Select(offset, offset);
3322 				break;
3323 
3324 			case K_DELETED:
3325 				BTextView::Insert(offset, text, length);
3326 				Select(offset, offset + length);
3327 				break;
3328 
3329 			case K_REPLACED:
3330 				BTextView::Delete(offset, offset + length);
3331 				status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
3332 				if (status == B_OK && history == K_DELETED) {
3333 					BTextView::Insert(offset, text, length);
3334 					Select(offset, offset + length);
3335 				} else {
3336 					::beep();
3337 					BAlert* alert = new BAlert("",
3338 						B_TRANSLATE("Inconsistency occurred in the undo/redo "
3339 							"buffer."),	B_TRANSLATE("OK"));
3340 					alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
3341 					alert->Go();
3342 				}
3343 				break;
3344 		}
3345 		ScrollToSelection();
3346 		ContentChanged();
3347 		fUndoBuffer.On();
3348 	}
3349 }
3350 
3351 
3352 void
3353 TTextView::Redo()
3354 {
3355 	if (fInputMethodUndoState.active)
3356 		return;
3357 
3358 	int32 length, offset, cursorPos;
3359 	undo_type history;
3360 	char *text;
3361 	status_t status;
3362 	bool replaced;
3363 
3364 	status = fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
3365 	if (status == B_OK) {
3366 		fUndoBuffer.Off();
3367 
3368 		switch (history) {
3369 			case K_INSERTED:
3370 				BTextView::Insert(offset, text, length);
3371 				Select(offset, offset + length);
3372 				break;
3373 
3374 			case K_DELETED:
3375 				BTextView::Delete(offset, offset + length);
3376 				if (replaced) {
3377 					fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
3378 					BTextView::Insert(offset, text, length);
3379 				}
3380 				Select(offset, offset + length);
3381 				break;
3382 
3383 			case K_REPLACED:
3384 				::beep();
3385 				BAlert* alert = new BAlert("",
3386 					B_TRANSLATE("Inconsistency occurred in the undo/redo "
3387 						"buffer."),	B_TRANSLATE("OK"));
3388 				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
3389 				alert->Go();
3390 				break;
3391 		}
3392 		ScrollToSelection();
3393 		ContentChanged();
3394 		fUndoBuffer.On();
3395 	}
3396 }
3397