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