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