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