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