xref: /haiku/src/apps/mail/Content.cpp (revision 5cbe06f482858c8b79671927e887ab08375a721c)
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<";
2335 			name.Append(B_TRANSLATE_COMMENT("Enclosure: %name% (Type: %type%)",
2336 				"Don't translate the variables %name% and %type%."));
2337 			name.Append(">\n");
2338 			name.ReplaceFirst("%name%", enclosure->name);
2339 			name.ReplaceFirst("%type%", typeDescription);
2340 
2341 			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
2342 			enclosure->text_start++;
2343 			enclosure->text_end += strlen(name.String()) - 1;
2344 
2345 			Insert(name.String(), name.Length(), true);
2346 			fEnclosures->AddItem(enclosure);
2347 		}
2348 //			default:
2349 //			{
2350 //				PlainTextBodyComponent *body = dynamic_cast<PlainTextBodyComponent *>(container->GetComponent(i));
2351 //				const char *text;
2352 //				if (body && (text = body->Text()) != NULL)
2353 //					Insert(text, strlen(text), false);
2354 //			}
2355 	}
2356 	return count > 0;
2357 }
2358 
2359 
2360 bool
2361 TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader)
2362 {
2363 	char line[522];
2364 	int32 count = 0;
2365 
2366 	for (int32 loop = 0; loop < data_len; loop++) {
2367 		if (fView->fStopLoading)
2368 			return false;
2369 
2370 		if (fQuote && (!loop || (loop && data[loop - 1] == '\n'))) {
2371 			strcpy(&line[count], QUOTE);
2372 			count += strlen(QUOTE);
2373 		}
2374 		if (!fRaw && fIncoming && (loop < data_len - 7)) {
2375 			size_t urlLength;
2376 			BString url;
2377 			uint8 type = CheckForURL(data + loop, urlLength, &url);
2378 
2379 			if (type) {
2380 				if (!Insert(line, count, false, isHeader))
2381 					return false;
2382 				count = 0;
2383 
2384 				hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2385 				memset(enclosure, 0, sizeof(hyper_text));
2386 				fView->GetSelection(&enclosure->text_start,
2387 									&enclosure->text_end);
2388 				enclosure->type = type;
2389 				enclosure->name = strdup(url.String());
2390 				if (enclosure->name == NULL)
2391 					return false;
2392 
2393 				Insert(&data[loop], urlLength, true, isHeader);
2394 				enclosure->text_end += urlLength;
2395 				loop += urlLength - 1;
2396 
2397 				fEnclosures->AddItem(enclosure);
2398 				continue;
2399 			}
2400 		}
2401 		if (!fRaw && fMime && data[loop] == '=') {
2402 			if ((loop) && (loop < data_len - 1) && (data[loop + 1] == '\r'))
2403 				loop += 2;
2404 			else
2405 				line[count++] = data[loop];
2406 		} else if (data[loop] != '\r')
2407 			line[count++] = data[loop];
2408 
2409 		if (count > 511 || (count && loop == data_len - 1)) {
2410 			if (!Insert(line, count, false, isHeader))
2411 				return false;
2412 			count = 0;
2413 		}
2414 	}
2415 	return true;
2416 }
2417 
2418 
2419 bool
2420 TTextView::Reader::Insert(const char *line, int32 count, bool isHyperLink,
2421 	bool isHeader)
2422 {
2423 	if (!count)
2424 		return true;
2425 
2426 	BFont font(fView->Font());
2427 	TextRunArray style(count / 8 + 8);
2428 
2429 	if (fView->fColoredQuotes && !isHeader && !isHyperLink) {
2430 		FillInQuoteTextRuns(fView, &fQuoteContext, line, count, font,
2431 			&style.Array(), style.MaxEntries());
2432 	} else {
2433 		text_run_array &array = style.Array();
2434 		array.count = 1;
2435 		array.runs[0].offset = 0;
2436 		if (isHeader) {
2437 			array.runs[0].color = isHyperLink ? kHyperLinkColor : kHeaderColor;
2438 			font.SetSize(font.Size() * 0.9);
2439 		} else {
2440 			array.runs[0].color = isHyperLink
2441 				? kHyperLinkColor : kNormalTextColor;
2442 		}
2443 		array.runs[0].font = font;
2444 	}
2445 
2446 	if (!fView->Window()->Lock())
2447 		return false;
2448 
2449 	fView->Insert(fView->TextLength(), line, count, &style.Array());
2450 
2451 	fView->Window()->Unlock();
2452 	return true;
2453 }
2454 
2455 
2456 status_t
2457 TTextView::Reader::Run(void *_this)
2458 {
2459 	Reader *reader = (Reader *)_this;
2460 	TTextView *view = reader->fView;
2461 	char *msg = NULL;
2462 	off_t size = 0;
2463 	int32 len = 0;
2464 
2465 	if (!reader->Lock())
2466 		return B_INTERRUPTED;
2467 
2468 	BFile *file = dynamic_cast<BFile *>(reader->fMail->Data());
2469 	if (file != NULL) {
2470 		len = header_len(file);
2471 
2472 		if (reader->fHeader)
2473 			size = len;
2474 		if (reader->fRaw || !reader->fMime)
2475 			file->GetSize(&size);
2476 
2477 		if (size != 0 && (msg = (char *)malloc(size)) == NULL)
2478 			goto done;
2479 		file->Seek(0, 0);
2480 
2481 		if (msg)
2482 			size = file->Read(msg, size);
2483 	}
2484 
2485 	// show the header?
2486 	if (reader->fHeader && len) {
2487 		// strip all headers except "From", "To", "Reply-To", "Subject", and "Date"
2488 	 	if (reader->fStripHeader) {
2489 		 	const char *header = msg;
2490 		 	char *buffer = NULL;
2491 
2492 			while (strncmp(header, "\r\n", 2)) {
2493 				const char *eol = header;
2494 				while ((eol = strstr(eol, "\r\n")) != NULL && isspace(eol[2]))
2495 					eol += 2;
2496 				if (eol == NULL)
2497 					break;
2498 
2499 				eol += 2;	// CR+LF belong to the line
2500 				size_t length = eol - header;
2501 
2502 		 		buffer = (char *)realloc(buffer, length + 1);
2503 		 		if (buffer == NULL)
2504 		 			goto done;
2505 
2506 		 		memcpy(buffer, header, length);
2507 
2508 				length = rfc2047_to_utf8(&buffer, &length, length);
2509 
2510 		 		if (!strncasecmp(header, "Reply-To: ", 10)
2511 		 			|| !strncasecmp(header, "To: ", 4)
2512 		 			|| !strncasecmp(header, "From: ", 6)
2513 		 			|| !strncasecmp(header, "Subject: ", 8)
2514 		 			|| !strncasecmp(header, "Date: ", 6))
2515 		 			reader->Process(buffer, length, true);
2516 
2517 		 		header = eol;
2518 	 		}
2519 		 	free(buffer);
2520 	 		reader->Process("\r\n", 2, true);
2521 	 	}
2522 	 	else if (!reader->Process(msg, len, true))
2523 			goto done;
2524 	}
2525 
2526 	if (reader->fRaw) {
2527 		if (!reader->Process((const char *)msg + len, size - len))
2528 			goto done;
2529 	} else {
2530 		//reader->fFile->Seek(0, 0);
2531 		//BEmailMessage *mail = new BEmailMessage(reader->fFile);
2532 		BEmailMessage *mail = reader->fMail;
2533 
2534 		// at first, insert the mail body
2535 		BTextMailComponent *body = NULL;
2536 		if (mail->BodyText() && !view->fStopLoading) {
2537 			char *bodyText = const_cast<char *>(mail->BodyText());
2538 			int32 bodyLength = strlen(bodyText);
2539 			body = mail->Body();
2540 			bool isHTML = false;
2541 
2542 			BMimeType type;
2543 			if (body->MIMEType(&type) == B_OK && type == "text/html") {
2544 				// strip out HTML tags
2545 				char *t = bodyText, *a, *end = bodyText + bodyLength;
2546 				bodyText = (char *)malloc(bodyLength + 1);
2547 				isHTML = true;
2548 
2549 				// TODO: is it correct to assume that the text is in Latin-1?
2550 				//		because if it isn't, the code below won't work correctly...
2551 
2552 				for (a = bodyText; t < end; t++) {
2553 					int32 c = *t;
2554 
2555 					// compact spaces
2556 					bool space = false;
2557 					while (c && (c == ' ' || c == '\t')) {
2558 						c = *(++t);
2559 						space = true;
2560 					}
2561 					if (space) {
2562 						c = ' ';
2563 						t--;
2564 					} else if (FilterHTMLTag(c, &t, end))	// the tag filter
2565 						continue;
2566 
2567 					Unicode2UTF8(c, &a);
2568 				}
2569 
2570 				*a = 0;
2571 				bodyLength = strlen(bodyText);
2572 				body = NULL;	// to add the HTML text as enclosure
2573 			}
2574 			if (!reader->Process(bodyText, bodyLength))
2575 				goto done;
2576 
2577 			if (isHTML)
2578 				free(bodyText);
2579 		}
2580 
2581 		if (!reader->ParseMail(mail, body))
2582 			goto done;
2583 
2584 		//reader->fView->fMail = mail;
2585 	}
2586 
2587 	if (!view->fStopLoading && view->Window()->Lock()) {
2588 		view->Select(0, 0);
2589 		view->MakeSelectable(true);
2590 		if (!reader->fIncoming)
2591 			view->MakeEditable(true);
2592 
2593 		view->Window()->Unlock();
2594 	}
2595 
2596 done:
2597 	reader->Unlock();
2598 
2599 	delete reader;
2600 	free(msg);
2601 
2602 	return B_NO_ERROR;
2603 }
2604 
2605 
2606 status_t
2607 TTextView::Reader::Unlock()
2608 {
2609 	return release_sem(fStopSem);
2610 }
2611 
2612 
2613 bool
2614 TTextView::Reader::Lock()
2615 {
2616 	if (acquire_sem_etc(fStopSem, 1, B_TIMEOUT, 0) != B_NO_ERROR)
2617 		return false;
2618 
2619 	return true;
2620 }
2621 
2622 
2623 //====================================================================
2624 //	#pragma mark -
2625 
2626 
2627 TSavePanel::TSavePanel(hyper_text *enclosure, TTextView *view)
2628 	: BFilePanel(B_SAVE_PANEL)
2629 {
2630 	fEnclosure = enclosure;
2631 	fView = view;
2632 	if (enclosure->name)
2633 		SetSaveText(enclosure->name);
2634 }
2635 
2636 
2637 void
2638 TSavePanel::SendMessage(const BMessenger * /* messenger */, BMessage *msg)
2639 {
2640 	const char	*name = NULL;
2641 	BMessage	save(M_SAVE);
2642 	entry_ref	ref;
2643 
2644 	if ((!msg->FindRef("directory", &ref)) && (!msg->FindString("name", &name))) {
2645 		save.AddPointer("enclosure", fEnclosure);
2646 		save.AddString("name", name);
2647 		save.AddRef("directory", &ref);
2648 		fView->Window()->PostMessage(&save, fView);
2649 	}
2650 }
2651 
2652 
2653 void
2654 TSavePanel::SetEnclosure(hyper_text *enclosure)
2655 {
2656 	fEnclosure = enclosure;
2657 	if (enclosure->name)
2658 		SetSaveText(enclosure->name);
2659 	else
2660 		SetSaveText("");
2661 
2662 	if (!IsShowing())
2663 		Show();
2664 	Window()->Activate();
2665 }
2666 
2667 
2668 //--------------------------------------------------------------------
2669 //	#pragma mark -
2670 
2671 
2672 void
2673 TTextView::InsertText(const char *insertText, int32 length, int32 offset,
2674 	const text_run_array *runs)
2675 {
2676 	ContentChanged();
2677 
2678 	// Undo function
2679 
2680 	int32 cursorPos, dummy;
2681 	GetSelection(&cursorPos, &dummy);
2682 
2683 	if (fInputMethodUndoState.active) {
2684 		// IMアクティブ時は、一旦別のバッファへ記憶
2685 		fInputMethodUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
2686 		fInputMethodUndoState.replace = false;
2687 	} else {
2688 		if (fUndoState.replaced) {
2689 			fUndoBuffer.AddUndo(insertText, length, offset, K_REPLACED, cursorPos);
2690 		} else {
2691 			if (length == 1 && insertText[0] == 0x0a)
2692 				fUndoBuffer.MakeNewUndoItem();
2693 
2694 			fUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
2695 
2696 			if (length == 1 && insertText[0] == 0x0a)
2697 				fUndoBuffer.MakeNewUndoItem();
2698 		}
2699 	}
2700 
2701 	fUndoState.replaced = false;
2702 	fUndoState.deleted = false;
2703 
2704 	struct text_runs : text_run_array { text_run _runs[1]; } style;
2705 	if (runs == NULL && IsEditable()) {
2706 		style.count = 1;
2707 		style.runs[0].offset = 0;
2708 		style.runs[0].font = fFont;
2709 		style.runs[0].color = kNormalTextColor;
2710 		runs = &style;
2711 	}
2712 
2713 	BTextView::InsertText(insertText, length, offset, runs);
2714 
2715 	if (fSpellCheck && IsEditable())
2716 	{
2717 		UpdateSpellMarks(offset, length);
2718 
2719 		rgb_color color;
2720 		GetFontAndColor(offset - 1, NULL, &color);
2721 		const char *text = Text();
2722 
2723 		if (length > 1
2724 			|| isalpha(text[offset + 1])
2725 			|| (!isalpha(text[offset]) && text[offset] != '\'')
2726 			|| (color.red == kSpellTextColor.red
2727 				&& color.green == kSpellTextColor.green
2728 				&& color.blue == kSpellTextColor.blue))
2729 		{
2730 			int32 start, end;
2731 			FindSpellBoundry(length, offset, &start, &end);
2732 
2733 			DSPELL(printf("Offset %ld, start %ld, end %ld\n", offset, start, end));
2734 			DSPELL(printf("\t\"%10.10s...\"\n", text + start));
2735 
2736 			CheckSpelling(start, end);
2737 		}
2738 	}
2739 }
2740 
2741 
2742 void
2743 TTextView::DeleteText(int32 start, int32 finish)
2744 {
2745 	ContentChanged();
2746 
2747 	// Undo function
2748 	int32 cursorPos, dummy;
2749 	GetSelection(&cursorPos, &dummy);
2750 	if (fInputMethodUndoState.active) {
2751 		if (fInputMethodUndoState.replace) {
2752 			fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
2753 			fInputMethodUndoState.replace = false;
2754 		} else {
2755 			fInputMethodUndoBuffer.AddUndo(&Text()[start], finish - start, start,
2756 				K_DELETED, cursorPos);
2757 		}
2758 	} else
2759 		fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
2760 
2761 	fUndoState.deleted = true;
2762 	fUndoState.replaced = true;
2763 
2764 	BTextView::DeleteText(start, finish);
2765 	if (fSpellCheck && IsEditable()) {
2766 		UpdateSpellMarks(start, start - finish);
2767 
2768 		int32 s, e;
2769 		FindSpellBoundry(1, start, &s, &e);
2770 		CheckSpelling(s, e);
2771 	}
2772 }
2773 
2774 
2775 void
2776 TTextView::ContentChanged(void)
2777 {
2778 	BLooper *looper = Looper();
2779 	if (looper == NULL)
2780 		return;
2781 
2782 	BMessage msg(FIELD_CHANGED);
2783 	msg.AddInt32("bitmask", FIELD_BODY);
2784 	msg.AddPointer("source", this);
2785 	looper->PostMessage(&msg);
2786 }
2787 
2788 
2789 void
2790 TTextView::CheckSpelling(int32 start, int32 end, int32 flags)
2791 {
2792 	const char 	*text = Text();
2793 	const char 	*next, *endPtr, *word = NULL;
2794 	int32 		wordLength = 0, wordOffset;
2795 	int32		nextHighlight = start;
2796 	BString 	testWord;
2797 	bool		isCap = false;
2798 	bool		isAlpha;
2799 	bool		isApost;
2800 
2801 	for (next = text + start, endPtr = text + end; next <= endPtr; next++) {
2802 		//printf("next=%c\n", *next);
2803 		// ToDo: this has to be refined to other languages...
2804 		// Alpha signifies the start of a word
2805 		isAlpha = isalpha(*next);
2806 		isApost = (*next == '\'');
2807 		if (!word && isAlpha) {
2808 			//printf("Found word start\n");
2809 			word = next;
2810 			wordLength++;
2811 			isCap = isupper(*word);
2812 		} else if (word && (isAlpha || isApost) && !(isApost && !isalpha(next[1]))
2813 					&& !(isCap && isApost && (next[1] == 's'))) {
2814 			// Word continues check
2815 			wordLength++;
2816 			//printf("Word continues...\n");
2817 		} else if (word) {
2818 			// End of word reached
2819 
2820 			//printf("Word End\n");
2821 			// Don't check single characters
2822 			if (wordLength > 1) {
2823 				bool isUpper = true;
2824 
2825 				// Look for all uppercase
2826 				for (int32 i = 0; i < wordLength; i++) {
2827 					if (word[i] == '\'')
2828 						break;
2829 
2830 					if (islower(word[i])) {
2831 						isUpper = false;
2832 						break;
2833 					}
2834 				}
2835 
2836 				// Don't check all uppercase words
2837 				if (!isUpper) {
2838 					bool foundMatch = false;
2839 					wordOffset = word - text;
2840 					testWord.SetTo(word, wordLength);
2841 
2842 					testWord = testWord.ToLower();
2843 					DSPELL(printf("Testing: \"%s\"\n", testWord.String()));
2844 
2845 					int32 key = -1;
2846 					if (gDictCount)
2847 						key = gExactWords[0]->GetKey(testWord.String());
2848 
2849 					// Search all dictionaries
2850 					for (int32 i = 0; i < gDictCount; i++) {
2851 						if (gExactWords[i]->Lookup(key) >= 0) {
2852 							foundMatch = true;
2853 							break;
2854 						}
2855 					}
2856 
2857 					if (!foundMatch) {
2858 						if (flags & S_CLEAR_ERRORS)
2859 							RemoveSpellMark(nextHighlight, wordOffset);
2860 
2861 						if (flags & S_SHOW_ERRORS)
2862 							AddSpellMark(wordOffset, wordOffset + wordLength);
2863 					} else if (flags & S_CLEAR_ERRORS)
2864 						RemoveSpellMark(nextHighlight, wordOffset + wordLength);
2865 
2866 					nextHighlight = wordOffset + wordLength;
2867 				}
2868 			}
2869 			// Reset state to looking for word
2870 			word = NULL;
2871 			wordLength = 0;
2872 		}
2873 	}
2874 
2875 	if (nextHighlight <= end
2876 		&& (flags & S_CLEAR_ERRORS) != 0
2877 		&& nextHighlight < TextLength())
2878 		SetFontAndColor(nextHighlight, end, NULL, B_FONT_ALL, &kNormalTextColor);
2879 }
2880 
2881 
2882 void
2883 TTextView::FindSpellBoundry(int32 length, int32 offset, int32 *_start, int32 *_end)
2884 {
2885 	int32 start, end, textLength;
2886 	const char *text = Text();
2887 	textLength = TextLength();
2888 
2889 	for (start = offset - 1; start >= 0
2890 		&& (isalpha(text[start]) || text[start] == '\''); start--) {}
2891 
2892 	start++;
2893 
2894 	for (end = offset + length; end < textLength
2895 		&& (isalpha(text[end]) || text[end] == '\''); end++) {}
2896 
2897 	*_start = start;
2898 	*_end = end;
2899 }
2900 
2901 
2902 TTextView::spell_mark *
2903 TTextView::FindSpellMark(int32 start, int32 end, spell_mark **_previousMark)
2904 {
2905 	spell_mark *lastMark = NULL;
2906 
2907 	for (spell_mark *spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
2908 		if (spellMark->start < end && spellMark->end > start) {
2909 			if (_previousMark)
2910 				*_previousMark = lastMark;
2911 			return spellMark;
2912 		}
2913 
2914 		lastMark = spellMark;
2915 	}
2916 	return NULL;
2917 }
2918 
2919 
2920 void
2921 TTextView::UpdateSpellMarks(int32 offset, int32 length)
2922 {
2923 	DSPELL(printf("UpdateSpellMarks: offset = %ld, length = %ld\n", offset, length));
2924 
2925 	spell_mark *spellMark;
2926 	for (spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
2927 		DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));
2928 
2929 		if (spellMark->end < offset)
2930 			continue;
2931 
2932 		if (spellMark->start > offset)
2933 			spellMark->start += length;
2934 
2935 		spellMark->end += length;
2936 
2937 		DSPELL(printf("\t-> reset: %ld - %ld\n", spellMark->start, spellMark->end));
2938 	}
2939 }
2940 
2941 
2942 status_t
2943 TTextView::AddSpellMark(int32 start, int32 end)
2944 {
2945 	DSPELL(printf("AddSpellMark: start = %ld, end = %ld\n", start, end));
2946 
2947 	// check if there is already a mark for this passage
2948 	spell_mark *spellMark = FindSpellMark(start, end);
2949 	if (spellMark) {
2950 		if (spellMark->start == start && spellMark->end == end) {
2951 			DSPELL(printf("\tfound one\n"));
2952 			return B_OK;
2953 		}
2954 
2955 		DSPELL(printf("\tremove old one\n"));
2956 		RemoveSpellMark(start, end);
2957 	}
2958 
2959 	spellMark = (spell_mark *)malloc(sizeof(spell_mark));
2960 	if (spellMark == NULL)
2961 		return B_NO_MEMORY;
2962 
2963 	spellMark->start = start;
2964 	spellMark->end = end;
2965 	spellMark->style = RunArray(start, end);
2966 
2967 	// set the spell marks appearance
2968 	BFont font(fFont);
2969 	font.SetFace(B_BOLD_FACE | B_ITALIC_FACE);
2970 	SetFontAndColor(start, end, &font, B_FONT_ALL, &kSpellTextColor);
2971 
2972 	// add it to the queue
2973 	spellMark->next = fFirstSpellMark;
2974 	fFirstSpellMark = spellMark;
2975 
2976 	return B_OK;
2977 }
2978 
2979 
2980 bool
2981 TTextView::RemoveSpellMark(int32 start, int32 end)
2982 {
2983 	DSPELL(printf("RemoveSpellMark: start = %ld, end = %ld\n", start, end));
2984 
2985 	// find spell mark
2986 	spell_mark *lastMark = NULL;
2987 	spell_mark *spellMark = FindSpellMark(start, end, &lastMark);
2988 	if (spellMark == NULL) {
2989 		DSPELL(printf("\tnot found!\n"));
2990 		return false;
2991 	}
2992 
2993 	DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));
2994 
2995 	// dequeue the spell mark
2996 	if (lastMark)
2997 		lastMark->next = spellMark->next;
2998 	else
2999 		fFirstSpellMark = spellMark->next;
3000 
3001 	if (spellMark->start < start)
3002 		start = spellMark->start;
3003 	if (spellMark->end > end)
3004 		end = spellMark->end;
3005 
3006 	// reset old text run array
3007 	SetRunArray(start, end, spellMark->style);
3008 
3009 	free(spellMark->style);
3010 	free(spellMark);
3011 
3012 	return true;
3013 }
3014 
3015 
3016 void
3017 TTextView::RemoveSpellMarks()
3018 {
3019 	spell_mark *spellMark, *nextMark;
3020 
3021 	for (spellMark = fFirstSpellMark; spellMark; spellMark = nextMark) {
3022 		nextMark = spellMark->next;
3023 
3024 		// reset old text run array
3025 		SetRunArray(spellMark->start, spellMark->end, spellMark->style);
3026 
3027 		free(spellMark->style);
3028 		free(spellMark);
3029 	}
3030 
3031 	fFirstSpellMark = NULL;
3032 }
3033 
3034 
3035 void
3036 TTextView::EnableSpellCheck(bool enable)
3037 {
3038 	if (fSpellCheck == enable)
3039 		return;
3040 
3041 	fSpellCheck = enable;
3042 	int32 textLength = TextLength();
3043 	if (fSpellCheck) {
3044 		// work-around for a bug in the BTextView class
3045 		// which causes lots of flicker
3046 		int32 start, end;
3047 		GetSelection(&start, &end);
3048 		if (start != end)
3049 			Select(start, start);
3050 
3051 		CheckSpelling(0, textLength);
3052 
3053 		if (start != end)
3054 			Select(start, end);
3055 	}
3056 	else
3057 		RemoveSpellMarks();
3058 }
3059 
3060 
3061 void
3062 TTextView::WindowActivated(bool flag)
3063 {
3064 	if (!flag) {
3065 		// WindowActivated(false) は、IM も Inactive になり、そのまま確定される。
3066 		// しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED)
3067 		// を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。
3068 		// OpenBeOSで修正されることを願って暫定処置としている。
3069 		fInputMethodUndoState.active = false;
3070 		// fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら(確定)正規のバッファへ追加
3071 		if (fInputMethodUndoBuffer.CountItems() > 0) {
3072 			KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1);
3073 			if (item->History == K_INSERTED) {
3074 				fUndoBuffer.MakeNewUndoItem();
3075 				fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset,
3076 					item->History, item->CursorPos);
3077 				fUndoBuffer.MakeNewUndoItem();
3078 			}
3079 			fInputMethodUndoBuffer.MakeEmpty();
3080 		}
3081 	}
3082 	BTextView::WindowActivated(flag);
3083 }
3084 
3085 
3086 void
3087 TTextView::AddQuote(int32 start, int32 finish)
3088 {
3089 	BRect rect = Bounds();
3090 
3091 	int32 lineStart;
3092 	GoToLine(CurrentLine());
3093 	GetSelection(&lineStart, &lineStart);
3094 
3095 	// make sure that we're changing the whole last line, too
3096 	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
3097 	{
3098 		const char *text = Text();
3099 		while (text[lineEnd] && text[lineEnd] != '\n')
3100 			lineEnd++;
3101 	}
3102 	Select(lineStart, lineEnd);
3103 
3104 	int32 textLength = lineEnd - lineStart;
3105 	char *text = (char *)malloc(textLength + 1);
3106 	if (text == NULL)
3107 		return;
3108 
3109 	GetText(lineStart, textLength, text);
3110 
3111 	int32 quoteLength = strlen(QUOTE);
3112 	int32 targetLength = 0;
3113 	char *target = NULL;
3114 	int32 lastLine = 0;
3115 
3116 	for (int32 index = 0; index < textLength; index++) {
3117 		if (text[index] == '\n' || index == textLength - 1) {
3118 			// add quote to this line
3119 			int32 lineLength = index - lastLine + 1;
3120 
3121 			target = (char *)realloc(target, targetLength + lineLength + quoteLength);
3122 			if (target == NULL) {
3123 				// free the old buffer?
3124 				free(text);
3125 				return;
3126 			}
3127 
3128 			// copy the quote sign
3129 			memcpy(&target[targetLength], QUOTE, quoteLength);
3130 			targetLength += quoteLength;
3131 
3132 			// copy the rest of the line
3133 			memcpy(&target[targetLength], &text[lastLine], lineLength);
3134 			targetLength += lineLength;
3135 
3136 			lastLine = index + 1;
3137 		}
3138 	}
3139 
3140 	// replace with quoted text
3141 	free(text);
3142 	Delete();
3143 
3144 	if (fColoredQuotes) {
3145 		const BFont *font = Font();
3146 		TextRunArray style(targetLength / 8 + 8);
3147 
3148 		FillInQuoteTextRuns(NULL, NULL, target, targetLength, font,
3149 			&style.Array(), style.MaxEntries());
3150 		Insert(target, targetLength, &style.Array());
3151 	} else
3152 		Insert(target, targetLength);
3153 
3154 	free(target);
3155 
3156 	// redo the old selection (compute the new start if necessary)
3157 	Select(start + quoteLength, finish + (targetLength - textLength));
3158 
3159 	ScrollTo(rect.LeftTop());
3160 }
3161 
3162 
3163 void
3164 TTextView::RemoveQuote(int32 start, int32 finish)
3165 {
3166 	BRect rect = Bounds();
3167 
3168 	GoToLine(CurrentLine());
3169 	int32 lineStart;
3170 	GetSelection(&lineStart, &lineStart);
3171 
3172 	// make sure that we're changing the whole last line, too
3173 	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
3174 	const char *text = Text();
3175 	while (text[lineEnd] && text[lineEnd] != '\n')
3176 		lineEnd++;
3177 
3178 	Select(lineStart, lineEnd);
3179 
3180 	int32 length = lineEnd - lineStart;
3181 	char *target = (char *)malloc(length + 1);
3182 	if (target == NULL)
3183 		return;
3184 
3185 	int32 quoteLength = strlen(QUOTE);
3186 	int32 removed = 0;
3187 	text += lineStart;
3188 
3189 	for (int32 index = 0; index < length;) {
3190 		// find out the length of the current line
3191 		int32 lineLength = 0;
3192 		while (index + lineLength < length && text[lineLength] != '\n')
3193 			lineLength++;
3194 
3195 		// include the newline to be part of this line
3196 		if (text[lineLength] == '\n' && index + lineLength + 1 < length)
3197 			lineLength++;
3198 
3199 		if (!strncmp(text, QUOTE, quoteLength)) {
3200 			// remove quote
3201 			length -= quoteLength;
3202 			removed += quoteLength;
3203 
3204 			lineLength -= quoteLength;
3205 			text += quoteLength;
3206 		}
3207 
3208 		if (lineLength == 0) {
3209 			target[index] = '\0';
3210 			break;
3211 		}
3212 
3213 		memcpy(&target[index], text, lineLength);
3214 
3215 		text += lineLength;
3216 		index += lineLength;
3217 	}
3218 
3219 	if (removed) {
3220 		Delete();
3221 
3222 		if (fColoredQuotes) {
3223 			const BFont *font = Font();
3224 			TextRunArray style(length / 8 + 8);
3225 
3226 			FillInQuoteTextRuns(NULL, NULL, target, length, font,
3227 				&style.Array(), style.MaxEntries());
3228 			Insert(target, length, &style.Array());
3229 		} else
3230 			Insert(target, length);
3231 
3232 		// redo old selection
3233 		bool noSelection = start == finish;
3234 
3235 		if (start > lineStart + quoteLength)
3236 			start -= quoteLength;
3237 		else
3238 			start = lineStart;
3239 
3240 		if (noSelection)
3241 			finish = start;
3242 		else
3243 			finish -= removed;
3244 	}
3245 
3246 	free(target);
3247 
3248 	Select(start, finish);
3249 	ScrollTo(rect.LeftTop());
3250 }
3251 
3252 
3253 void
3254 TTextView::Undo(BClipboard */*clipboard*/)
3255 {
3256 	if (fInputMethodUndoState.active)
3257 		return;
3258 
3259 	int32 length, offset, cursorPos;
3260 	undo_type history;
3261 	char *text;
3262 	status_t status;
3263 
3264 	status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
3265 	if (status == B_OK) {
3266 		fUndoBuffer.Off();
3267 
3268 		switch (history) {
3269 			case K_INSERTED:
3270 				BTextView::Delete(offset, offset + length);
3271 				Select(offset, offset);
3272 				break;
3273 
3274 			case K_DELETED:
3275 				BTextView::Insert(offset, text, length);
3276 				Select(offset, offset + length);
3277 				break;
3278 
3279 			case K_REPLACED:
3280 				BTextView::Delete(offset, offset + length);
3281 				status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
3282 				if (status == B_OK && history == K_DELETED) {
3283 					BTextView::Insert(offset, text, length);
3284 					Select(offset, offset + length);
3285 				} else {
3286 					::beep();
3287 					(new BAlert("",
3288 						B_TRANSLATE("Inconsistency occurred in the undo/redo "
3289 							"buffer."),
3290 						B_TRANSLATE("OK")))->Go();
3291 				}
3292 				break;
3293 		}
3294 		ScrollToSelection();
3295 		ContentChanged();
3296 		fUndoBuffer.On();
3297 	}
3298 }
3299 
3300 
3301 void
3302 TTextView::Redo()
3303 {
3304 	if (fInputMethodUndoState.active)
3305 		return;
3306 
3307 	int32 length, offset, cursorPos;
3308 	undo_type history;
3309 	char *text;
3310 	status_t status;
3311 	bool replaced;
3312 
3313 	status = fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
3314 	if (status == B_OK) {
3315 		fUndoBuffer.Off();
3316 
3317 		switch (history) {
3318 			case K_INSERTED:
3319 				BTextView::Insert(offset, text, length);
3320 				Select(offset, offset + length);
3321 				break;
3322 
3323 			case K_DELETED:
3324 				BTextView::Delete(offset, offset + length);
3325 				if (replaced) {
3326 					fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
3327 					BTextView::Insert(offset, text, length);
3328 				}
3329 				Select(offset, offset + length);
3330 				break;
3331 
3332 			case K_REPLACED:
3333 				::beep();
3334 				(new BAlert("",
3335 					B_TRANSLATE("Inconsistency occurred in the undo/redo "
3336 						"buffer."),
3337 					B_TRANSLATE("OK")))->Go();
3338 				break;
3339 		}
3340 		ScrollToSelection();
3341 		ContentChanged();
3342 		fUndoBuffer.On();
3343 	}
3344 }
3345 
3346