xref: /haiku/src/apps/mail/Content.cpp (revision 5b189b0e1e2f51f367bfcb126b2f00a3702f352d)
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_QUOTE:
697 		{
698 			int32 start, finish;
699 			fTextView->GetSelection(&start, &finish);
700 			fTextView->AddQuote(start, finish);
701 			break;
702 		}
703 		case M_REMOVE_QUOTE:
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 						fYankBuffer = (char *)realloc(fYankBuffer,
1043 									 strlen(fYankBuffer) + (end - start) + 1);
1044 						GetText(start, end - start,
1045 							    &fYankBuffer[strlen(fYankBuffer)]);
1046 					} else {
1047 						fYankBuffer = (char *)malloc(end - start + 1);
1048 						GetText(start, end - start, fYankBuffer);
1049 					}
1050 					Delete();
1051 					ScrollToSelection();
1052 				}
1053 				break;
1054 			}
1055 
1056 			BTextView::KeyDown(key, count);
1057 			break;
1058 
1059 		case 0x10:						// ^p goto previous line
1060 			if (IsSelectable()) {
1061 				raw = B_UP_ARROW;
1062 				BTextView::KeyDown(&raw, 1);
1063 			}
1064 			break;
1065 
1066 		case 0x19:						// ^y yank text
1067 			if (IsEditable() && fYankBuffer) {
1068 				Delete();
1069 				Insert(fYankBuffer);
1070 				ScrollToSelection();
1071 			}
1072 			break;
1073 
1074 		default:
1075 			BTextView::KeyDown(key, count);
1076 	}
1077 }
1078 
1079 
1080 void
1081 TTextView::MakeFocus(bool focus)
1082 {
1083 	if (!focus) {
1084 		// ToDo: can someone please translate this? Otherwise I will remove it - axeld.
1085 		// MakeFocus(false) は、IM も Inactive になり、そのまま確定される。
1086 		// しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED)
1087 		// を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。
1088 		fInputMethodUndoState.active = false;
1089 		// fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら(確定)正規のバッファへ追加
1090 		if (fInputMethodUndoBuffer.CountItems() > 0) {
1091 			KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1);
1092 			if (item->History == K_INSERTED) {
1093 				fUndoBuffer.MakeNewUndoItem();
1094 				fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset, item->History, item->CursorPos);
1095 				fUndoBuffer.MakeNewUndoItem();
1096 			}
1097 			fInputMethodUndoBuffer.MakeEmpty();
1098 		}
1099 	}
1100 	BTextView::MakeFocus(focus);
1101 
1102 	fParent->Focus(focus);
1103 }
1104 
1105 
1106 void
1107 TTextView::MessageReceived(BMessage *msg)
1108 {
1109 	switch (msg->what) {
1110 		case B_SIMPLE_DATA:
1111 		{
1112 			if (fIncoming)
1113 				break;
1114 
1115 			BMessage message(REFS_RECEIVED);
1116 			bool isEnclosure = false;
1117 			bool inserted = false;
1118 
1119 			off_t len = 0;
1120 			int32 end;
1121 			int32 start;
1122 
1123 			int32 index = 0;
1124 			entry_ref ref;
1125 			while (msg->FindRef("refs", index++, &ref) == B_OK) {
1126 				BFile file(&ref, B_READ_ONLY);
1127 				if (file.InitCheck() == B_OK) {
1128 					BNodeInfo node(&file);
1129 					char type[B_FILE_NAME_LENGTH];
1130 					node.GetType(type);
1131 
1132 					off_t size = 0;
1133 					file.GetSize(&size);
1134 
1135 					if (!strncasecmp(type, "text/", 5) && size > 0) {
1136 						len += size;
1137 						char *text = (char *)malloc(size);
1138 						if (text == NULL) {
1139 							puts("no memory!");
1140 							return;
1141 						}
1142 						if (file.Read(text, size) < B_OK) {
1143 							puts("could not read from file");
1144 							free(text);
1145 							continue;
1146 						}
1147 						if (!inserted) {
1148 							GetSelection(&start, &end);
1149 							Delete();
1150 							inserted = true;
1151 						}
1152 
1153 						int32 offset = 0;
1154 						for (int32 loop = 0; loop < size; loop++) {
1155 							if (text[loop] == '\n') {
1156 								Insert(&text[offset], loop - offset + 1);
1157 								offset = loop + 1;
1158 							} else if (text[loop] == '\r') {
1159 								text[loop] = '\n';
1160 								Insert(&text[offset], loop - offset + 1);
1161 								if ((loop + 1 < size)
1162 										&& (text[loop + 1] == '\n'))
1163 									loop++;
1164 								offset = loop + 1;
1165 							}
1166 						}
1167 						free(text);
1168 					} else {
1169 						isEnclosure = true;
1170 						message.AddRef("refs", &ref);
1171 					}
1172 				}
1173 			}
1174 
1175 			if (index == 1) {
1176 				// message doesn't contain any refs - maybe the parent class likes it
1177 				BTextView::MessageReceived(msg);
1178 				break;
1179 			}
1180 
1181 			if (inserted)
1182 				Select(start, start + len);
1183 			if (isEnclosure)
1184 				Window()->PostMessage(&message, Window());
1185 			break;
1186 		}
1187 
1188 		case M_HEADER:
1189 			msg->FindBool("header", &fHeader);
1190 			SetText(NULL);
1191 			LoadMessage(fMail, false, NULL);
1192 			break;
1193 
1194 		case M_RAW:
1195 			StopLoad();
1196 
1197 			msg->FindBool("raw", &fRaw);
1198 			SetText(NULL);
1199 			LoadMessage(fMail, false, NULL);
1200 			break;
1201 
1202 		case M_SELECT:
1203 			if (IsSelectable())
1204 				Select(0, TextLength());
1205 			break;
1206 
1207 		case M_SAVE:
1208 			Save(msg);
1209 			break;
1210 
1211 		case B_NODE_MONITOR:
1212 		{
1213 			int32 opcode;
1214 			if (msg->FindInt32("opcode", &opcode) == B_NO_ERROR) {
1215 				dev_t device;
1216 				if (msg->FindInt32("device", &device) < B_OK)
1217 					break;
1218 				ino_t inode;
1219 				if (msg->FindInt64("node", &inode) < B_OK)
1220 					break;
1221 
1222 				hyper_text *enclosure;
1223 				for (int32 index = 0;
1224 						(enclosure = (hyper_text *)fEnclosures->ItemAt(index++)) != NULL;) {
1225 					if (device == enclosure->node.device
1226 						&& inode == enclosure->node.node) {
1227 						if (opcode == B_ENTRY_REMOVED) {
1228 							enclosure->saved = false;
1229 							enclosure->have_ref = false;
1230 						} else if (opcode == B_ENTRY_MOVED) {
1231 							enclosure->ref.device = device;
1232 							msg->FindInt64("to directory", &enclosure->ref.directory);
1233 
1234 							const char *name;
1235 							msg->FindString("name", &name);
1236 							enclosure->ref.set_name(name);
1237 						}
1238 						break;
1239 					}
1240 				}
1241 			}
1242 			break;
1243 		}
1244 
1245 		//
1246 		// Tracker has responded to a BMessage that was dragged out of
1247 		// this email message.  It has created a file for us, we just have to
1248 		// put the stuff in it.
1249 		//
1250 		case B_COPY_TARGET:
1251 		{
1252 			BMessage data;
1253 			if (msg->FindMessage("be:originator-data", &data) == B_OK) {
1254 				entry_ref directory;
1255 				const char *name;
1256 				hyper_text *enclosure;
1257 
1258 				if (data.FindPointer("enclosure", (void **)&enclosure) == B_OK
1259 					&& msg->FindString("name", &name) == B_OK
1260 					&& msg->FindRef("directory", &directory) == B_OK) {
1261 					switch (enclosure->type) {
1262 						case TYPE_ENCLOSURE:
1263 						case TYPE_BE_ENCLOSURE:
1264 						{
1265 							//
1266 							//	Enclosure.  Decode the data and write it out.
1267 							//
1268 							BMessage saveMsg(M_SAVE);
1269 							saveMsg.AddString("name", name);
1270 							saveMsg.AddRef("directory", &directory);
1271 							saveMsg.AddPointer("enclosure", enclosure);
1272 							Save(&saveMsg, false);
1273 							break;
1274 						}
1275 
1276 						case TYPE_URL:
1277 						{
1278 							const char *replyType;
1279 							if (msg->FindString("be:filetypes", &replyType) != B_OK)
1280 								// drag recipient didn't ask for any specific type,
1281 								// create a bookmark file as default
1282 								replyType = "application/x-vnd.Be-bookmark";
1283 
1284 							BDirectory dir(&directory);
1285 							BFile file(&dir, name, B_READ_WRITE);
1286 							if (file.InitCheck() == B_OK) {
1287 								if (strcmp(replyType, "application/x-vnd.Be-bookmark") == 0) {
1288 									// we got a request to create a bookmark, stuff
1289 									// it with the url attribute
1290 									file.WriteAttr("META:url", B_STRING_TYPE, 0,
1291 													enclosure->name, strlen(enclosure->name) + 1);
1292 								} else if (strcasecmp(replyType, "text/plain") == 0) {
1293 									// create a plain text file, stuff it with
1294 									// the url as text
1295 									file.Write(enclosure->name, strlen(enclosure->name));
1296 								}
1297 
1298 								BNodeInfo fileInfo(&file);
1299 								fileInfo.SetType(replyType);
1300 							}
1301 							break;
1302 						}
1303 
1304 						case TYPE_MAILTO:
1305 						{
1306 							//
1307 							//	Add some attributes to the already created
1308 							//	person file.  Strip out the 'mailto:' if
1309 							//  possible.
1310 							//
1311 							char *addrStart = enclosure->name;
1312 							while (true) {
1313 								if (*addrStart == ':') {
1314 									addrStart++;
1315 									break;
1316 								}
1317 
1318 								if (*addrStart == '\0') {
1319 									addrStart = enclosure->name;
1320 									break;
1321 								}
1322 
1323 								addrStart++;
1324 							}
1325 
1326 							const char *replyType;
1327 							if (msg->FindString("be:filetypes", &replyType) != B_OK)
1328 								// drag recipient didn't ask for any specific type,
1329 								// create a bookmark file as default
1330 								replyType = "application/x-vnd.Be-bookmark";
1331 
1332 							BDirectory dir(&directory);
1333 							BFile file(&dir, name, B_READ_WRITE);
1334 							if (file.InitCheck() == B_OK) {
1335 								if (!strcmp(replyType, "application/x-person")) {
1336 									// we got a request to create a bookmark, stuff
1337 									// it with the address attribute
1338 									file.WriteAttr("META:email", B_STRING_TYPE, 0,
1339 									  addrStart, strlen(enclosure->name) + 1);
1340 								} else if (!strcasecmp(replyType, "text/plain")) {
1341 									// create a plain text file, stuff it with the
1342 									// email as text
1343 									file.Write(addrStart, strlen(addrStart));
1344 								}
1345 
1346 								BNodeInfo fileInfo(&file);
1347 								fileInfo.SetType(replyType);
1348 							}
1349 							break;
1350 						}
1351 					}
1352 				} else {
1353 					//
1354 					// Assume this is handled by BTextView...
1355 					// (Probably drag clipping.)
1356 					//
1357 					BTextView::MessageReceived(msg);
1358 				}
1359 			}
1360 			break;
1361 		}
1362 
1363 		case B_INPUT_METHOD_EVENT:
1364 		{
1365 			int32 im_op;
1366 			if (msg->FindInt32("be:opcode", &im_op) == B_OK){
1367 				switch (im_op) {
1368 					case B_INPUT_METHOD_STARTED:
1369 						fInputMethodUndoState.replace = true;
1370 						fInputMethodUndoState.active = true;
1371 						break;
1372 					case B_INPUT_METHOD_STOPPED:
1373 						fInputMethodUndoState.active = false;
1374 						if (fInputMethodUndoBuffer.CountItems() > 0) {
1375 							KUndoItem *undo = fInputMethodUndoBuffer.ItemAt(
1376 								fInputMethodUndoBuffer.CountItems() - 1);
1377 							if (undo->History == K_INSERTED){
1378 								fUndoBuffer.MakeNewUndoItem();
1379 								fUndoBuffer.AddUndo(undo->RedoText, undo->Length,
1380 									undo->Offset, undo->History, undo->CursorPos);
1381 								fUndoBuffer.MakeNewUndoItem();
1382 							}
1383 							fInputMethodUndoBuffer.MakeEmpty();
1384 						}
1385 						break;
1386 					case B_INPUT_METHOD_CHANGED:
1387 						fInputMethodUndoState.active = true;
1388 						break;
1389 					case B_INPUT_METHOD_LOCATION_REQUEST:
1390 						fInputMethodUndoState.active = true;
1391 						break;
1392 				}
1393 			}
1394 			BTextView::MessageReceived(msg);
1395 			break;
1396 		}
1397 
1398 		case M_REDO:
1399 			Redo();
1400 			break;
1401 
1402 		default:
1403 			BTextView::MessageReceived(msg);
1404 	}
1405 }
1406 
1407 
1408 void
1409 TTextView::MouseDown(BPoint where)
1410 {
1411 	if (IsEditable()) {
1412 		BPoint point;
1413 		uint32 buttons;
1414 		GetMouse(&point, &buttons);
1415 		if (gDictCount && (buttons == B_SECONDARY_MOUSE_BUTTON)) {
1416 			int32 offset, start, end, length;
1417 			const char *text = Text();
1418 			offset = OffsetAt(where);
1419 			if (isalpha(text[offset])) {
1420 				length = TextLength();
1421 
1422 				//Find start and end of word
1423 				//FindSpellBoundry(length, offset, &start, &end);
1424 
1425 				char c;
1426 				bool isAlpha, isApost, isCap;
1427 				int32 first;
1428 
1429 				for (first = offset;
1430 					(first >= 0) && (((c = text[first]) == '\'') || isalpha(c));
1431 					first--) {}
1432 				isCap = isupper(text[++first]);
1433 
1434 				for (start = offset, c = text[start], isAlpha = isalpha(c), isApost = (c=='\'');
1435 					(start >= 0) && (isAlpha || (isApost
1436 					&& (((c = text[start+1]) != 's') || !isCap) && isalpha(c)
1437 					&& isalpha(text[start-1])));
1438 					start--, c = text[start], isAlpha = isalpha(c), isApost = (c == '\'')) {}
1439 				start++;
1440 
1441 				for (end = offset, c = text[end], isAlpha = isalpha(c), isApost = (c == '\'');
1442 					(end < length) && (isAlpha || (isApost
1443 					&& (((c = text[end + 1]) != 's') || !isCap) && isalpha(c)));
1444 					end++, c = text[end], isAlpha = isalpha(c), isApost = (c == '\'')) {}
1445 
1446 				length = end - start;
1447 				BString srcWord;
1448 				srcWord.SetTo(text + start, length);
1449 
1450 				bool		foundWord = false;
1451 				BList 		matches;
1452 				BString 	*string;
1453 
1454 				BMenuItem *menuItem;
1455 				BPopUpMenu menu("Words", false, false);
1456 
1457 				for (int32 i = 0; i < gDictCount; i++)
1458 					gWords[i]->FindBestMatches(&matches,
1459 						srcWord.String());
1460 
1461 				if (matches.CountItems()) {
1462 					sort_word_list(&matches, srcWord.String());
1463 					for (int32 i = 0; (string = (BString *)matches.ItemAt(i)) != NULL; i++) {
1464 						menu.AddItem((menuItem = new BMenuItem(string->String(), NULL)));
1465 						if (!strcasecmp(string->String(), srcWord.String())) {
1466 							menuItem->SetEnabled(false);
1467 							foundWord = true;
1468 						}
1469 						delete string;
1470 					}
1471 				} else {
1472 					menuItem = new BMenuItem(B_TRANSLATE("No matches"), NULL);
1473 					menuItem->SetEnabled(false);
1474 					menu.AddItem(menuItem);
1475 				}
1476 
1477 				BMenuItem *addItem = NULL;
1478 				if (!foundWord && gUserDict >= 0) {
1479 					menu.AddSeparatorItem();
1480 					addItem = new BMenuItem(B_TRANSLATE("Add"), NULL);
1481 					menu.AddItem(addItem);
1482 				}
1483 
1484 				point = ConvertToScreen(where);
1485 				if ((menuItem = menu.Go(point, false, false)) != NULL) {
1486 					if (menuItem == addItem) {
1487 						BString newItem(srcWord.String());
1488 						newItem << "\n";
1489 						gWords[gUserDict]->InitIndex();
1490 						gExactWords[gUserDict]->InitIndex();
1491 						gUserDictFile->Write(newItem.String(), newItem.Length());
1492 						gWords[gUserDict]->BuildIndex();
1493 						gExactWords[gUserDict]->BuildIndex();
1494 
1495 						if (fSpellCheck)
1496 							CheckSpelling(0, TextLength());
1497 					} else {
1498 						int32 len = strlen(menuItem->Label());
1499 						Select(start, start);
1500 						Delete(start, end);
1501 						Insert(start, menuItem->Label(), len);
1502 						Select(start+len, start+len);
1503 					}
1504 				}
1505 			}
1506 			return;
1507 		} else if (fSpellCheck && IsEditable()) {
1508 			int32 start, end;
1509 
1510 			GetSelection(&start, &end);
1511 			FindSpellBoundry(1, start, &start, &end);
1512 			CheckSpelling(start, end);
1513 		}
1514 	} else {
1515 		// is not editable, look for enclosures/links
1516 
1517 		int32 clickOffset = OffsetAt(where);
1518 		int32 items = fEnclosures->CountItems();
1519 		for (int32 loop = 0; loop < items; loop++) {
1520 			hyper_text *enclosure = (hyper_text*) fEnclosures->ItemAt(loop);
1521 			if (clickOffset < enclosure->text_start || clickOffset >= enclosure->text_end)
1522 				continue;
1523 
1524 			//
1525 			// The user is clicking on this attachment
1526 			//
1527 
1528 			int32 start;
1529 			int32 finish;
1530 			Select(enclosure->text_start, enclosure->text_end);
1531 			GetSelection(&start, &finish);
1532 			Window()->UpdateIfNeeded();
1533 
1534 			bool drag = false;
1535 			bool held = false;
1536 			uint32 buttons = 0;
1537 			if (Window()->CurrentMessage()) {
1538 				Window()->CurrentMessage()->FindInt32("buttons",
1539 				  (int32 *) &buttons);
1540 			}
1541 
1542 			//
1543 			// If this is the primary button, wait to see if the user is going
1544 			// to single click, hold, or drag.
1545 			//
1546 			if (buttons != B_SECONDARY_MOUSE_BUTTON) {
1547 				BPoint point = where;
1548 				bigtime_t popupDelay;
1549 				get_click_speed(&popupDelay);
1550 				popupDelay *= 2;
1551 				popupDelay += system_time();
1552 				while (buttons && abs((int)(point.x - where.x)) < 4
1553 					&& abs((int)(point.y - where.y)) < 4
1554 					&& system_time() < popupDelay) {
1555 					snooze(10000);
1556 					GetMouse(&point, &buttons);
1557 				}
1558 
1559 				if (system_time() < popupDelay) {
1560 					//
1561 					// The user either dragged this or released the button.
1562 					// check if it was dragged.
1563 					//
1564 					if (!(abs((int)(point.x - where.x)) < 4
1565 						&& abs((int)(point.y - where.y)) < 4) && buttons)
1566 						drag = true;
1567 				} else  {
1568 					//
1569 					//	The user held the button down.
1570 					//
1571 					held = true;
1572 				}
1573 			}
1574 
1575 			//
1576 			//	If the user has right clicked on this menu,
1577 			// 	or held the button down on it for a while,
1578 			//	pop up a context menu.
1579 			//
1580 			if (buttons == B_SECONDARY_MOUSE_BUTTON || held) {
1581 				//
1582 				// Right mouse click... Display a menu
1583 				//
1584 				BPoint point = where;
1585 				ConvertToScreen(&point);
1586 
1587 				BMenuItem *item;
1588 				if ((enclosure->type != TYPE_ENCLOSURE)
1589 					&& (enclosure->type != TYPE_BE_ENCLOSURE))
1590 					item = fLinkMenu->Go(point, true);
1591 				else
1592 					item = fEnclosureMenu->Go(point, true);
1593 
1594 				BMessage *msg;
1595 				if (item && (msg = item->Message()) != NULL) {
1596 					if (msg->what == M_SAVE) {
1597 						if (fPanel)
1598 							fPanel->SetEnclosure(enclosure);
1599 						else {
1600 							fPanel = new TSavePanel(enclosure, this);
1601 							fPanel->Window()->Show();
1602 						}
1603 					} else if (msg->what == M_COPY) {
1604 						// copy link location to clipboard
1605 
1606 						if (be_clipboard->Lock()) {
1607 							be_clipboard->Clear();
1608 
1609 							BMessage *clip;
1610 							if ((clip = be_clipboard->Data()) != NULL) {
1611 								clip->AddData("text/plain", B_MIME_TYPE,
1612 									enclosure->name, strlen(enclosure->name));
1613 								be_clipboard->Commit();
1614 							}
1615 							be_clipboard->Unlock();
1616 						}
1617 					} else
1618 						Open(enclosure);
1619 				}
1620 			} else {
1621 				//
1622 				// Left button.  If the user single clicks, open this link.
1623 				// Otherwise, initiate a drag.
1624 				//
1625 				if (drag) {
1626 					BMessage dragMessage(B_SIMPLE_DATA);
1627 					dragMessage.AddInt32("be:actions", B_COPY_TARGET);
1628 					dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1629 					switch (enclosure->type) {
1630 						case TYPE_BE_ENCLOSURE:
1631 						case TYPE_ENCLOSURE:
1632 							//
1633 							// Attachment.  The type is specified in the message.
1634 							//
1635 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1636 							dragMessage.AddString("be:filetypes",
1637 								enclosure->content_type ? enclosure->content_type : "");
1638 							dragMessage.AddString("be:clip_name", enclosure->name);
1639 							break;
1640 
1641 						case TYPE_URL:
1642 							//
1643 							// URL.  The user can drag it into the tracker to
1644 							// create a bookmark file.
1645 							//
1646 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1647 							dragMessage.AddString("be:filetypes",
1648 							  "application/x-vnd.Be-bookmark");
1649 							dragMessage.AddString("be:filetypes", "text/plain");
1650 							dragMessage.AddString("be:clip_name", "Bookmark");
1651 
1652 							dragMessage.AddString("be:url", enclosure->name);
1653 							break;
1654 
1655 						case TYPE_MAILTO:
1656 							//
1657 							// Mailto address.  The user can drag it into the
1658 							// tracker to create a people file.
1659 							//
1660 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1661 							dragMessage.AddString("be:filetypes",
1662 							  "application/x-person");
1663 							dragMessage.AddString("be:filetypes", "text/plain");
1664 							dragMessage.AddString("be:clip_name", "Person");
1665 
1666 							dragMessage.AddString("be:email", enclosure->name);
1667 							break;
1668 
1669 						default:
1670 							//
1671 							// Otherwise it doesn't have a type that I know how
1672 							// to save.  It won't have any types and if any
1673 							// program wants to accept it, more power to them.
1674 							// (tracker won't.)
1675 							//
1676 							dragMessage.AddString("be:clip_name", "Hyperlink");
1677 					}
1678 
1679 					BMessage data;
1680 					data.AddPointer("enclosure", enclosure);
1681 					dragMessage.AddMessage("be:originator-data", &data);
1682 
1683 					BRegion selectRegion;
1684 					GetTextRegion(start, finish, &selectRegion);
1685 					DragMessage(&dragMessage, selectRegion.Frame(), this);
1686 				} else {
1687 					//
1688 					//	User Single clicked on the attachment.  Open it.
1689 					//
1690 					Open(enclosure);
1691 				}
1692 			}
1693 			return;
1694 		}
1695 	}
1696 	BTextView::MouseDown(where);
1697 }
1698 
1699 
1700 void
1701 TTextView::MouseMoved(BPoint where, uint32 code, const BMessage *msg)
1702 {
1703 	int32 start = OffsetAt(where);
1704 
1705 	for (int32 loop = fEnclosures->CountItems(); loop-- > 0;) {
1706 		hyper_text *enclosure = (hyper_text *)fEnclosures->ItemAt(loop);
1707 		if ((start >= enclosure->text_start) && (start < enclosure->text_end)) {
1708 			if (!fCursor)
1709 				SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
1710 			fCursor = true;
1711 			return;
1712 		}
1713 	}
1714 
1715 	if (fCursor) {
1716 		SetViewCursor(B_CURSOR_I_BEAM);
1717 		fCursor = false;
1718 	}
1719 
1720 	BTextView::MouseMoved(where, code, msg);
1721 }
1722 
1723 
1724 void
1725 TTextView::ClearList()
1726 {
1727 	hyper_text *enclosure;
1728 	while ((enclosure = (hyper_text *)fEnclosures->FirstItem()) != NULL) {
1729 		fEnclosures->RemoveItem(enclosure);
1730 
1731 		if (enclosure->name)
1732 			free(enclosure->name);
1733 		if (enclosure->content_type)
1734 			free(enclosure->content_type);
1735 		if (enclosure->encoding)
1736 			free(enclosure->encoding);
1737 		if (enclosure->have_ref && !enclosure->saved) {
1738 			BEntry entry(&enclosure->ref);
1739 			entry.Remove();
1740 		}
1741 
1742 		watch_node(&enclosure->node, B_STOP_WATCHING, this);
1743 		free(enclosure);
1744 	}
1745 }
1746 
1747 
1748 void
1749 TTextView::LoadMessage(BEmailMessage *mail, bool quoteIt, const char *text)
1750 {
1751 	StopLoad();
1752 
1753 	fMail = mail;
1754 
1755 	ClearList();
1756 
1757 	MakeSelectable(true);
1758 	MakeEditable(false);
1759 	if (text)
1760 		Insert(text, strlen(text));
1761 
1762 	//attr_info attrInfo;
1763 	TTextView::Reader *reader = new TTextView::Reader(fHeader, fRaw, quoteIt, fIncoming,
1764 				text != NULL, true,
1765 				// I removed the following, because I absolutely can't imagine why it's
1766 				// there (the mail kit should be able to deal with non-compliant mails)
1767 				// -- axeld.
1768 				// fFile->GetAttrInfo(B_MAIL_ATTR_MIME, &attrInfo) == B_OK,
1769 				this, mail, fEnclosures, fStopSem);
1770 
1771 	resume_thread(fThread = spawn_thread(Reader::Run, "reader", B_NORMAL_PRIORITY, reader));
1772 }
1773 
1774 
1775 void
1776 TTextView::Open(hyper_text *enclosure)
1777 {
1778 	switch (enclosure->type) {
1779 		case TYPE_URL:
1780 		{
1781 			const struct {const char *urlType, *handler; } handlerTable[] = {
1782 				{"http",	B_URL_HTTP},
1783 				{"https",	B_URL_HTTPS},
1784 				{"ftp",		B_URL_FTP},
1785 				{"gopher",	B_URL_GOPHER},
1786 				{"mailto",	B_URL_MAILTO},
1787 				{"news",	B_URL_NEWS},
1788 				{"nntp",	B_URL_NNTP},
1789 				{"telnet",	B_URL_TELNET},
1790 				{"rlogin",	B_URL_RLOGIN},
1791 				{"tn3270",	B_URL_TN3270},
1792 				{"wais",	B_URL_WAIS},
1793 				{"file",	B_URL_FILE},
1794 				{NULL,		NULL}
1795 			};
1796 			const char *handlerToLaunch = NULL;
1797 
1798 			const char *colonPos = strchr(enclosure->name, ':');
1799 			if (colonPos) {
1800 				int urlTypeLength = colonPos - enclosure->name;
1801 
1802 				for (int32 index = 0; handlerTable[index].urlType; index++) {
1803 					if (!strncasecmp(enclosure->name,
1804 							handlerTable[index].urlType, urlTypeLength)) {
1805 						handlerToLaunch = handlerTable[index].handler;
1806 						break;
1807 					}
1808 				}
1809 			}
1810 			if (handlerToLaunch) {
1811 				entry_ref appRef;
1812 				if (be_roster->FindApp(handlerToLaunch, &appRef) != B_OK)
1813 					handlerToLaunch = NULL;
1814 			}
1815 			if (!handlerToLaunch)
1816 				handlerToLaunch = "application/x-vnd.Be-Bookmark";
1817 
1818 			status_t result = be_roster->Launch(handlerToLaunch, 1, &enclosure->name);
1819 			if (result != B_NO_ERROR && result != B_ALREADY_RUNNING) {
1820 				beep();
1821 				BAlert* alert = new BAlert("",
1822 					B_TRANSLATE("There is no installed handler for "
1823 						"URL links."), B_TRANSLATE("Sorry"));
1824 				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
1825 				alert->Go();
1826 			}
1827 			break;
1828 		}
1829 
1830 		case TYPE_MAILTO:
1831 			if (be_roster->Launch(B_MAIL_TYPE, 1, &enclosure->name) < B_OK) {
1832 				char *argv[] = {(char *)"Mail", enclosure->name};
1833 				be_app->ArgvReceived(2, argv);
1834 			}
1835 			break;
1836 
1837 		case TYPE_ENCLOSURE:
1838 		case TYPE_BE_ENCLOSURE:
1839 			if (!enclosure->have_ref) {
1840 				BPath path;
1841 				if (find_directory(B_SYSTEM_TEMP_DIRECTORY, &path) == B_NO_ERROR) {
1842 					BDirectory dir(path.Path());
1843 					if (dir.InitCheck() == B_NO_ERROR) {
1844 						char name[B_FILE_NAME_LENGTH];
1845 						char baseName[B_FILE_NAME_LENGTH];
1846 						strcpy(baseName, enclosure->name ? enclosure->name : "enclosure");
1847 						strcpy(name, baseName);
1848 						for (int32 index = 0; dir.Contains(name); index++)
1849 							sprintf(name, "%s_%" B_PRId32, baseName, index);
1850 
1851 						BEntry entry(path.Path());
1852 						entry_ref ref;
1853 						entry.GetRef(&ref);
1854 
1855 						BMessage save(M_SAVE);
1856 						save.AddRef("directory", &ref);
1857 						save.AddString("name", name);
1858 						save.AddPointer("enclosure", enclosure);
1859 						if (Save(&save) != B_NO_ERROR)
1860 							break;
1861 						enclosure->saved = false;
1862 					}
1863 				}
1864 			}
1865 
1866 			BMessenger tracker("application/x-vnd.Be-TRAK");
1867 			if (tracker.IsValid()) {
1868 				BMessage openMsg(B_REFS_RECEIVED);
1869 				openMsg.AddRef("refs", &enclosure->ref);
1870 				tracker.SendMessage(&openMsg);
1871 			}
1872 			break;
1873 	}
1874 }
1875 
1876 
1877 status_t
1878 TTextView::Save(BMessage *msg, bool makeNewFile)
1879 {
1880 	const char		*name;
1881 	entry_ref		ref;
1882 	BFile			file;
1883 	BPath			path;
1884 	hyper_text		*enclosure;
1885 	status_t		result = B_NO_ERROR;
1886 	char 			entry_name[B_FILE_NAME_LENGTH];
1887 
1888 	msg->FindString("name", &name);
1889 	msg->FindRef("directory", &ref);
1890 	msg->FindPointer("enclosure", (void **)&enclosure);
1891 
1892 	BDirectory dir;
1893 	dir.SetTo(&ref);
1894 	result = dir.InitCheck();
1895 
1896 	if (result == B_OK) {
1897 		if (makeNewFile) {
1898 			//
1899 			// Search for the file and delete it if it already exists.
1900 			// (It may not, that's ok.)
1901 			//
1902 			BEntry entry;
1903 			if (dir.FindEntry(name, &entry) == B_NO_ERROR)
1904 				entry.Remove();
1905 
1906 			if ((enclosure->have_ref) && (!enclosure->saved)) {
1907 				entry.SetTo(&enclosure->ref);
1908 
1909 				//
1910 				// Added true arg and entry_name so MoveTo clobbers as
1911 				// before. This may not be the correct behaviour, but
1912 				// it's the preserved behaviour.
1913 				//
1914 				entry.GetName(entry_name);
1915 				result = entry.MoveTo(&dir, entry_name, true);
1916 				if (result == B_NO_ERROR) {
1917 					entry.Rename(name);
1918 					entry.GetRef(&enclosure->ref);
1919 					entry.GetNodeRef(&enclosure->node);
1920 					enclosure->saved = true;
1921 					return result;
1922 				}
1923 			}
1924 
1925 			if (result == B_NO_ERROR) {
1926 				result = dir.CreateFile(name, &file);
1927 				if (result == B_NO_ERROR && enclosure->content_type) {
1928 					char type[B_MIME_TYPE_LENGTH];
1929 
1930 					if (!strcasecmp(enclosure->content_type, "message/rfc822"))
1931 						strcpy(type, "text/x-email");
1932 					else if (!strcasecmp(enclosure->content_type, "message/delivery-status"))
1933 						strcpy(type, "text/plain");
1934 					else
1935 						strcpy(type, enclosure->content_type);
1936 
1937 					BNodeInfo info(&file);
1938 					info.SetType(type);
1939 				}
1940 			}
1941 		} else {
1942 			//
1943 			// 	This file was dragged into the tracker or desktop.  The file
1944 			//	already exists.
1945 			//
1946 			result = file.SetTo(&dir, name, B_WRITE_ONLY);
1947 		}
1948 	}
1949 
1950 	if (enclosure->component == NULL)
1951 		result = B_ERROR;
1952 
1953 	if (result == B_NO_ERROR) {
1954 		//
1955 		// Write the data
1956 		//
1957 		enclosure->component->GetDecodedData(&file);
1958 
1959 		BEntry entry;
1960 		dir.FindEntry(name, &entry);
1961 		entry.GetRef(&enclosure->ref);
1962 		enclosure->have_ref = true;
1963 		enclosure->saved = true;
1964 		entry.GetPath(&path);
1965 		update_mime_info(path.Path(), false, true,
1966 			!cistrcmp("application/octet-stream", enclosure->content_type ? enclosure->content_type : B_EMPTY_STRING));
1967 		entry.GetNodeRef(&enclosure->node);
1968 		watch_node(&enclosure->node, B_WATCH_NAME, this);
1969 	}
1970 
1971 	if (result != B_NO_ERROR) {
1972 		beep();
1973 		BAlert* alert = new BAlert("", B_TRANSLATE("An error occurred trying to save "
1974 				"the attachment."),	B_TRANSLATE("Sorry"));
1975 		alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
1976 		alert->Go();
1977 	}
1978 
1979 	return result;
1980 }
1981 
1982 
1983 void
1984 TTextView::StopLoad()
1985 {
1986 	Window()->Unlock();
1987 
1988 	thread_info	info;
1989 	if (fThread != 0 && get_thread_info(fThread, &info) == B_NO_ERROR) {
1990 		fStopLoading = true;
1991 		acquire_sem(fStopSem);
1992 		int32 result;
1993 		wait_for_thread(fThread, &result);
1994 		fThread = 0;
1995 		release_sem(fStopSem);
1996 		fStopLoading = false;
1997 	}
1998 
1999 	Window()->Lock();
2000 }
2001 
2002 
2003 bool
2004 TTextView::IsReaderThreadRunning()
2005 {
2006 	if (fThread == 0)
2007 		return false;
2008 
2009 	thread_info info;
2010 	for (int i = 5; i > 0; i--, usleep(100000))
2011 		if (get_thread_info(fThread, &info) != B_OK)
2012 			return false;
2013 	return true;
2014 }
2015 
2016 
2017 void
2018 TTextView::AddAsContent(BEmailMessage *mail, bool wrap, uint32 charset, mail_encoding encoding)
2019 {
2020 	if (mail == NULL)
2021 		return;
2022 
2023 	int32 textLength = TextLength();
2024 	const char *text = Text();
2025 
2026 	BTextMailComponent *body = mail->Body();
2027 	if (body == NULL) {
2028 		if (mail->SetBody(body = new BTextMailComponent()) < B_OK)
2029 			return;
2030 	}
2031 	body->SetEncoding(encoding, charset);
2032 
2033 	// Just add the text as a whole if we can, or ...
2034 	if (!wrap) {
2035 		body->AppendText(text);
2036 		return;
2037 	}
2038 
2039 	// ... do word wrapping.
2040 
2041 	BWindow	*window = Window();
2042 	char *saveText = strdup(text);
2043 	BRect saveTextRect = TextRect();
2044 
2045 	// do this before we start messing with the fonts
2046 	// the user will never know...
2047 	window->DisableUpdates();
2048 	Hide();
2049 	BScrollBar *vScroller = ScrollBar(B_VERTICAL);
2050 	BScrollBar *hScroller = ScrollBar(B_HORIZONTAL);
2051 	if (vScroller != NULL)
2052 		vScroller->SetTarget((BView *)NULL);
2053 	if (hScroller != NULL)
2054 		hScroller->SetTarget((BView *)NULL);
2055 
2056 	// Temporarily set the font to a fixed width font for line wrapping
2057 	// calculations.  If the font doesn't have as many of the symbols as
2058 	// the preferred font, go back to using the user's preferred font.
2059 
2060 	bool *boolArray;
2061 	int missingCharactersFixedWidth = 0;
2062 	int missingCharactersPreferredFont = 0;
2063 	int32 numberOfCharacters;
2064 
2065 	numberOfCharacters = BString(text).CountChars();
2066 	if (numberOfCharacters > 0
2067 		&& (boolArray = (bool *)malloc(sizeof(bool) * numberOfCharacters)) != NULL) {
2068 		memset(boolArray, 0, sizeof (bool) * numberOfCharacters);
2069 		be_fixed_font->GetHasGlyphs(text, numberOfCharacters, boolArray);
2070 		for (int i = 0; i < numberOfCharacters; i++) {
2071 			if (!boolArray[i])
2072 				missingCharactersFixedWidth += 1;
2073 		}
2074 
2075 		memset(boolArray, 0, sizeof (bool) * numberOfCharacters);
2076 		fFont.GetHasGlyphs(text, numberOfCharacters, boolArray);
2077 		for (int i = 0; i < numberOfCharacters; i++) {
2078 			if (!boolArray[i])
2079 				missingCharactersPreferredFont += 1;
2080 		}
2081 
2082 		free(boolArray);
2083 	}
2084 
2085 	if (missingCharactersFixedWidth > missingCharactersPreferredFont)
2086 		SetFontAndColor(0, textLength, &fFont);
2087 	else // All things being equal, the fixed font is better for wrapping.
2088 		SetFontAndColor(0, textLength, be_fixed_font);
2089 
2090 	// calculate a text rect that is 72 columns wide
2091 	BRect newTextRect = saveTextRect;
2092 	newTextRect.right = newTextRect.left + be_fixed_font->StringWidth("m") * 72;
2093 	SetTextRect(newTextRect);
2094 
2095 	// hard-wrap, based on TextView's soft-wrapping
2096 	int32 numLines = CountLines();
2097 	bool spaceMoved = false;
2098 	char *content = (char *)malloc(textLength + numLines * 72);
2099 		// more we'll ever need
2100 	if (content != NULL) {
2101 		int32 contentLength = 0;
2102 
2103 		int32 nextUrlAt = 0, nextUrlLength = 0;
2104 		BString textStr(text);
2105 		FindURL(text, 0, nextUrlAt, nextUrlLength, NULL);
2106 
2107 		for (int32 i = 0; i < numLines; i++) {
2108 			int32 startOffset = OffsetAt(i);
2109 			if (spaceMoved) {
2110 				startOffset++;
2111 				spaceMoved = false;
2112 			}
2113 			int32 endOffset = OffsetAt(i + 1);
2114 			int32 lineLength = endOffset - startOffset;
2115 
2116 			// don't break URLs into several parts
2117 			if (nextUrlAt >= startOffset && nextUrlAt < endOffset
2118 					&& (nextUrlAt + nextUrlLength) > endOffset) {
2119 				int32 pos = nextUrlAt + nextUrlLength;
2120 
2121 				// find first break character after the URL
2122 				for (; text[pos]; pos++) {
2123 					if (isalnum(text[pos]) || isspace(text[pos]))
2124 						break;
2125 				}
2126 				if (text[pos] && isspace(text[pos]) && text[pos] != '\n')
2127 					pos++;
2128 
2129 				endOffset += pos - endOffset;
2130 				lineLength = endOffset - startOffset;
2131 
2132 				// insert a newline (and the same number of quotes) after the
2133 				// URL to make sure the rest of the text is properly wrapped
2134 
2135 				char buffer[64];
2136 				if (text[pos] == '\n')
2137 					buffer[0] = '\0';
2138 				else
2139 					strcpy(buffer, "\n");
2140 
2141 				size_t quoteLength;
2142 				CopyQuotes(text + startOffset, lineLength, buffer + strlen(buffer), quoteLength);
2143 
2144 				Insert(pos, buffer, strlen(buffer));
2145 				numLines = CountLines();
2146 				text = Text();
2147 				i++;
2148 
2149 				textStr = BString(text);
2150 				FindURL(text, endOffset, nextUrlAt, nextUrlLength, NULL);
2151 			}
2152 			if (text[endOffset - 1] != ' '
2153 				&& text[endOffset - 1] != '\n'
2154 				&& text[endOffset] == ' ') {
2155 				// make sure spaces will be part of this line
2156 				endOffset++;
2157 				lineLength++;
2158 				spaceMoved = true;
2159 			}
2160 
2161 			memcpy(content + contentLength, text + startOffset, lineLength);
2162 			contentLength += lineLength;
2163 
2164 			// add a newline to every line except for the ones
2165 			// that already end in newlines, and the last line
2166 			if ((text[endOffset - 1] != '\n') && (i < numLines - 1)) {
2167 				content[contentLength++] = '\n';
2168 
2169 				// copy quote level of the first line
2170 				size_t quoteLength;
2171 				CopyQuotes(text + startOffset, lineLength, content + contentLength, quoteLength);
2172 				contentLength += quoteLength;
2173 			}
2174 		}
2175 		content[contentLength] = '\0';
2176 
2177 		body->AppendText(content);
2178 		free(content);
2179 	}
2180 
2181 	// reset the text rect and font
2182 	SetTextRect(saveTextRect);
2183 	SetText(saveText);
2184 	free(saveText);
2185 	SetFontAndColor(0, textLength, &fFont);
2186 
2187 	// should be OK to hook these back up now
2188 	if (vScroller != NULL)
2189 		vScroller->SetTarget(this);
2190 	if (hScroller != NULL)
2191 		hScroller->SetTarget(this);
2192 
2193 	Show();
2194 	window->EnableUpdates();
2195 }
2196 
2197 
2198 //	#pragma mark -
2199 
2200 
2201 TTextView::Reader::Reader(bool header, bool raw, bool quote, bool incoming,
2202 		bool stripHeader, bool mime, TTextView *view, BEmailMessage *mail,
2203 		BList *list, sem_id sem)
2204 	:
2205 	fHeader(header),
2206 	fRaw(raw),
2207 	fQuote(quote),
2208 	fIncoming(incoming),
2209 	fStripHeader(stripHeader),
2210 	fMime(mime),
2211 	fView(view),
2212 	fMail(mail),
2213 	fEnclosures(list),
2214 	fStopSem(sem)
2215 {
2216 }
2217 
2218 
2219 bool
2220 TTextView::Reader::ParseMail(BMailContainer *container,
2221 	BTextMailComponent *ignore)
2222 {
2223 	int32 count = 0;
2224 	for (int32 i = 0; i < container->CountComponents(); i++) {
2225 		if (fView->fStopLoading)
2226 			return false;
2227 
2228 		BMailComponent *component;
2229 		if ((component = container->GetComponent(i)) == NULL) {
2230 			if (fView->fStopLoading)
2231 				return false;
2232 
2233 			hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2234 			if (enclosure == NULL)
2235 				return false;
2236 
2237 			memset(enclosure, 0, sizeof(hyper_text));
2238 
2239 			enclosure->type = TYPE_ENCLOSURE;
2240 
2241 			const char *name = "\n<Attachment: could not handle>\n";
2242 
2243 			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
2244 			enclosure->text_start++;
2245 			enclosure->text_end += strlen(name) - 1;
2246 
2247 			Insert(name, strlen(name), true);
2248 			fEnclosures->AddItem(enclosure);
2249 			continue;
2250 		}
2251 
2252 		count++;
2253 		if (component == ignore)
2254 			continue;
2255 
2256 		if (component->ComponentType() == B_MAIL_MULTIPART_CONTAINER) {
2257 			BMIMEMultipartMailContainer *c = dynamic_cast<BMIMEMultipartMailContainer *>(container->GetComponent(i));
2258 			ASSERT(c != NULL);
2259 
2260 			if (!ParseMail(c, ignore))
2261 				count--;
2262 		} else if (fIncoming) {
2263 			hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2264 			if (enclosure == NULL)
2265 				return false;
2266 
2267 			memset(enclosure, 0, sizeof(hyper_text));
2268 
2269 			enclosure->type = TYPE_ENCLOSURE;
2270 			enclosure->component = component;
2271 
2272 			BString name;
2273 			char fileName[B_FILE_NAME_LENGTH];
2274 			strcpy(fileName, "untitled");
2275 			if (BMailAttachment *attachment = dynamic_cast <BMailAttachment *> (component))
2276 				attachment->FileName(fileName);
2277 
2278 			BPath path(fileName);
2279 			enclosure->name = strdup(path.Leaf());
2280 
2281 			BMimeType type;
2282 			component->MIMEType(&type);
2283 			enclosure->content_type = strdup(type.Type());
2284 
2285 			char typeDescription[B_MIME_TYPE_LENGTH];
2286 			if (type.GetShortDescription(typeDescription) != B_OK)
2287 				strcpy(typeDescription, type.Type() ? type.Type() : B_EMPTY_STRING);
2288 
2289 			name = "\n<";
2290 			name.Append(B_TRANSLATE_COMMENT("Enclosure: %name% (Type: %type%)",
2291 				"Don't translate the variables %name% and %type%."));
2292 			name.Append(">\n");
2293 			name.ReplaceFirst("%name%", enclosure->name);
2294 			name.ReplaceFirst("%type%", typeDescription);
2295 
2296 			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
2297 			enclosure->text_start++;
2298 			enclosure->text_end += strlen(name.String()) - 1;
2299 
2300 			Insert(name.String(), name.Length(), true);
2301 			fEnclosures->AddItem(enclosure);
2302 		}
2303 //			default:
2304 //			{
2305 //				PlainTextBodyComponent *body = dynamic_cast<PlainTextBodyComponent *>(container->GetComponent(i));
2306 //				const char *text;
2307 //				if (body && (text = body->Text()) != NULL)
2308 //					Insert(text, strlen(text), false);
2309 //			}
2310 	}
2311 	return count > 0;
2312 }
2313 
2314 
2315 bool
2316 TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader)
2317 {
2318 	char line[522];
2319 	int32 count = 0;
2320 
2321 	const BString dataStr(data, data_len);
2322 	BString nextUrl;
2323 	int32 nextUrlPos = 0, nextUrlLength = 0;
2324 	uint8 nextUrlType
2325 		= FindURL(dataStr, 0, nextUrlPos, nextUrlLength, &nextUrl);
2326 
2327 	for (int32 loop = 0; loop < data_len; loop++) {
2328 		if (fView->fStopLoading)
2329 			return false;
2330 
2331 		if (fQuote && (!loop || (loop && data[loop - 1] == '\n'))) {
2332 			strcpy(&line[count], QUOTE);
2333 			count += strlen(QUOTE);
2334 		}
2335 		if (!fRaw && fIncoming && (loop < data_len - 7)) {
2336 			uint8 type = (nextUrlPos == loop) ? nextUrlType : 0;
2337 
2338 			if (type) {
2339 				if (!Insert(line, count, false, isHeader))
2340 					return false;
2341 				count = 0;
2342 
2343 				hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2344 				if (enclosure == NULL)
2345 					return false;
2346 
2347 				memset(enclosure, 0, sizeof(hyper_text));
2348 				fView->GetSelection(&enclosure->text_start,
2349 					&enclosure->text_end);
2350 				enclosure->type = type;
2351 				enclosure->name = strdup(nextUrl.String());
2352 				if (enclosure->name == NULL) {
2353 					free(enclosure);
2354 					return false;
2355 				}
2356 
2357 				Insert(&data[loop], nextUrlLength, true, isHeader);
2358 				enclosure->text_end += nextUrlLength;
2359 				loop += nextUrlLength - 1;
2360 
2361 				fEnclosures->AddItem(enclosure);
2362 
2363 				nextUrlType
2364 					= FindURL(dataStr, loop,
2365 						nextUrlPos, nextUrlLength, &nextUrl);
2366 				continue;
2367 			}
2368 		}
2369 		if (!fRaw && fMime && data[loop] == '=') {
2370 			if ((loop) && (loop < data_len - 1) && (data[loop + 1] == '\r'))
2371 				loop += 2;
2372 			else
2373 				line[count++] = data[loop];
2374 		} else if (data[loop] != '\r')
2375 			line[count++] = data[loop];
2376 
2377 		if (count > 511 || (count && loop == data_len - 1)) {
2378 			if (!Insert(line, count, false, isHeader))
2379 				return false;
2380 			count = 0;
2381 		}
2382 	}
2383 	return true;
2384 }
2385 
2386 
2387 bool
2388 TTextView::Reader::Insert(const char *line, int32 count, bool isHyperLink,
2389 	bool isHeader)
2390 {
2391 	if (!count)
2392 		return true;
2393 
2394 	BFont font(fView->Font());
2395 	TextRunArray style(count / 8 + 8);
2396 
2397 	if (fView->fColoredQuotes && !isHeader && !isHyperLink) {
2398 		FillInQuoteTextRuns(fView, &fQuoteContext, line, count, font,
2399 			&style.Array(), style.MaxEntries());
2400 	} else {
2401 		text_run_array &array = style.Array();
2402 		array.count = 1;
2403 		array.runs[0].offset = 0;
2404 		if (isHeader) {
2405 			array.runs[0].color = isHyperLink ? kHyperLinkColor : kHeaderColor;
2406 			font.SetSize(font.Size() * 0.9);
2407 		} else {
2408 			array.runs[0].color = isHyperLink
2409 				? kHyperLinkColor : kNormalTextColor;
2410 		}
2411 		array.runs[0].font = font;
2412 	}
2413 
2414 	if (!fView->Window()->Lock())
2415 		return false;
2416 
2417 	fView->Insert(fView->TextLength(), line, count, &style.Array());
2418 
2419 	fView->Window()->Unlock();
2420 	return true;
2421 }
2422 
2423 
2424 status_t
2425 TTextView::Reader::Run(void *_this)
2426 {
2427 	Reader *reader = (Reader *)_this;
2428 	TTextView *view = reader->fView;
2429 	char *msg = NULL;
2430 	off_t size = 0;
2431 	int32 len = 0;
2432 
2433 	if (!reader->Lock())
2434 		return B_INTERRUPTED;
2435 
2436 	BFile *file = dynamic_cast<BFile *>(reader->fMail->Data());
2437 	if (file != NULL) {
2438 		len = header_len(file);
2439 
2440 		if (reader->fHeader)
2441 			size = len;
2442 		if (reader->fRaw || !reader->fMime)
2443 			file->GetSize(&size);
2444 
2445 		if (size != 0 && (msg = (char *)malloc(size)) == NULL)
2446 			goto done;
2447 		file->Seek(0, 0);
2448 
2449 		if (msg)
2450 			size = file->Read(msg, size);
2451 	}
2452 
2453 	// show the header?
2454 	if (reader->fHeader && len) {
2455 		// strip all headers except "From", "To", "Reply-To", "Subject", and "Date"
2456 	 	if (reader->fStripHeader) {
2457 		 	const char *header = msg;
2458 		 	char *buffer = NULL;
2459 
2460 			while (strncmp(header, "\r\n", 2)) {
2461 				const char *eol = header;
2462 				while ((eol = strstr(eol, "\r\n")) != NULL && isspace(eol[2]))
2463 					eol += 2;
2464 				if (eol == NULL)
2465 					break;
2466 
2467 				eol += 2;	// CR+LF belong to the line
2468 				size_t length = eol - header;
2469 
2470 		 		buffer = (char *)realloc(buffer, length + 1);
2471 		 		if (buffer == NULL)
2472 		 			goto done;
2473 
2474 		 		memcpy(buffer, header, length);
2475 
2476 				length = rfc2047_to_utf8(&buffer, &length, length);
2477 
2478 		 		if (!strncasecmp(header, "Reply-To: ", 10)
2479 		 			|| !strncasecmp(header, "To: ", 4)
2480 		 			|| !strncasecmp(header, "From: ", 6)
2481 		 			|| !strncasecmp(header, "Subject: ", 8)
2482 		 			|| !strncasecmp(header, "Date: ", 6))
2483 		 			reader->Process(buffer, length, true);
2484 
2485 		 		header = eol;
2486 	 		}
2487 		 	free(buffer);
2488 	 		reader->Process("\r\n", 2, true);
2489 	 	}
2490 	 	else if (!reader->Process(msg, len, true))
2491 			goto done;
2492 	}
2493 
2494 	if (reader->fRaw) {
2495 		if (!reader->Process((const char *)msg + len, size - len))
2496 			goto done;
2497 	} else {
2498 		//reader->fFile->Seek(0, 0);
2499 		//BEmailMessage *mail = new BEmailMessage(reader->fFile);
2500 		BEmailMessage *mail = reader->fMail;
2501 
2502 		// at first, insert the mail body
2503 		BTextMailComponent *body = NULL;
2504 		if (mail->BodyText() && !view->fStopLoading) {
2505 			char *bodyText = const_cast<char *>(mail->BodyText());
2506 			int32 bodyLength = strlen(bodyText);
2507 			body = mail->Body();
2508 			bool isHTML = false;
2509 
2510 			BMimeType type;
2511 			if (body->MIMEType(&type) == B_OK && type == "text/html") {
2512 				// strip out HTML tags
2513 				char *t = bodyText, *a, *end = bodyText + bodyLength;
2514 				bodyText = (char *)malloc(bodyLength + 1);
2515 				isHTML = true;
2516 
2517 				// TODO: is it correct to assume that the text is in Latin-1?
2518 				//		because if it isn't, the code below won't work correctly...
2519 
2520 				for (a = bodyText; t < end; t++) {
2521 					int32 c = *t;
2522 
2523 					// compact spaces
2524 					bool space = false;
2525 					while (c && (c == ' ' || c == '\t')) {
2526 						c = *(++t);
2527 						space = true;
2528 					}
2529 					if (space) {
2530 						c = ' ';
2531 						t--;
2532 					} else if (FilterHTMLTag(c, &t, end))	// the tag filter
2533 						continue;
2534 
2535 					Unicode2UTF8(c, &a);
2536 				}
2537 
2538 				*a = 0;
2539 				bodyLength = strlen(bodyText);
2540 				body = NULL;	// to add the HTML text as enclosure
2541 			}
2542 			if (!reader->Process(bodyText, bodyLength))
2543 				goto done;
2544 
2545 			if (isHTML)
2546 				free(bodyText);
2547 		}
2548 
2549 		if (!reader->ParseMail(mail, body))
2550 			goto done;
2551 
2552 		//reader->fView->fMail = mail;
2553 	}
2554 
2555 	if (!view->fStopLoading && view->Window()->Lock()) {
2556 		view->Select(0, 0);
2557 		view->MakeSelectable(true);
2558 		if (!reader->fIncoming)
2559 			view->MakeEditable(true);
2560 
2561 		view->Window()->Unlock();
2562 	}
2563 
2564 done:
2565 	// restore the reading position if available
2566 	view->Window()->PostMessage(M_READ_POS);
2567 
2568 	reader->Unlock();
2569 
2570 	delete reader;
2571 	free(msg);
2572 
2573 	return B_NO_ERROR;
2574 }
2575 
2576 
2577 status_t
2578 TTextView::Reader::Unlock()
2579 {
2580 	return release_sem(fStopSem);
2581 }
2582 
2583 
2584 bool
2585 TTextView::Reader::Lock()
2586 {
2587 	if (acquire_sem_etc(fStopSem, 1, B_TIMEOUT, 0) != B_NO_ERROR)
2588 		return false;
2589 
2590 	return true;
2591 }
2592 
2593 
2594 //====================================================================
2595 //	#pragma mark -
2596 
2597 
2598 TSavePanel::TSavePanel(hyper_text *enclosure, TTextView *view)
2599 	: BFilePanel(B_SAVE_PANEL)
2600 {
2601 	fEnclosure = enclosure;
2602 	fView = view;
2603 	if (enclosure->name)
2604 		SetSaveText(enclosure->name);
2605 }
2606 
2607 
2608 void
2609 TSavePanel::SendMessage(const BMessenger * /* messenger */, BMessage *msg)
2610 {
2611 	const char	*name = NULL;
2612 	BMessage	save(M_SAVE);
2613 	entry_ref	ref;
2614 
2615 	if ((!msg->FindRef("directory", &ref)) && (!msg->FindString("name", &name))) {
2616 		save.AddPointer("enclosure", fEnclosure);
2617 		save.AddString("name", name);
2618 		save.AddRef("directory", &ref);
2619 		fView->Window()->PostMessage(&save, fView);
2620 	}
2621 }
2622 
2623 
2624 void
2625 TSavePanel::SetEnclosure(hyper_text *enclosure)
2626 {
2627 	fEnclosure = enclosure;
2628 	if (enclosure->name)
2629 		SetSaveText(enclosure->name);
2630 	else
2631 		SetSaveText("");
2632 
2633 	if (!IsShowing())
2634 		Show();
2635 	Window()->Activate();
2636 }
2637 
2638 
2639 //--------------------------------------------------------------------
2640 //	#pragma mark -
2641 
2642 
2643 void
2644 TTextView::InsertText(const char *insertText, int32 length, int32 offset,
2645 	const text_run_array *runs)
2646 {
2647 	ContentChanged();
2648 
2649 	// Undo function
2650 
2651 	int32 cursorPos, dummy;
2652 	GetSelection(&cursorPos, &dummy);
2653 
2654 	if (fInputMethodUndoState.active) {
2655 		// IMアクティブ時は、一旦別のバッファへ記憶
2656 		fInputMethodUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
2657 		fInputMethodUndoState.replace = false;
2658 	} else {
2659 		if (fUndoState.replaced) {
2660 			fUndoBuffer.AddUndo(insertText, length, offset, K_REPLACED, cursorPos);
2661 		} else {
2662 			if (length == 1 && insertText[0] == 0x0a)
2663 				fUndoBuffer.MakeNewUndoItem();
2664 
2665 			fUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
2666 
2667 			if (length == 1 && insertText[0] == 0x0a)
2668 				fUndoBuffer.MakeNewUndoItem();
2669 		}
2670 	}
2671 
2672 	fUndoState.replaced = false;
2673 	fUndoState.deleted = false;
2674 
2675 	struct text_runs : text_run_array { text_run _runs[1]; } style;
2676 	if (runs == NULL && IsEditable()) {
2677 		style.count = 1;
2678 		style.runs[0].offset = 0;
2679 		style.runs[0].font = fFont;
2680 		style.runs[0].color = kNormalTextColor;
2681 		runs = &style;
2682 	}
2683 
2684 	BTextView::InsertText(insertText, length, offset, runs);
2685 
2686 	if (fSpellCheck && IsEditable())
2687 	{
2688 		UpdateSpellMarks(offset, length);
2689 
2690 		rgb_color color;
2691 		GetFontAndColor(offset - 1, NULL, &color);
2692 		const char *text = Text();
2693 
2694 		if (length > 1
2695 			|| isalpha(text[offset + 1])
2696 			|| (!isalpha(text[offset]) && text[offset] != '\'')
2697 			|| (color.red == kSpellTextColor.red
2698 				&& color.green == kSpellTextColor.green
2699 				&& color.blue == kSpellTextColor.blue))
2700 		{
2701 			int32 start, end;
2702 			FindSpellBoundry(length, offset, &start, &end);
2703 
2704 			DSPELL(printf("Offset %ld, start %ld, end %ld\n", offset, start, end));
2705 			DSPELL(printf("\t\"%10.10s...\"\n", text + start));
2706 
2707 			CheckSpelling(start, end);
2708 		}
2709 	}
2710 }
2711 
2712 
2713 void
2714 TTextView::DeleteText(int32 start, int32 finish)
2715 {
2716 	ContentChanged();
2717 
2718 	// Undo function
2719 	int32 cursorPos, dummy;
2720 	GetSelection(&cursorPos, &dummy);
2721 	if (fInputMethodUndoState.active) {
2722 		if (fInputMethodUndoState.replace) {
2723 			fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
2724 			fInputMethodUndoState.replace = false;
2725 		} else {
2726 			fInputMethodUndoBuffer.AddUndo(&Text()[start], finish - start, start,
2727 				K_DELETED, cursorPos);
2728 		}
2729 	} else
2730 		fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
2731 
2732 	fUndoState.deleted = true;
2733 	fUndoState.replaced = true;
2734 
2735 	BTextView::DeleteText(start, finish);
2736 	if (fSpellCheck && IsEditable()) {
2737 		UpdateSpellMarks(start, start - finish);
2738 
2739 		int32 s, e;
2740 		FindSpellBoundry(1, start, &s, &e);
2741 		CheckSpelling(s, e);
2742 	}
2743 }
2744 
2745 
2746 void
2747 TTextView::ContentChanged(void)
2748 {
2749 	BLooper *looper = Looper();
2750 	if (looper == NULL)
2751 		return;
2752 
2753 	BMessage msg(FIELD_CHANGED);
2754 	msg.AddInt32("bitmask", FIELD_BODY);
2755 	msg.AddPointer("source", this);
2756 	looper->PostMessage(&msg);
2757 }
2758 
2759 
2760 void
2761 TTextView::CheckSpelling(int32 start, int32 end, int32 flags)
2762 {
2763 	const char 	*text = Text();
2764 	const char 	*next, *endPtr, *word = NULL;
2765 	int32 		wordLength = 0, wordOffset;
2766 	int32		nextHighlight = start;
2767 	BString 	testWord;
2768 	bool		isCap = false;
2769 	bool		isAlpha;
2770 	bool		isApost;
2771 
2772 	for (next = text + start, endPtr = text + end; next <= endPtr; next++) {
2773 		//printf("next=%c\n", *next);
2774 		// ToDo: this has to be refined to other languages...
2775 		// Alpha signifies the start of a word
2776 		isAlpha = isalpha(*next);
2777 		isApost = (*next == '\'');
2778 		if (!word && isAlpha) {
2779 			//printf("Found word start\n");
2780 			word = next;
2781 			wordLength++;
2782 			isCap = isupper(*word);
2783 		} else if (word && (isAlpha || isApost) && !(isApost && !isalpha(next[1]))
2784 					&& !(isCap && isApost && (next[1] == 's'))) {
2785 			// Word continues check
2786 			wordLength++;
2787 			//printf("Word continues...\n");
2788 		} else if (word) {
2789 			// End of word reached
2790 
2791 			//printf("Word End\n");
2792 			// Don't check single characters
2793 			if (wordLength > 1) {
2794 				bool isUpper = true;
2795 
2796 				// Look for all uppercase
2797 				for (int32 i = 0; i < wordLength; i++) {
2798 					if (word[i] == '\'')
2799 						break;
2800 
2801 					if (islower(word[i])) {
2802 						isUpper = false;
2803 						break;
2804 					}
2805 				}
2806 
2807 				// Don't check all uppercase words
2808 				if (!isUpper) {
2809 					bool foundMatch = false;
2810 					wordOffset = word - text;
2811 					testWord.SetTo(word, wordLength);
2812 
2813 					testWord = testWord.ToLower();
2814 					DSPELL(printf("Testing: \"%s\"\n", testWord.String()));
2815 
2816 					int32 key = -1;
2817 					if (gDictCount)
2818 						key = gExactWords[0]->GetKey(testWord.String());
2819 
2820 					// Search all dictionaries
2821 					for (int32 i = 0; i < gDictCount; i++) {
2822 						if (gExactWords[i]->Lookup(key) >= 0) {
2823 							foundMatch = true;
2824 							break;
2825 						}
2826 					}
2827 
2828 					if (!foundMatch) {
2829 						if (flags & S_CLEAR_ERRORS)
2830 							RemoveSpellMark(nextHighlight, wordOffset);
2831 
2832 						if (flags & S_SHOW_ERRORS)
2833 							AddSpellMark(wordOffset, wordOffset + wordLength);
2834 					} else if (flags & S_CLEAR_ERRORS)
2835 						RemoveSpellMark(nextHighlight, wordOffset + wordLength);
2836 
2837 					nextHighlight = wordOffset + wordLength;
2838 				}
2839 			}
2840 			// Reset state to looking for word
2841 			word = NULL;
2842 			wordLength = 0;
2843 		}
2844 	}
2845 
2846 	if (nextHighlight <= end
2847 		&& (flags & S_CLEAR_ERRORS) != 0
2848 		&& nextHighlight < TextLength())
2849 		SetFontAndColor(nextHighlight, end, NULL, B_FONT_ALL, &kNormalTextColor);
2850 }
2851 
2852 
2853 void
2854 TTextView::FindSpellBoundry(int32 length, int32 offset, int32 *_start, int32 *_end)
2855 {
2856 	int32 start, end, textLength;
2857 	const char *text = Text();
2858 	textLength = TextLength();
2859 
2860 	for (start = offset - 1; start >= 0
2861 		&& (isalpha(text[start]) || text[start] == '\''); start--) {}
2862 
2863 	start++;
2864 
2865 	for (end = offset + length; end < textLength
2866 		&& (isalpha(text[end]) || text[end] == '\''); end++) {}
2867 
2868 	*_start = start;
2869 	*_end = end;
2870 }
2871 
2872 
2873 TTextView::spell_mark *
2874 TTextView::FindSpellMark(int32 start, int32 end, spell_mark **_previousMark)
2875 {
2876 	spell_mark *lastMark = NULL;
2877 
2878 	for (spell_mark *spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
2879 		if (spellMark->start < end && spellMark->end > start) {
2880 			if (_previousMark)
2881 				*_previousMark = lastMark;
2882 			return spellMark;
2883 		}
2884 
2885 		lastMark = spellMark;
2886 	}
2887 	return NULL;
2888 }
2889 
2890 
2891 void
2892 TTextView::UpdateSpellMarks(int32 offset, int32 length)
2893 {
2894 	DSPELL(printf("UpdateSpellMarks: offset = %ld, length = %ld\n", offset, length));
2895 
2896 	spell_mark *spellMark;
2897 	for (spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
2898 		DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));
2899 
2900 		if (spellMark->end < offset)
2901 			continue;
2902 
2903 		if (spellMark->start > offset)
2904 			spellMark->start += length;
2905 
2906 		spellMark->end += length;
2907 
2908 		DSPELL(printf("\t-> reset: %ld - %ld\n", spellMark->start, spellMark->end));
2909 	}
2910 }
2911 
2912 
2913 status_t
2914 TTextView::AddSpellMark(int32 start, int32 end)
2915 {
2916 	DSPELL(printf("AddSpellMark: start = %ld, end = %ld\n", start, end));
2917 
2918 	// check if there is already a mark for this passage
2919 	spell_mark *spellMark = FindSpellMark(start, end);
2920 	if (spellMark) {
2921 		if (spellMark->start == start && spellMark->end == end) {
2922 			DSPELL(printf("\tfound one\n"));
2923 			return B_OK;
2924 		}
2925 
2926 		DSPELL(printf("\tremove old one\n"));
2927 		RemoveSpellMark(start, end);
2928 	}
2929 
2930 	spellMark = (spell_mark *)malloc(sizeof(spell_mark));
2931 	if (spellMark == NULL)
2932 		return B_NO_MEMORY;
2933 
2934 	spellMark->start = start;
2935 	spellMark->end = end;
2936 	spellMark->style = RunArray(start, end);
2937 
2938 	// set the spell marks appearance
2939 	BFont font(fFont);
2940 	font.SetFace(B_BOLD_FACE | B_ITALIC_FACE);
2941 	SetFontAndColor(start, end, &font, B_FONT_ALL, &kSpellTextColor);
2942 
2943 	// add it to the queue
2944 	spellMark->next = fFirstSpellMark;
2945 	fFirstSpellMark = spellMark;
2946 
2947 	return B_OK;
2948 }
2949 
2950 
2951 bool
2952 TTextView::RemoveSpellMark(int32 start, int32 end)
2953 {
2954 	DSPELL(printf("RemoveSpellMark: start = %ld, end = %ld\n", start, end));
2955 
2956 	// find spell mark
2957 	spell_mark *lastMark = NULL;
2958 	spell_mark *spellMark = FindSpellMark(start, end, &lastMark);
2959 	if (spellMark == NULL) {
2960 		DSPELL(printf("\tnot found!\n"));
2961 		return false;
2962 	}
2963 
2964 	DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));
2965 
2966 	// dequeue the spell mark
2967 	if (lastMark)
2968 		lastMark->next = spellMark->next;
2969 	else
2970 		fFirstSpellMark = spellMark->next;
2971 
2972 	if (spellMark->start < start)
2973 		start = spellMark->start;
2974 	if (spellMark->end > end)
2975 		end = spellMark->end;
2976 
2977 	// reset old text run array
2978 	SetRunArray(start, end, spellMark->style);
2979 
2980 	free(spellMark->style);
2981 	free(spellMark);
2982 
2983 	return true;
2984 }
2985 
2986 
2987 void
2988 TTextView::RemoveSpellMarks()
2989 {
2990 	spell_mark *spellMark, *nextMark;
2991 
2992 	for (spellMark = fFirstSpellMark; spellMark; spellMark = nextMark) {
2993 		nextMark = spellMark->next;
2994 
2995 		// reset old text run array
2996 		SetRunArray(spellMark->start, spellMark->end, spellMark->style);
2997 
2998 		free(spellMark->style);
2999 		free(spellMark);
3000 	}
3001 
3002 	fFirstSpellMark = NULL;
3003 }
3004 
3005 
3006 void
3007 TTextView::EnableSpellCheck(bool enable)
3008 {
3009 	if (fSpellCheck == enable)
3010 		return;
3011 
3012 	fSpellCheck = enable;
3013 	int32 textLength = TextLength();
3014 	if (fSpellCheck) {
3015 		// work-around for a bug in the BTextView class
3016 		// which causes lots of flicker
3017 		int32 start, end;
3018 		GetSelection(&start, &end);
3019 		if (start != end)
3020 			Select(start, start);
3021 
3022 		CheckSpelling(0, textLength);
3023 
3024 		if (start != end)
3025 			Select(start, end);
3026 	}
3027 	else
3028 		RemoveSpellMarks();
3029 }
3030 
3031 
3032 void
3033 TTextView::WindowActivated(bool flag)
3034 {
3035 	if (!flag) {
3036 		// WindowActivated(false) は、IM も Inactive になり、そのまま確定される。
3037 		// しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED)
3038 		// を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。
3039 		// OpenBeOSで修正されることを願って暫定処置としている。
3040 		fInputMethodUndoState.active = false;
3041 		// fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら(確定)正規のバッファへ追加
3042 		if (fInputMethodUndoBuffer.CountItems() > 0) {
3043 			KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1);
3044 			if (item->History == K_INSERTED) {
3045 				fUndoBuffer.MakeNewUndoItem();
3046 				fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset,
3047 					item->History, item->CursorPos);
3048 				fUndoBuffer.MakeNewUndoItem();
3049 			}
3050 			fInputMethodUndoBuffer.MakeEmpty();
3051 		}
3052 	}
3053 	BTextView::WindowActivated(flag);
3054 }
3055 
3056 
3057 void
3058 TTextView::AddQuote(int32 start, int32 finish)
3059 {
3060 	BRect rect = Bounds();
3061 
3062 	int32 lineStart;
3063 	GoToLine(CurrentLine());
3064 	GetSelection(&lineStart, &lineStart);
3065 	lineStart = LineStart(lineStart);
3066 
3067 	// make sure that we're changing the whole last line, too
3068 	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
3069 	{
3070 		const char *text = Text();
3071 		while (text[lineEnd] && text[lineEnd] != '\n')
3072 			lineEnd++;
3073 	}
3074 	Select(lineStart, lineEnd);
3075 
3076 	int32 textLength = lineEnd - lineStart;
3077 	char *text = (char *)malloc(textLength + 1);
3078 	if (text == NULL)
3079 		return;
3080 
3081 	GetText(lineStart, textLength, text);
3082 
3083 	int32 quoteLength = strlen(QUOTE);
3084 	int32 targetLength = 0;
3085 	char *target = NULL;
3086 	int32 lastLine = 0;
3087 
3088 	for (int32 index = 0; index < textLength; index++) {
3089 		if (text[index] == '\n' || index == textLength - 1) {
3090 			// add quote to this line
3091 			int32 lineLength = index - lastLine + 1;
3092 
3093 			char* newTarget = (char *)realloc(target,
3094 				targetLength + lineLength + quoteLength);
3095 			if (newTarget == NULL) {
3096 				// free the old buffer
3097 				free(target);
3098 				target = NULL;
3099 				free(text);
3100 				return;
3101 			} else {
3102 				target = newTarget;
3103 			}
3104 
3105 			// copy the quote sign
3106 			memcpy(&target[targetLength], QUOTE, quoteLength);
3107 			targetLength += quoteLength;
3108 
3109 			// copy the rest of the line
3110 			memcpy(&target[targetLength], &text[lastLine], lineLength);
3111 			targetLength += lineLength;
3112 
3113 			lastLine = index + 1;
3114 		}
3115 	}
3116 
3117 	// replace with quoted text
3118 	free(text);
3119 	Delete();
3120 
3121 	if (fColoredQuotes) {
3122 		const BFont *font = Font();
3123 		TextRunArray style(targetLength / 8 + 8);
3124 
3125 		FillInQuoteTextRuns(NULL, NULL, target, targetLength, font,
3126 			&style.Array(), style.MaxEntries());
3127 		Insert(target, targetLength, &style.Array());
3128 	} else
3129 		Insert(target, targetLength);
3130 
3131 	free(target);
3132 
3133 	// redo the old selection (compute the new start if necessary)
3134 	Select(start + quoteLength, finish + (targetLength - textLength));
3135 
3136 	ScrollTo(rect.LeftTop());
3137 }
3138 
3139 
3140 void
3141 TTextView::RemoveQuote(int32 start, int32 finish)
3142 {
3143 	BRect rect = Bounds();
3144 
3145 	GoToLine(CurrentLine());
3146 	int32 lineStart;
3147 	GetSelection(&lineStart, &lineStart);
3148 	lineStart = LineStart(lineStart);
3149 
3150 	// make sure that we're changing the whole last line, too
3151 	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
3152 	const char *text = Text();
3153 	while (text[lineEnd] && text[lineEnd] != '\n')
3154 		lineEnd++;
3155 
3156 	Select(lineStart, lineEnd);
3157 
3158 	int32 length = lineEnd - lineStart;
3159 	char *target = (char *)malloc(length + 1);
3160 	if (target == NULL)
3161 		return;
3162 
3163 	int32 quoteLength = strlen(QUOTE);
3164 	int32 removed = 0;
3165 	text += lineStart;
3166 
3167 	for (int32 index = 0; index < length;) {
3168 		// find out the length of the current line
3169 		int32 lineLength = 0;
3170 		while (index + lineLength < length && text[lineLength] != '\n')
3171 			lineLength++;
3172 
3173 		// include the newline to be part of this line
3174 		if (text[lineLength] == '\n' && index + lineLength + 1 < length)
3175 			lineLength++;
3176 
3177 		if (!strncmp(text, QUOTE, quoteLength)) {
3178 			// remove quote
3179 			length -= quoteLength;
3180 			removed += quoteLength;
3181 
3182 			lineLength -= quoteLength;
3183 			text += quoteLength;
3184 		}
3185 
3186 		if (lineLength == 0) {
3187 			target[index] = '\0';
3188 			break;
3189 		}
3190 
3191 		memcpy(&target[index], text, lineLength);
3192 
3193 		text += lineLength;
3194 		index += lineLength;
3195 	}
3196 
3197 	if (removed) {
3198 		Delete();
3199 
3200 		if (fColoredQuotes) {
3201 			const BFont *font = Font();
3202 			TextRunArray style(length / 8 + 8);
3203 
3204 			FillInQuoteTextRuns(NULL, NULL, target, length, font,
3205 				&style.Array(), style.MaxEntries());
3206 			Insert(target, length, &style.Array());
3207 		} else
3208 			Insert(target, length);
3209 
3210 		// redo old selection
3211 		bool noSelection = start == finish;
3212 
3213 		if (start > lineStart + quoteLength)
3214 			start -= quoteLength;
3215 		else
3216 			start = lineStart;
3217 
3218 		if (noSelection)
3219 			finish = start;
3220 		else
3221 			finish -= removed;
3222 	}
3223 
3224 	free(target);
3225 
3226 	Select(start, finish);
3227 	ScrollTo(rect.LeftTop());
3228 }
3229 
3230 
3231 int32
3232 TTextView::LineStart(int32 offset)
3233 {
3234 	if (offset <= 0)
3235 		return 0;
3236 
3237 	while (offset > 0) {
3238 		offset = PreviousByte(offset);
3239 		if (ByteAt(offset) == B_ENTER)
3240 			return offset + 1;
3241 	}
3242 
3243 	return offset;
3244 }
3245 
3246 
3247 int32
3248 TTextView::PreviousByte(int32 offset) const
3249 {
3250 	if (offset <= 0)
3251 		return 0;
3252 
3253 	int32 count = 6;
3254 
3255 	for (--offset; offset > 0 && count; --offset, --count) {
3256 		if ((ByteAt(offset) & 0xC0) != 0x80)
3257 			break;
3258 	}
3259 
3260 	return count ? offset : 0;
3261 }
3262 
3263 
3264 void
3265 TTextView::Undo(BClipboard */*clipboard*/)
3266 {
3267 	if (fInputMethodUndoState.active)
3268 		return;
3269 
3270 	int32 length, offset, cursorPos;
3271 	undo_type history;
3272 	char *text;
3273 	status_t status;
3274 
3275 	status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
3276 	if (status == B_OK) {
3277 		fUndoBuffer.Off();
3278 
3279 		switch (history) {
3280 			case K_INSERTED:
3281 				BTextView::Delete(offset, offset + length);
3282 				Select(offset, offset);
3283 				break;
3284 
3285 			case K_DELETED:
3286 				BTextView::Insert(offset, text, length);
3287 				Select(offset, offset + length);
3288 				break;
3289 
3290 			case K_REPLACED:
3291 				BTextView::Delete(offset, offset + length);
3292 				status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
3293 				if (status == B_OK && history == K_DELETED) {
3294 					BTextView::Insert(offset, text, length);
3295 					Select(offset, offset + length);
3296 				} else {
3297 					::beep();
3298 					BAlert* alert = new BAlert("",
3299 						B_TRANSLATE("Inconsistency occurred in the undo/redo "
3300 							"buffer."),	B_TRANSLATE("OK"));
3301 					alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
3302 					alert->Go();
3303 				}
3304 				break;
3305 		}
3306 		ScrollToSelection();
3307 		ContentChanged();
3308 		fUndoBuffer.On();
3309 	}
3310 }
3311 
3312 
3313 void
3314 TTextView::Redo()
3315 {
3316 	if (fInputMethodUndoState.active)
3317 		return;
3318 
3319 	int32 length, offset, cursorPos;
3320 	undo_type history;
3321 	char *text;
3322 	status_t status;
3323 	bool replaced;
3324 
3325 	status = fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
3326 	if (status == B_OK) {
3327 		fUndoBuffer.Off();
3328 
3329 		switch (history) {
3330 			case K_INSERTED:
3331 				BTextView::Insert(offset, text, length);
3332 				Select(offset, offset + length);
3333 				break;
3334 
3335 			case K_DELETED:
3336 				BTextView::Delete(offset, offset + length);
3337 				if (replaced) {
3338 					fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
3339 					BTextView::Insert(offset, text, length);
3340 				}
3341 				Select(offset, offset + length);
3342 				break;
3343 
3344 			case K_REPLACED:
3345 				::beep();
3346 				BAlert* alert = new BAlert("",
3347 					B_TRANSLATE("Inconsistency occurred in the undo/redo "
3348 						"buffer."),	B_TRANSLATE("OK"));
3349 				alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE);
3350 				alert->Go();
3351 				break;
3352 		}
3353 		ScrollToSelection();
3354 		ContentChanged();
3355 		fUndoBuffer.On();
3356 	}
3357 }
3358