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