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