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