xref: /haiku/src/apps/mail/Content.cpp (revision 6e434fd80e4640c64031faf5e49720c5672fc470)
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_TRANSLATE_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 				int32 matchCount;
1554 				for (int32 i = 0; i < gDictCount; i++)
1555 					matchCount = gWords[i]->FindBestMatches(&matches,
1556 						srcWord.String());
1557 
1558 				if (matches.CountItems()) {
1559 					sort_word_list(&matches, srcWord.String());
1560 					for (int32 i = 0; (string = (BString *)matches.ItemAt(i)) != NULL; i++) {
1561 						menu.AddItem((menuItem = new BMenuItem(string->String(), NULL)));
1562 						if (!strcasecmp(string->String(), srcWord.String())) {
1563 							menuItem->SetEnabled(false);
1564 							foundWord = true;
1565 						}
1566 						delete string;
1567 					}
1568 				} else {
1569 					menuItem = new BMenuItem(B_TRANSLATE("No matches"), NULL);
1570 					menuItem->SetEnabled(false);
1571 					menu.AddItem(menuItem);
1572 				}
1573 
1574 				BMenuItem *addItem = NULL;
1575 				if (!foundWord && gUserDict >= 0) {
1576 					menu.AddSeparatorItem();
1577 					addItem = new BMenuItem(B_TRANSLATE("Add"), NULL);
1578 					menu.AddItem(addItem);
1579 				}
1580 
1581 				point = ConvertToScreen(where);
1582 				if ((menuItem = menu.Go(point, false, false)) != NULL) {
1583 					if (menuItem == addItem) {
1584 						BString newItem(srcWord.String());
1585 						newItem << "\n";
1586 						gWords[gUserDict]->InitIndex();
1587 						gExactWords[gUserDict]->InitIndex();
1588 						gUserDictFile->Write(newItem.String(), newItem.Length());
1589 						gWords[gUserDict]->BuildIndex();
1590 						gExactWords[gUserDict]->BuildIndex();
1591 
1592 						if (fSpellCheck)
1593 							CheckSpelling(0, TextLength());
1594 					} else {
1595 						int32 len = strlen(menuItem->Label());
1596 						Select(start, start);
1597 						Delete(start, end);
1598 						Insert(start, menuItem->Label(), len);
1599 						Select(start+len, start+len);
1600 					}
1601 				}
1602 			}
1603 			return;
1604 		} else if (fSpellCheck && IsEditable()) {
1605 			int32 start, end;
1606 
1607 			GetSelection(&start, &end);
1608 			FindSpellBoundry(1, start, &start, &end);
1609 			CheckSpelling(start, end);
1610 		}
1611 	} else {
1612 		// is not editable, look for enclosures/links
1613 
1614 		int32 clickOffset = OffsetAt(where);
1615 		int32 items = fEnclosures->CountItems();
1616 		for (int32 loop = 0; loop < items; loop++) {
1617 			hyper_text *enclosure = (hyper_text*) fEnclosures->ItemAt(loop);
1618 			if (clickOffset < enclosure->text_start || clickOffset >= enclosure->text_end)
1619 				continue;
1620 
1621 			//
1622 			// The user is clicking on this attachment
1623 			//
1624 
1625 			int32 start;
1626 			int32 finish;
1627 			Select(enclosure->text_start, enclosure->text_end);
1628 			GetSelection(&start, &finish);
1629 			Window()->UpdateIfNeeded();
1630 
1631 			bool drag = false;
1632 			bool held = false;
1633 			uint32 buttons = 0;
1634 			if (Window()->CurrentMessage()) {
1635 				Window()->CurrentMessage()->FindInt32("buttons",
1636 				  (int32 *) &buttons);
1637 			}
1638 
1639 			//
1640 			// If this is the primary button, wait to see if the user is going
1641 			// to single click, hold, or drag.
1642 			//
1643 			if (buttons != B_SECONDARY_MOUSE_BUTTON) {
1644 				BPoint point = where;
1645 				bigtime_t popupDelay;
1646 				get_click_speed(&popupDelay);
1647 				popupDelay *= 2;
1648 				popupDelay += system_time();
1649 				while (buttons && abs((int)(point.x - where.x)) < 4
1650 					&& abs((int)(point.y - where.y)) < 4
1651 					&& system_time() < popupDelay) {
1652 					snooze(10000);
1653 					GetMouse(&point, &buttons);
1654 				}
1655 
1656 				if (system_time() < popupDelay) {
1657 					//
1658 					// The user either dragged this or released the button.
1659 					// check if it was dragged.
1660 					//
1661 					if (!(abs((int)(point.x - where.x)) < 4
1662 						&& abs((int)(point.y - where.y)) < 4) && buttons)
1663 						drag = true;
1664 				} else  {
1665 					//
1666 					//	The user held the button down.
1667 					//
1668 					held = true;
1669 				}
1670 			}
1671 
1672 			//
1673 			//	If the user has right clicked on this menu,
1674 			// 	or held the button down on it for a while,
1675 			//	pop up a context menu.
1676 			//
1677 			if (buttons == B_SECONDARY_MOUSE_BUTTON || held) {
1678 				//
1679 				// Right mouse click... Display a menu
1680 				//
1681 				BPoint point = where;
1682 				ConvertToScreen(&point);
1683 
1684 				BMenuItem *item;
1685 				if ((enclosure->type != TYPE_ENCLOSURE)
1686 					&& (enclosure->type != TYPE_BE_ENCLOSURE))
1687 					item = fLinkMenu->Go(point, true);
1688 				else
1689 					item = fEnclosureMenu->Go(point, true);
1690 
1691 				BMessage *msg;
1692 				if (item && (msg = item->Message()) != NULL) {
1693 					if (msg->what == M_SAVE) {
1694 						if (fPanel)
1695 							fPanel->SetEnclosure(enclosure);
1696 						else {
1697 							fPanel = new TSavePanel(enclosure, this);
1698 							fPanel->Window()->Show();
1699 						}
1700 					} else if (msg->what == M_COPY) {
1701 						// copy link location to clipboard
1702 
1703 						if (be_clipboard->Lock()) {
1704 							be_clipboard->Clear();
1705 
1706 							BMessage *clip;
1707 							if ((clip = be_clipboard->Data()) != NULL) {
1708 								clip->AddData("text/plain", B_MIME_TYPE,
1709 									enclosure->name, strlen(enclosure->name));
1710 								be_clipboard->Commit();
1711 							}
1712 							be_clipboard->Unlock();
1713 						}
1714 					} else
1715 						Open(enclosure);
1716 				}
1717 			} else {
1718 				//
1719 				// Left button.  If the user single clicks, open this link.
1720 				// Otherwise, initiate a drag.
1721 				//
1722 				if (drag) {
1723 					BMessage dragMessage(B_SIMPLE_DATA);
1724 					dragMessage.AddInt32("be:actions", B_COPY_TARGET);
1725 					dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1726 					switch (enclosure->type) {
1727 						case TYPE_BE_ENCLOSURE:
1728 						case TYPE_ENCLOSURE:
1729 							//
1730 							// Attachment.  The type is specified in the message.
1731 							//
1732 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1733 							dragMessage.AddString("be:filetypes",
1734 								enclosure->content_type ? enclosure->content_type : "");
1735 							dragMessage.AddString("be:clip_name", enclosure->name);
1736 							break;
1737 
1738 						case TYPE_URL:
1739 							//
1740 							// URL.  The user can drag it into the tracker to
1741 							// create a bookmark file.
1742 							//
1743 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1744 							dragMessage.AddString("be:filetypes",
1745 							  "application/x-vnd.Be-bookmark");
1746 							dragMessage.AddString("be:filetypes", "text/plain");
1747 							dragMessage.AddString("be:clip_name", "Bookmark");
1748 
1749 							dragMessage.AddString("be:url", enclosure->name);
1750 							break;
1751 
1752 						case TYPE_MAILTO:
1753 							//
1754 							// Mailto address.  The user can drag it into the
1755 							// tracker to create a people file.
1756 							//
1757 							dragMessage.AddString("be:types", B_FILE_MIME_TYPE);
1758 							dragMessage.AddString("be:filetypes",
1759 							  "application/x-person");
1760 							dragMessage.AddString("be:filetypes", "text/plain");
1761 							dragMessage.AddString("be:clip_name", "Person");
1762 
1763 							dragMessage.AddString("be:email", enclosure->name);
1764 							break;
1765 
1766 						default:
1767 							//
1768 							// Otherwise it doesn't have a type that I know how
1769 							// to save.  It won't have any types and if any
1770 							// program wants to accept it, more power to them.
1771 							// (tracker won't.)
1772 							//
1773 							dragMessage.AddString("be:clip_name", "Hyperlink");
1774 					}
1775 
1776 					BMessage data;
1777 					data.AddPointer("enclosure", enclosure);
1778 					dragMessage.AddMessage("be:originator-data", &data);
1779 
1780 					BRegion selectRegion;
1781 					GetTextRegion(start, finish, &selectRegion);
1782 					DragMessage(&dragMessage, selectRegion.Frame(), this);
1783 				} else {
1784 					//
1785 					//	User Single clicked on the attachment.  Open it.
1786 					//
1787 					Open(enclosure);
1788 				}
1789 			}
1790 			return;
1791 		}
1792 	}
1793 	BTextView::MouseDown(where);
1794 }
1795 
1796 
1797 void
1798 TTextView::MouseMoved(BPoint where, uint32 code, const BMessage *msg)
1799 {
1800 	int32 start = OffsetAt(where);
1801 
1802 	for (int32 loop = fEnclosures->CountItems(); loop-- > 0;) {
1803 		hyper_text *enclosure = (hyper_text *)fEnclosures->ItemAt(loop);
1804 		if ((start >= enclosure->text_start) && (start < enclosure->text_end)) {
1805 			if (!fCursor)
1806 				SetViewCursor(B_CURSOR_SYSTEM_DEFAULT);
1807 			fCursor = true;
1808 			return;
1809 		}
1810 	}
1811 
1812 	if (fCursor) {
1813 		SetViewCursor(B_CURSOR_I_BEAM);
1814 		fCursor = false;
1815 	}
1816 
1817 	BTextView::MouseMoved(where, code, msg);
1818 }
1819 
1820 
1821 void
1822 TTextView::ClearList()
1823 {
1824 	hyper_text *enclosure;
1825 	while ((enclosure = (hyper_text *)fEnclosures->FirstItem()) != NULL) {
1826 		fEnclosures->RemoveItem(enclosure);
1827 
1828 		if (enclosure->name)
1829 			free(enclosure->name);
1830 		if (enclosure->content_type)
1831 			free(enclosure->content_type);
1832 		if (enclosure->encoding)
1833 			free(enclosure->encoding);
1834 		if (enclosure->have_ref && !enclosure->saved) {
1835 			BEntry entry(&enclosure->ref);
1836 			entry.Remove();
1837 		}
1838 
1839 		watch_node(&enclosure->node, B_STOP_WATCHING, this);
1840 		free(enclosure);
1841 	}
1842 }
1843 
1844 
1845 void
1846 TTextView::LoadMessage(BEmailMessage *mail, bool quoteIt, const char *text)
1847 {
1848 	StopLoad();
1849 
1850 	fMail = mail;
1851 
1852 	ClearList();
1853 
1854 	MakeSelectable(true);
1855 	MakeEditable(false);
1856 	if (text)
1857 		Insert(text, strlen(text));
1858 
1859 	//attr_info attrInfo;
1860 	TTextView::Reader *reader = new TTextView::Reader(fHeader, fRaw, quoteIt, fIncoming,
1861 				text != NULL, true,
1862 				// I removed the following, because I absolutely can't imagine why it's
1863 				// there (the mail kit should be able to deal with non-compliant mails)
1864 				// -- axeld.
1865 				// fFile->GetAttrInfo(B_MAIL_ATTR_MIME, &attrInfo) == B_OK,
1866 				this, mail, fEnclosures, fStopSem);
1867 
1868 	resume_thread(fThread = spawn_thread(Reader::Run, "reader", B_NORMAL_PRIORITY, reader));
1869 }
1870 
1871 
1872 void
1873 TTextView::Open(hyper_text *enclosure)
1874 {
1875 	switch (enclosure->type) {
1876 		case TYPE_URL:
1877 		{
1878 			const struct {const char *urlType, *handler; } handlerTable[] = {
1879 				{"http",	B_URL_HTTP},
1880 				{"https",	B_URL_HTTPS},
1881 				{"ftp",		B_URL_FTP},
1882 				{"gopher",	B_URL_GOPHER},
1883 				{"mailto",	B_URL_MAILTO},
1884 				{"news",	B_URL_NEWS},
1885 				{"nntp",	B_URL_NNTP},
1886 				{"telnet",	B_URL_TELNET},
1887 				{"rlogin",	B_URL_RLOGIN},
1888 				{"tn3270",	B_URL_TN3270},
1889 				{"wais",	B_URL_WAIS},
1890 				{"file",	B_URL_FILE},
1891 				{NULL,		NULL}
1892 			};
1893 			const char *handlerToLaunch = NULL;
1894 
1895 			const char *colonPos = strchr(enclosure->name, ':');
1896 			if (colonPos) {
1897 				int urlTypeLength = colonPos - enclosure->name;
1898 
1899 				for (int32 index = 0; handlerTable[index].urlType; index++) {
1900 					if (!strncasecmp(enclosure->name,
1901 							handlerTable[index].urlType, urlTypeLength)) {
1902 						handlerToLaunch = handlerTable[index].handler;
1903 						break;
1904 					}
1905 				}
1906 			}
1907 			if (handlerToLaunch) {
1908 				entry_ref appRef;
1909 				if (be_roster->FindApp(handlerToLaunch, &appRef) != B_OK)
1910 					handlerToLaunch = NULL;
1911 			}
1912 			if (!handlerToLaunch)
1913 				handlerToLaunch = "application/x-vnd.Be-Bookmark";
1914 
1915 			status_t result = be_roster->Launch(handlerToLaunch, 1, &enclosure->name);
1916 			if (result != B_NO_ERROR && result != B_ALREADY_RUNNING) {
1917 				beep();
1918 				(new BAlert("",
1919 					B_TRANSLATE("There is no installed handler for "
1920 						"URL links."),
1921 					"Sorry"))->Go();
1922 			}
1923 			break;
1924 		}
1925 
1926 		case TYPE_MAILTO:
1927 			if (be_roster->Launch(B_MAIL_TYPE, 1, &enclosure->name) < B_OK) {
1928 				char *argv[] = {(char *)"Mail", enclosure->name};
1929 				be_app->ArgvReceived(2, argv);
1930 			}
1931 			break;
1932 
1933 		case TYPE_ENCLOSURE:
1934 		case TYPE_BE_ENCLOSURE:
1935 			if (!enclosure->have_ref) {
1936 				BPath path;
1937 				if (find_directory(B_COMMON_TEMP_DIRECTORY, &path) == B_NO_ERROR) {
1938 					BDirectory dir(path.Path());
1939 					if (dir.InitCheck() == B_NO_ERROR) {
1940 						char name[B_FILE_NAME_LENGTH];
1941 						char baseName[B_FILE_NAME_LENGTH];
1942 						strcpy(baseName, enclosure->name ? enclosure->name : "enclosure");
1943 						strcpy(name, baseName);
1944 						for (int32 index = 0; dir.Contains(name); index++)
1945 							sprintf(name, "%s_%ld", baseName, index);
1946 
1947 						BEntry entry(path.Path());
1948 						entry_ref ref;
1949 						entry.GetRef(&ref);
1950 
1951 						BMessage save(M_SAVE);
1952 						save.AddRef("directory", &ref);
1953 						save.AddString("name", name);
1954 						save.AddPointer("enclosure", enclosure);
1955 						if (Save(&save) != B_NO_ERROR)
1956 							break;
1957 						enclosure->saved = false;
1958 					}
1959 				}
1960 			}
1961 
1962 			BMessenger tracker("application/x-vnd.Be-TRAK");
1963 			if (tracker.IsValid()) {
1964 				BMessage openMsg(B_REFS_RECEIVED);
1965 				openMsg.AddRef("refs", &enclosure->ref);
1966 				tracker.SendMessage(&openMsg);
1967 			}
1968 			break;
1969 	}
1970 }
1971 
1972 
1973 status_t
1974 TTextView::Save(BMessage *msg, bool makeNewFile)
1975 {
1976 	const char		*name;
1977 	entry_ref		ref;
1978 	BFile			file;
1979 	BPath			path;
1980 	hyper_text		*enclosure;
1981 	status_t		result = B_NO_ERROR;
1982 	char 			entry_name[B_FILE_NAME_LENGTH];
1983 
1984 	msg->FindString("name", &name);
1985 	msg->FindRef("directory", &ref);
1986 	msg->FindPointer("enclosure", (void **)&enclosure);
1987 
1988 	BDirectory dir;
1989 	dir.SetTo(&ref);
1990 	result = dir.InitCheck();
1991 
1992 	if (result == B_OK) {
1993 		if (makeNewFile) {
1994 			//
1995 			// Search for the file and delete it if it already exists.
1996 			// (It may not, that's ok.)
1997 			//
1998 			BEntry entry;
1999 			if (dir.FindEntry(name, &entry) == B_NO_ERROR)
2000 				entry.Remove();
2001 
2002 			if ((enclosure->have_ref) && (!enclosure->saved)) {
2003 				entry.SetTo(&enclosure->ref);
2004 
2005 				//
2006 				// Added true arg and entry_name so MoveTo clobbers as
2007 				// before. This may not be the correct behaviour, but
2008 				// it's the preserved behaviour.
2009 				//
2010 				entry.GetName(entry_name);
2011 				result = entry.MoveTo(&dir, entry_name, true);
2012 				if (result == B_NO_ERROR) {
2013 					entry.Rename(name);
2014 					entry.GetRef(&enclosure->ref);
2015 					entry.GetNodeRef(&enclosure->node);
2016 					enclosure->saved = true;
2017 					return result;
2018 				}
2019 			}
2020 
2021 			if (result == B_NO_ERROR) {
2022 				result = dir.CreateFile(name, &file);
2023 				if (result == B_NO_ERROR && enclosure->content_type) {
2024 					char type[B_MIME_TYPE_LENGTH];
2025 
2026 					if (!strcasecmp(enclosure->content_type, "message/rfc822"))
2027 						strcpy(type, "text/x-email");
2028 					else if (!strcasecmp(enclosure->content_type, "message/delivery-status"))
2029 						strcpy(type, "text/plain");
2030 					else
2031 						strcpy(type, enclosure->content_type);
2032 
2033 					BNodeInfo info(&file);
2034 					info.SetType(type);
2035 				}
2036 			}
2037 		} else {
2038 			//
2039 			// 	This file was dragged into the tracker or desktop.  The file
2040 			//	already exists.
2041 			//
2042 			result = file.SetTo(&dir, name, B_WRITE_ONLY);
2043 		}
2044 	}
2045 
2046 	if (enclosure->component == NULL)
2047 		result = B_ERROR;
2048 
2049 	if (result == B_NO_ERROR) {
2050 		//
2051 		// Write the data
2052 		//
2053 		enclosure->component->GetDecodedData(&file);
2054 
2055 		BEntry entry;
2056 		dir.FindEntry(name, &entry);
2057 		entry.GetRef(&enclosure->ref);
2058 		enclosure->have_ref = true;
2059 		enclosure->saved = true;
2060 		entry.GetPath(&path);
2061 		update_mime_info(path.Path(), false, true,
2062 			!cistrcmp("application/octet-stream", enclosure->content_type ? enclosure->content_type : B_EMPTY_STRING));
2063 		entry.GetNodeRef(&enclosure->node);
2064 		watch_node(&enclosure->node, B_WATCH_NAME, this);
2065 	}
2066 
2067 	if (result != B_NO_ERROR) {
2068 		beep();
2069 		(new BAlert("", B_TRANSLATE("An error occurred trying to save "
2070 				"the attachment."),
2071 			B_TRANSLATE("Sorry")))->Go();
2072 	}
2073 
2074 	return result;
2075 }
2076 
2077 
2078 void
2079 TTextView::StopLoad()
2080 {
2081 	Window()->Unlock();
2082 
2083 	thread_info	info;
2084 	if (fThread != 0 && get_thread_info(fThread, &info) == B_NO_ERROR) {
2085 		fStopLoading = true;
2086 		acquire_sem(fStopSem);
2087 		int32 result;
2088 		wait_for_thread(fThread, &result);
2089 		fThread = 0;
2090 		release_sem(fStopSem);
2091 		fStopLoading = false;
2092 	}
2093 
2094 	Window()->Lock();
2095 }
2096 
2097 
2098 bool
2099 TTextView::IsReaderThreadRunning()
2100 {
2101 	if (fThread == 0)
2102 		return false;
2103 
2104 	thread_info info;
2105 	for (int i = 5; i > 0; i--, usleep(100000))
2106 		if (get_thread_info(fThread, &info) != B_OK)
2107 			return false;
2108 	return true;
2109 }
2110 
2111 
2112 void
2113 TTextView::AddAsContent(BEmailMessage *mail, bool wrap, uint32 charset, mail_encoding encoding)
2114 {
2115 	if (mail == NULL)
2116 		return;
2117 
2118 	int32 textLength = TextLength();
2119 	const char *text = Text();
2120 
2121 	BTextMailComponent *body = mail->Body();
2122 	if (body == NULL) {
2123 		if (mail->SetBody(body = new BTextMailComponent()) < B_OK)
2124 			return;
2125 	}
2126 	body->SetEncoding(encoding, charset);
2127 
2128 	// Just add the text as a whole if we can, or ...
2129 	if (!wrap) {
2130 		body->AppendText(text);
2131 		return;
2132 	}
2133 
2134 	// ... do word wrapping.
2135 
2136 	BWindow	*window = Window();
2137 	char *saveText = strdup(text);
2138 	BRect saveTextRect = TextRect();
2139 
2140 	// do this before we start messing with the fonts
2141 	// the user will never know...
2142 	window->DisableUpdates();
2143 	Hide();
2144 	BScrollBar *vScroller = ScrollBar(B_VERTICAL);
2145 	BScrollBar *hScroller = ScrollBar(B_HORIZONTAL);
2146 	if (vScroller != NULL)
2147 		vScroller->SetTarget((BView *)NULL);
2148 	if (hScroller != NULL)
2149 		hScroller->SetTarget((BView *)NULL);
2150 
2151 	// Temporarily set the font to a fixed width font for line wrapping
2152 	// calculations.  If the font doesn't have as many of the symbols as
2153 	// the preferred font, go back to using the user's preferred font.
2154 
2155 	bool *boolArray;
2156 	int missingCharactersFixedWidth = 0;
2157 	int missingCharactersPreferredFont = 0;
2158 	int32 numberOfCharacters;
2159 
2160 	numberOfCharacters = BString(text).CountChars();
2161 	if (numberOfCharacters > 0
2162 		&& (boolArray = (bool *)malloc(sizeof(bool) * numberOfCharacters)) != NULL) {
2163 		memset(boolArray, 0, sizeof (bool) * numberOfCharacters);
2164 		be_fixed_font->GetHasGlyphs(text, numberOfCharacters, boolArray);
2165 		for (int i = 0; i < numberOfCharacters; i++) {
2166 			if (!boolArray[i])
2167 				missingCharactersFixedWidth += 1;
2168 		}
2169 
2170 		memset(boolArray, 0, sizeof (bool) * numberOfCharacters);
2171 		fFont.GetHasGlyphs(text, numberOfCharacters, boolArray);
2172 		for (int i = 0; i < numberOfCharacters; i++) {
2173 			if (!boolArray[i])
2174 				missingCharactersPreferredFont += 1;
2175 		}
2176 
2177 		free(boolArray);
2178 	}
2179 
2180 	if (missingCharactersFixedWidth > missingCharactersPreferredFont)
2181 		SetFontAndColor(0, textLength, &fFont);
2182 	else // All things being equal, the fixed font is better for wrapping.
2183 		SetFontAndColor(0, textLength, be_fixed_font);
2184 
2185 	// calculate a text rect that is 72 columns wide
2186 	BRect newTextRect = saveTextRect;
2187 	newTextRect.right = newTextRect.left + be_fixed_font->StringWidth("m") * 72;
2188 	SetTextRect(newTextRect);
2189 
2190 	// hard-wrap, based on TextView's soft-wrapping
2191 	int32 numLines = CountLines();
2192 	bool spaceMoved = false;
2193 	char *content = (char *)malloc(textLength + numLines * 72);	// more we'll ever need
2194 	if (content != NULL) {
2195 		int32 contentLength = 0;
2196 
2197 		for (int32 i = 0; i < numLines; i++) {
2198 			int32 startOffset = OffsetAt(i);
2199 			if (spaceMoved) {
2200 				startOffset++;
2201 				spaceMoved = false;
2202 			}
2203 			int32 endOffset = OffsetAt(i + 1);
2204 			int32 lineLength = endOffset - startOffset;
2205 
2206 			// quick hack to not break URLs into several parts
2207 			for (int32 pos = startOffset; pos < endOffset; pos++) {
2208 				size_t urlLength;
2209 				uint8 type = CheckForURL(text + pos, urlLength);
2210 				if (type != 0)
2211 					pos += urlLength;
2212 
2213 				if (pos > endOffset) {
2214 					// find first break character after the URL
2215 					for (; text[pos]; pos++) {
2216 						if (isalnum(text[pos]) || isspace(text[pos]))
2217 							break;
2218 					}
2219 					if (text[pos] && isspace(text[pos]) && text[pos] != '\n')
2220 						pos++;
2221 
2222 					endOffset += pos - endOffset;
2223 					lineLength = endOffset - startOffset;
2224 
2225 					// insert a newline (and the same number of quotes) after the
2226 					// URL to make sure the rest of the text is properly wrapped
2227 
2228 					char buffer[64];
2229 					if (text[pos] == '\n')
2230 						buffer[0] = '\0';
2231 					else
2232 						strcpy(buffer, "\n");
2233 
2234 					size_t quoteLength;
2235 					CopyQuotes(text + startOffset, lineLength, buffer + strlen(buffer), quoteLength);
2236 
2237 					Insert(pos, buffer, strlen(buffer));
2238 					numLines = CountLines();
2239 					text = Text();
2240 					i++;
2241 				}
2242 			}
2243 			if (text[endOffset - 1] != ' '
2244 				&& text[endOffset - 1] != '\n'
2245 				&& text[endOffset] == ' ') {
2246 				// make sure spaces will be part of this line
2247 				endOffset++;
2248 				lineLength++;
2249 				spaceMoved = true;
2250 			}
2251 
2252 			memcpy(content + contentLength, text + startOffset, lineLength);
2253 			contentLength += lineLength;
2254 
2255 			// add a newline to every line except for the ones
2256 			// that already end in newlines, and the last line
2257 			if ((text[endOffset - 1] != '\n') && (i < (numLines - 1))) {
2258 				content[contentLength++] = '\n';
2259 
2260 				// copy quote level of the first line
2261 				size_t quoteLength;
2262 				CopyQuotes(text + startOffset, lineLength, content + contentLength, quoteLength);
2263 				contentLength += quoteLength;
2264 			}
2265 		}
2266 		content[contentLength] = '\0';
2267 
2268 		body->AppendText(content);
2269 		free(content);
2270 	}
2271 
2272 	// reset the text rect and font
2273 	SetTextRect(saveTextRect);
2274 	SetText(saveText);
2275 	free(saveText);
2276 	SetFontAndColor(0, textLength, &fFont);
2277 
2278 	// should be OK to hook these back up now
2279 	if (vScroller != NULL)
2280 		vScroller->SetTarget(this);
2281 	if (hScroller != NULL)
2282 		hScroller->SetTarget(this);
2283 
2284 	Show();
2285 	window->EnableUpdates();
2286 }
2287 
2288 
2289 //	#pragma mark -
2290 
2291 
2292 TTextView::Reader::Reader(bool header, bool raw, bool quote, bool incoming,
2293 		bool stripHeader, bool mime, TTextView *view, BEmailMessage *mail,
2294 		BList *list, sem_id sem)
2295 	:
2296 	fHeader(header),
2297 	fRaw(raw),
2298 	fQuote(quote),
2299 	fIncoming(incoming),
2300 	fStripHeader(stripHeader),
2301 	fMime(mime),
2302 	fView(view),
2303 	fMail(mail),
2304 	fEnclosures(list),
2305 	fStopSem(sem)
2306 {
2307 }
2308 
2309 
2310 bool
2311 TTextView::Reader::ParseMail(BMailContainer *container,
2312 	BTextMailComponent *ignore)
2313 {
2314 	int32 count = 0;
2315 	for (int32 i = 0; i < container->CountComponents(); i++) {
2316 		if (fView->fStopLoading)
2317 			return false;
2318 
2319 		BMailComponent *component;
2320 		if ((component = container->GetComponent(i)) == NULL) {
2321 			if (fView->fStopLoading)
2322 				return false;
2323 
2324 			hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2325 			if (enclosure == NULL)
2326 				return false;
2327 
2328 			memset(enclosure, 0, sizeof(hyper_text));
2329 
2330 			enclosure->type = TYPE_ENCLOSURE;
2331 
2332 			const char *name = "\n<Attachment: could not handle>\n";
2333 
2334 			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
2335 			enclosure->text_start++;
2336 			enclosure->text_end += strlen(name) - 1;
2337 
2338 			Insert(name, strlen(name), true);
2339 			fEnclosures->AddItem(enclosure);
2340 			continue;
2341 		}
2342 
2343 		count++;
2344 		if (component == ignore)
2345 			continue;
2346 
2347 		if (component->ComponentType() == B_MAIL_MULTIPART_CONTAINER) {
2348 			BMIMEMultipartMailContainer *c = dynamic_cast<BMIMEMultipartMailContainer *>(container->GetComponent(i));
2349 			ASSERT(c != NULL);
2350 
2351 			if (!ParseMail(c, ignore))
2352 				count--;
2353 		} else if (fIncoming) {
2354 			hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2355 			if (enclosure == NULL)
2356 				return false;
2357 
2358 			memset(enclosure, 0, sizeof(hyper_text));
2359 
2360 			enclosure->type = TYPE_ENCLOSURE;
2361 			enclosure->component = component;
2362 
2363 			BString name;
2364 			char fileName[B_FILE_NAME_LENGTH];
2365 			strcpy(fileName, "untitled");
2366 			if (BMailAttachment *attachment = dynamic_cast <BMailAttachment *> (component))
2367 				attachment->FileName(fileName);
2368 
2369 			BPath path(fileName);
2370 			enclosure->name = strdup(path.Leaf());
2371 
2372 			BMimeType type;
2373 			component->MIMEType(&type);
2374 			enclosure->content_type = strdup(type.Type());
2375 
2376 			char typeDescription[B_MIME_TYPE_LENGTH];
2377 			if (type.GetShortDescription(typeDescription) != B_OK)
2378 				strcpy(typeDescription, type.Type() ? type.Type() : B_EMPTY_STRING);
2379 
2380 			name = "\n<";
2381 			name.Append(B_TRANSLATE_COMMENT("Enclosure: %name% (Type: %type%)",
2382 				"Don't translate the variables %name% and %type%."));
2383 			name.Append(">\n");
2384 			name.ReplaceFirst("%name%", enclosure->name);
2385 			name.ReplaceFirst("%type%", typeDescription);
2386 
2387 			fView->GetSelection(&enclosure->text_start, &enclosure->text_end);
2388 			enclosure->text_start++;
2389 			enclosure->text_end += strlen(name.String()) - 1;
2390 
2391 			Insert(name.String(), name.Length(), true);
2392 			fEnclosures->AddItem(enclosure);
2393 		}
2394 //			default:
2395 //			{
2396 //				PlainTextBodyComponent *body = dynamic_cast<PlainTextBodyComponent *>(container->GetComponent(i));
2397 //				const char *text;
2398 //				if (body && (text = body->Text()) != NULL)
2399 //					Insert(text, strlen(text), false);
2400 //			}
2401 	}
2402 	return count > 0;
2403 }
2404 
2405 
2406 bool
2407 TTextView::Reader::Process(const char *data, int32 data_len, bool isHeader)
2408 {
2409 	char line[522];
2410 	int32 count = 0;
2411 
2412 	for (int32 loop = 0; loop < data_len; loop++) {
2413 		if (fView->fStopLoading)
2414 			return false;
2415 
2416 		if (fQuote && (!loop || (loop && data[loop - 1] == '\n'))) {
2417 			strcpy(&line[count], QUOTE);
2418 			count += strlen(QUOTE);
2419 		}
2420 		if (!fRaw && fIncoming && (loop < data_len - 7)) {
2421 			size_t urlLength;
2422 			BString url;
2423 			uint8 type = CheckForURL(data + loop, urlLength, &url);
2424 
2425 			if (type) {
2426 				if (!Insert(line, count, false, isHeader))
2427 					return false;
2428 				count = 0;
2429 
2430 				hyper_text *enclosure = (hyper_text *)malloc(sizeof(hyper_text));
2431 				if (enclosure == NULL)
2432 					return false;
2433 
2434 				memset(enclosure, 0, sizeof(hyper_text));
2435 				fView->GetSelection(&enclosure->text_start,
2436 									&enclosure->text_end);
2437 				enclosure->type = type;
2438 				enclosure->name = strdup(url.String());
2439 				if (enclosure->name == NULL) {
2440 					free(enclosure);
2441 					return false;
2442 				}
2443 
2444 				Insert(&data[loop], urlLength, true, isHeader);
2445 				enclosure->text_end += urlLength;
2446 				loop += urlLength - 1;
2447 
2448 				fEnclosures->AddItem(enclosure);
2449 				continue;
2450 			}
2451 		}
2452 		if (!fRaw && fMime && data[loop] == '=') {
2453 			if ((loop) && (loop < data_len - 1) && (data[loop + 1] == '\r'))
2454 				loop += 2;
2455 			else
2456 				line[count++] = data[loop];
2457 		} else if (data[loop] != '\r')
2458 			line[count++] = data[loop];
2459 
2460 		if (count > 511 || (count && loop == data_len - 1)) {
2461 			if (!Insert(line, count, false, isHeader))
2462 				return false;
2463 			count = 0;
2464 		}
2465 	}
2466 	return true;
2467 }
2468 
2469 
2470 bool
2471 TTextView::Reader::Insert(const char *line, int32 count, bool isHyperLink,
2472 	bool isHeader)
2473 {
2474 	if (!count)
2475 		return true;
2476 
2477 	BFont font(fView->Font());
2478 	TextRunArray style(count / 8 + 8);
2479 
2480 	if (fView->fColoredQuotes && !isHeader && !isHyperLink) {
2481 		FillInQuoteTextRuns(fView, &fQuoteContext, line, count, font,
2482 			&style.Array(), style.MaxEntries());
2483 	} else {
2484 		text_run_array &array = style.Array();
2485 		array.count = 1;
2486 		array.runs[0].offset = 0;
2487 		if (isHeader) {
2488 			array.runs[0].color = isHyperLink ? kHyperLinkColor : kHeaderColor;
2489 			font.SetSize(font.Size() * 0.9);
2490 		} else {
2491 			array.runs[0].color = isHyperLink
2492 				? kHyperLinkColor : kNormalTextColor;
2493 		}
2494 		array.runs[0].font = font;
2495 	}
2496 
2497 	if (!fView->Window()->Lock())
2498 		return false;
2499 
2500 	fView->Insert(fView->TextLength(), line, count, &style.Array());
2501 
2502 	fView->Window()->Unlock();
2503 	return true;
2504 }
2505 
2506 
2507 status_t
2508 TTextView::Reader::Run(void *_this)
2509 {
2510 	Reader *reader = (Reader *)_this;
2511 	TTextView *view = reader->fView;
2512 	char *msg = NULL;
2513 	off_t size = 0;
2514 	int32 len = 0;
2515 
2516 	if (!reader->Lock())
2517 		return B_INTERRUPTED;
2518 
2519 	BFile *file = dynamic_cast<BFile *>(reader->fMail->Data());
2520 	if (file != NULL) {
2521 		len = header_len(file);
2522 
2523 		if (reader->fHeader)
2524 			size = len;
2525 		if (reader->fRaw || !reader->fMime)
2526 			file->GetSize(&size);
2527 
2528 		if (size != 0 && (msg = (char *)malloc(size)) == NULL)
2529 			goto done;
2530 		file->Seek(0, 0);
2531 
2532 		if (msg)
2533 			size = file->Read(msg, size);
2534 	}
2535 
2536 	// show the header?
2537 	if (reader->fHeader && len) {
2538 		// strip all headers except "From", "To", "Reply-To", "Subject", and "Date"
2539 	 	if (reader->fStripHeader) {
2540 		 	const char *header = msg;
2541 		 	char *buffer = NULL;
2542 
2543 			while (strncmp(header, "\r\n", 2)) {
2544 				const char *eol = header;
2545 				while ((eol = strstr(eol, "\r\n")) != NULL && isspace(eol[2]))
2546 					eol += 2;
2547 				if (eol == NULL)
2548 					break;
2549 
2550 				eol += 2;	// CR+LF belong to the line
2551 				size_t length = eol - header;
2552 
2553 		 		buffer = (char *)realloc(buffer, length + 1);
2554 		 		if (buffer == NULL)
2555 		 			goto done;
2556 
2557 		 		memcpy(buffer, header, length);
2558 
2559 				length = rfc2047_to_utf8(&buffer, &length, length);
2560 
2561 		 		if (!strncasecmp(header, "Reply-To: ", 10)
2562 		 			|| !strncasecmp(header, "To: ", 4)
2563 		 			|| !strncasecmp(header, "From: ", 6)
2564 		 			|| !strncasecmp(header, "Subject: ", 8)
2565 		 			|| !strncasecmp(header, "Date: ", 6))
2566 		 			reader->Process(buffer, length, true);
2567 
2568 		 		header = eol;
2569 	 		}
2570 		 	free(buffer);
2571 	 		reader->Process("\r\n", 2, true);
2572 	 	}
2573 	 	else if (!reader->Process(msg, len, true))
2574 			goto done;
2575 	}
2576 
2577 	if (reader->fRaw) {
2578 		if (!reader->Process((const char *)msg + len, size - len))
2579 			goto done;
2580 	} else {
2581 		//reader->fFile->Seek(0, 0);
2582 		//BEmailMessage *mail = new BEmailMessage(reader->fFile);
2583 		BEmailMessage *mail = reader->fMail;
2584 
2585 		// at first, insert the mail body
2586 		BTextMailComponent *body = NULL;
2587 		if (mail->BodyText() && !view->fStopLoading) {
2588 			char *bodyText = const_cast<char *>(mail->BodyText());
2589 			int32 bodyLength = strlen(bodyText);
2590 			body = mail->Body();
2591 			bool isHTML = false;
2592 
2593 			BMimeType type;
2594 			if (body->MIMEType(&type) == B_OK && type == "text/html") {
2595 				// strip out HTML tags
2596 				char *t = bodyText, *a, *end = bodyText + bodyLength;
2597 				bodyText = (char *)malloc(bodyLength + 1);
2598 				isHTML = true;
2599 
2600 				// TODO: is it correct to assume that the text is in Latin-1?
2601 				//		because if it isn't, the code below won't work correctly...
2602 
2603 				for (a = bodyText; t < end; t++) {
2604 					int32 c = *t;
2605 
2606 					// compact spaces
2607 					bool space = false;
2608 					while (c && (c == ' ' || c == '\t')) {
2609 						c = *(++t);
2610 						space = true;
2611 					}
2612 					if (space) {
2613 						c = ' ';
2614 						t--;
2615 					} else if (FilterHTMLTag(c, &t, end))	// the tag filter
2616 						continue;
2617 
2618 					Unicode2UTF8(c, &a);
2619 				}
2620 
2621 				*a = 0;
2622 				bodyLength = strlen(bodyText);
2623 				body = NULL;	// to add the HTML text as enclosure
2624 			}
2625 			if (!reader->Process(bodyText, bodyLength))
2626 				goto done;
2627 
2628 			if (isHTML)
2629 				free(bodyText);
2630 		}
2631 
2632 		if (!reader->ParseMail(mail, body))
2633 			goto done;
2634 
2635 		//reader->fView->fMail = mail;
2636 	}
2637 
2638 	if (!view->fStopLoading && view->Window()->Lock()) {
2639 		view->Select(0, 0);
2640 		view->MakeSelectable(true);
2641 		if (!reader->fIncoming)
2642 			view->MakeEditable(true);
2643 
2644 		view->Window()->Unlock();
2645 	}
2646 
2647 done:
2648 	// restore the reading position if available
2649 	view->Window()->PostMessage(M_READ_POS);
2650 
2651 	reader->Unlock();
2652 
2653 	delete reader;
2654 	free(msg);
2655 
2656 	return B_NO_ERROR;
2657 }
2658 
2659 
2660 status_t
2661 TTextView::Reader::Unlock()
2662 {
2663 	return release_sem(fStopSem);
2664 }
2665 
2666 
2667 bool
2668 TTextView::Reader::Lock()
2669 {
2670 	if (acquire_sem_etc(fStopSem, 1, B_TIMEOUT, 0) != B_NO_ERROR)
2671 		return false;
2672 
2673 	return true;
2674 }
2675 
2676 
2677 //====================================================================
2678 //	#pragma mark -
2679 
2680 
2681 TSavePanel::TSavePanel(hyper_text *enclosure, TTextView *view)
2682 	: BFilePanel(B_SAVE_PANEL)
2683 {
2684 	fEnclosure = enclosure;
2685 	fView = view;
2686 	if (enclosure->name)
2687 		SetSaveText(enclosure->name);
2688 }
2689 
2690 
2691 void
2692 TSavePanel::SendMessage(const BMessenger * /* messenger */, BMessage *msg)
2693 {
2694 	const char	*name = NULL;
2695 	BMessage	save(M_SAVE);
2696 	entry_ref	ref;
2697 
2698 	if ((!msg->FindRef("directory", &ref)) && (!msg->FindString("name", &name))) {
2699 		save.AddPointer("enclosure", fEnclosure);
2700 		save.AddString("name", name);
2701 		save.AddRef("directory", &ref);
2702 		fView->Window()->PostMessage(&save, fView);
2703 	}
2704 }
2705 
2706 
2707 void
2708 TSavePanel::SetEnclosure(hyper_text *enclosure)
2709 {
2710 	fEnclosure = enclosure;
2711 	if (enclosure->name)
2712 		SetSaveText(enclosure->name);
2713 	else
2714 		SetSaveText("");
2715 
2716 	if (!IsShowing())
2717 		Show();
2718 	Window()->Activate();
2719 }
2720 
2721 
2722 //--------------------------------------------------------------------
2723 //	#pragma mark -
2724 
2725 
2726 void
2727 TTextView::InsertText(const char *insertText, int32 length, int32 offset,
2728 	const text_run_array *runs)
2729 {
2730 	ContentChanged();
2731 
2732 	// Undo function
2733 
2734 	int32 cursorPos, dummy;
2735 	GetSelection(&cursorPos, &dummy);
2736 
2737 	if (fInputMethodUndoState.active) {
2738 		// IMアクティブ時は、一旦別のバッファへ記憶
2739 		fInputMethodUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
2740 		fInputMethodUndoState.replace = false;
2741 	} else {
2742 		if (fUndoState.replaced) {
2743 			fUndoBuffer.AddUndo(insertText, length, offset, K_REPLACED, cursorPos);
2744 		} else {
2745 			if (length == 1 && insertText[0] == 0x0a)
2746 				fUndoBuffer.MakeNewUndoItem();
2747 
2748 			fUndoBuffer.AddUndo(insertText, length, offset, K_INSERTED, cursorPos);
2749 
2750 			if (length == 1 && insertText[0] == 0x0a)
2751 				fUndoBuffer.MakeNewUndoItem();
2752 		}
2753 	}
2754 
2755 	fUndoState.replaced = false;
2756 	fUndoState.deleted = false;
2757 
2758 	struct text_runs : text_run_array { text_run _runs[1]; } style;
2759 	if (runs == NULL && IsEditable()) {
2760 		style.count = 1;
2761 		style.runs[0].offset = 0;
2762 		style.runs[0].font = fFont;
2763 		style.runs[0].color = kNormalTextColor;
2764 		runs = &style;
2765 	}
2766 
2767 	BTextView::InsertText(insertText, length, offset, runs);
2768 
2769 	if (fSpellCheck && IsEditable())
2770 	{
2771 		UpdateSpellMarks(offset, length);
2772 
2773 		rgb_color color;
2774 		GetFontAndColor(offset - 1, NULL, &color);
2775 		const char *text = Text();
2776 
2777 		if (length > 1
2778 			|| isalpha(text[offset + 1])
2779 			|| (!isalpha(text[offset]) && text[offset] != '\'')
2780 			|| (color.red == kSpellTextColor.red
2781 				&& color.green == kSpellTextColor.green
2782 				&& color.blue == kSpellTextColor.blue))
2783 		{
2784 			int32 start, end;
2785 			FindSpellBoundry(length, offset, &start, &end);
2786 
2787 			DSPELL(printf("Offset %ld, start %ld, end %ld\n", offset, start, end));
2788 			DSPELL(printf("\t\"%10.10s...\"\n", text + start));
2789 
2790 			CheckSpelling(start, end);
2791 		}
2792 	}
2793 }
2794 
2795 
2796 void
2797 TTextView::DeleteText(int32 start, int32 finish)
2798 {
2799 	ContentChanged();
2800 
2801 	// Undo function
2802 	int32 cursorPos, dummy;
2803 	GetSelection(&cursorPos, &dummy);
2804 	if (fInputMethodUndoState.active) {
2805 		if (fInputMethodUndoState.replace) {
2806 			fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
2807 			fInputMethodUndoState.replace = false;
2808 		} else {
2809 			fInputMethodUndoBuffer.AddUndo(&Text()[start], finish - start, start,
2810 				K_DELETED, cursorPos);
2811 		}
2812 	} else
2813 		fUndoBuffer.AddUndo(&Text()[start], finish - start, start, K_DELETED, cursorPos);
2814 
2815 	fUndoState.deleted = true;
2816 	fUndoState.replaced = true;
2817 
2818 	BTextView::DeleteText(start, finish);
2819 	if (fSpellCheck && IsEditable()) {
2820 		UpdateSpellMarks(start, start - finish);
2821 
2822 		int32 s, e;
2823 		FindSpellBoundry(1, start, &s, &e);
2824 		CheckSpelling(s, e);
2825 	}
2826 }
2827 
2828 
2829 void
2830 TTextView::ContentChanged(void)
2831 {
2832 	BLooper *looper = Looper();
2833 	if (looper == NULL)
2834 		return;
2835 
2836 	BMessage msg(FIELD_CHANGED);
2837 	msg.AddInt32("bitmask", FIELD_BODY);
2838 	msg.AddPointer("source", this);
2839 	looper->PostMessage(&msg);
2840 }
2841 
2842 
2843 void
2844 TTextView::CheckSpelling(int32 start, int32 end, int32 flags)
2845 {
2846 	const char 	*text = Text();
2847 	const char 	*next, *endPtr, *word = NULL;
2848 	int32 		wordLength = 0, wordOffset;
2849 	int32		nextHighlight = start;
2850 	BString 	testWord;
2851 	bool		isCap = false;
2852 	bool		isAlpha;
2853 	bool		isApost;
2854 
2855 	for (next = text + start, endPtr = text + end; next <= endPtr; next++) {
2856 		//printf("next=%c\n", *next);
2857 		// ToDo: this has to be refined to other languages...
2858 		// Alpha signifies the start of a word
2859 		isAlpha = isalpha(*next);
2860 		isApost = (*next == '\'');
2861 		if (!word && isAlpha) {
2862 			//printf("Found word start\n");
2863 			word = next;
2864 			wordLength++;
2865 			isCap = isupper(*word);
2866 		} else if (word && (isAlpha || isApost) && !(isApost && !isalpha(next[1]))
2867 					&& !(isCap && isApost && (next[1] == 's'))) {
2868 			// Word continues check
2869 			wordLength++;
2870 			//printf("Word continues...\n");
2871 		} else if (word) {
2872 			// End of word reached
2873 
2874 			//printf("Word End\n");
2875 			// Don't check single characters
2876 			if (wordLength > 1) {
2877 				bool isUpper = true;
2878 
2879 				// Look for all uppercase
2880 				for (int32 i = 0; i < wordLength; i++) {
2881 					if (word[i] == '\'')
2882 						break;
2883 
2884 					if (islower(word[i])) {
2885 						isUpper = false;
2886 						break;
2887 					}
2888 				}
2889 
2890 				// Don't check all uppercase words
2891 				if (!isUpper) {
2892 					bool foundMatch = false;
2893 					wordOffset = word - text;
2894 					testWord.SetTo(word, wordLength);
2895 
2896 					testWord = testWord.ToLower();
2897 					DSPELL(printf("Testing: \"%s\"\n", testWord.String()));
2898 
2899 					int32 key = -1;
2900 					if (gDictCount)
2901 						key = gExactWords[0]->GetKey(testWord.String());
2902 
2903 					// Search all dictionaries
2904 					for (int32 i = 0; i < gDictCount; i++) {
2905 						if (gExactWords[i]->Lookup(key) >= 0) {
2906 							foundMatch = true;
2907 							break;
2908 						}
2909 					}
2910 
2911 					if (!foundMatch) {
2912 						if (flags & S_CLEAR_ERRORS)
2913 							RemoveSpellMark(nextHighlight, wordOffset);
2914 
2915 						if (flags & S_SHOW_ERRORS)
2916 							AddSpellMark(wordOffset, wordOffset + wordLength);
2917 					} else if (flags & S_CLEAR_ERRORS)
2918 						RemoveSpellMark(nextHighlight, wordOffset + wordLength);
2919 
2920 					nextHighlight = wordOffset + wordLength;
2921 				}
2922 			}
2923 			// Reset state to looking for word
2924 			word = NULL;
2925 			wordLength = 0;
2926 		}
2927 	}
2928 
2929 	if (nextHighlight <= end
2930 		&& (flags & S_CLEAR_ERRORS) != 0
2931 		&& nextHighlight < TextLength())
2932 		SetFontAndColor(nextHighlight, end, NULL, B_FONT_ALL, &kNormalTextColor);
2933 }
2934 
2935 
2936 void
2937 TTextView::FindSpellBoundry(int32 length, int32 offset, int32 *_start, int32 *_end)
2938 {
2939 	int32 start, end, textLength;
2940 	const char *text = Text();
2941 	textLength = TextLength();
2942 
2943 	for (start = offset - 1; start >= 0
2944 		&& (isalpha(text[start]) || text[start] == '\''); start--) {}
2945 
2946 	start++;
2947 
2948 	for (end = offset + length; end < textLength
2949 		&& (isalpha(text[end]) || text[end] == '\''); end++) {}
2950 
2951 	*_start = start;
2952 	*_end = end;
2953 }
2954 
2955 
2956 TTextView::spell_mark *
2957 TTextView::FindSpellMark(int32 start, int32 end, spell_mark **_previousMark)
2958 {
2959 	spell_mark *lastMark = NULL;
2960 
2961 	for (spell_mark *spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
2962 		if (spellMark->start < end && spellMark->end > start) {
2963 			if (_previousMark)
2964 				*_previousMark = lastMark;
2965 			return spellMark;
2966 		}
2967 
2968 		lastMark = spellMark;
2969 	}
2970 	return NULL;
2971 }
2972 
2973 
2974 void
2975 TTextView::UpdateSpellMarks(int32 offset, int32 length)
2976 {
2977 	DSPELL(printf("UpdateSpellMarks: offset = %ld, length = %ld\n", offset, length));
2978 
2979 	spell_mark *spellMark;
2980 	for (spellMark = fFirstSpellMark; spellMark; spellMark = spellMark->next) {
2981 		DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));
2982 
2983 		if (spellMark->end < offset)
2984 			continue;
2985 
2986 		if (spellMark->start > offset)
2987 			spellMark->start += length;
2988 
2989 		spellMark->end += length;
2990 
2991 		DSPELL(printf("\t-> reset: %ld - %ld\n", spellMark->start, spellMark->end));
2992 	}
2993 }
2994 
2995 
2996 status_t
2997 TTextView::AddSpellMark(int32 start, int32 end)
2998 {
2999 	DSPELL(printf("AddSpellMark: start = %ld, end = %ld\n", start, end));
3000 
3001 	// check if there is already a mark for this passage
3002 	spell_mark *spellMark = FindSpellMark(start, end);
3003 	if (spellMark) {
3004 		if (spellMark->start == start && spellMark->end == end) {
3005 			DSPELL(printf("\tfound one\n"));
3006 			return B_OK;
3007 		}
3008 
3009 		DSPELL(printf("\tremove old one\n"));
3010 		RemoveSpellMark(start, end);
3011 	}
3012 
3013 	spellMark = (spell_mark *)malloc(sizeof(spell_mark));
3014 	if (spellMark == NULL)
3015 		return B_NO_MEMORY;
3016 
3017 	spellMark->start = start;
3018 	spellMark->end = end;
3019 	spellMark->style = RunArray(start, end);
3020 
3021 	// set the spell marks appearance
3022 	BFont font(fFont);
3023 	font.SetFace(B_BOLD_FACE | B_ITALIC_FACE);
3024 	SetFontAndColor(start, end, &font, B_FONT_ALL, &kSpellTextColor);
3025 
3026 	// add it to the queue
3027 	spellMark->next = fFirstSpellMark;
3028 	fFirstSpellMark = spellMark;
3029 
3030 	return B_OK;
3031 }
3032 
3033 
3034 bool
3035 TTextView::RemoveSpellMark(int32 start, int32 end)
3036 {
3037 	DSPELL(printf("RemoveSpellMark: start = %ld, end = %ld\n", start, end));
3038 
3039 	// find spell mark
3040 	spell_mark *lastMark = NULL;
3041 	spell_mark *spellMark = FindSpellMark(start, end, &lastMark);
3042 	if (spellMark == NULL) {
3043 		DSPELL(printf("\tnot found!\n"));
3044 		return false;
3045 	}
3046 
3047 	DSPELL(printf("\tfound: %ld - %ld\n", spellMark->start, spellMark->end));
3048 
3049 	// dequeue the spell mark
3050 	if (lastMark)
3051 		lastMark->next = spellMark->next;
3052 	else
3053 		fFirstSpellMark = spellMark->next;
3054 
3055 	if (spellMark->start < start)
3056 		start = spellMark->start;
3057 	if (spellMark->end > end)
3058 		end = spellMark->end;
3059 
3060 	// reset old text run array
3061 	SetRunArray(start, end, spellMark->style);
3062 
3063 	free(spellMark->style);
3064 	free(spellMark);
3065 
3066 	return true;
3067 }
3068 
3069 
3070 void
3071 TTextView::RemoveSpellMarks()
3072 {
3073 	spell_mark *spellMark, *nextMark;
3074 
3075 	for (spellMark = fFirstSpellMark; spellMark; spellMark = nextMark) {
3076 		nextMark = spellMark->next;
3077 
3078 		// reset old text run array
3079 		SetRunArray(spellMark->start, spellMark->end, spellMark->style);
3080 
3081 		free(spellMark->style);
3082 		free(spellMark);
3083 	}
3084 
3085 	fFirstSpellMark = NULL;
3086 }
3087 
3088 
3089 void
3090 TTextView::EnableSpellCheck(bool enable)
3091 {
3092 	if (fSpellCheck == enable)
3093 		return;
3094 
3095 	fSpellCheck = enable;
3096 	int32 textLength = TextLength();
3097 	if (fSpellCheck) {
3098 		// work-around for a bug in the BTextView class
3099 		// which causes lots of flicker
3100 		int32 start, end;
3101 		GetSelection(&start, &end);
3102 		if (start != end)
3103 			Select(start, start);
3104 
3105 		CheckSpelling(0, textLength);
3106 
3107 		if (start != end)
3108 			Select(start, end);
3109 	}
3110 	else
3111 		RemoveSpellMarks();
3112 }
3113 
3114 
3115 void
3116 TTextView::WindowActivated(bool flag)
3117 {
3118 	if (!flag) {
3119 		// WindowActivated(false) は、IM も Inactive になり、そのまま確定される。
3120 		// しかしこの場合、input_server が B_INPUT_METHOD_EVENT(B_INPUT_METHOD_STOPPED)
3121 		// を送ってこないまま矛盾してしまうので、やむを得ずここでつじつまあわせ処理している。
3122 		// OpenBeOSで修正されることを願って暫定処置としている。
3123 		fInputMethodUndoState.active = false;
3124 		// fInputMethodUndoBufferに溜まっている最後のデータがK_INSERTEDなら(確定)正規のバッファへ追加
3125 		if (fInputMethodUndoBuffer.CountItems() > 0) {
3126 			KUndoItem *item = fInputMethodUndoBuffer.ItemAt(fInputMethodUndoBuffer.CountItems() - 1);
3127 			if (item->History == K_INSERTED) {
3128 				fUndoBuffer.MakeNewUndoItem();
3129 				fUndoBuffer.AddUndo(item->RedoText, item->Length, item->Offset,
3130 					item->History, item->CursorPos);
3131 				fUndoBuffer.MakeNewUndoItem();
3132 			}
3133 			fInputMethodUndoBuffer.MakeEmpty();
3134 		}
3135 	}
3136 	BTextView::WindowActivated(flag);
3137 }
3138 
3139 
3140 void
3141 TTextView::AddQuote(int32 start, int32 finish)
3142 {
3143 	BRect rect = Bounds();
3144 
3145 	int32 lineStart;
3146 	GoToLine(CurrentLine());
3147 	GetSelection(&lineStart, &lineStart);
3148 
3149 	// make sure that we're changing the whole last line, too
3150 	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
3151 	{
3152 		const char *text = Text();
3153 		while (text[lineEnd] && text[lineEnd] != '\n')
3154 			lineEnd++;
3155 	}
3156 	Select(lineStart, lineEnd);
3157 
3158 	int32 textLength = lineEnd - lineStart;
3159 	char *text = (char *)malloc(textLength + 1);
3160 	if (text == NULL)
3161 		return;
3162 
3163 	GetText(lineStart, textLength, text);
3164 
3165 	int32 quoteLength = strlen(QUOTE);
3166 	int32 targetLength = 0;
3167 	char *target = NULL;
3168 	int32 lastLine = 0;
3169 
3170 	for (int32 index = 0; index < textLength; index++) {
3171 		if (text[index] == '\n' || index == textLength - 1) {
3172 			// add quote to this line
3173 			int32 lineLength = index - lastLine + 1;
3174 
3175 			target = (char *)realloc(target, targetLength + lineLength + quoteLength);
3176 			if (target == NULL) {
3177 				// free the old buffer?
3178 				free(text);
3179 				return;
3180 			}
3181 
3182 			// copy the quote sign
3183 			memcpy(&target[targetLength], QUOTE, quoteLength);
3184 			targetLength += quoteLength;
3185 
3186 			// copy the rest of the line
3187 			memcpy(&target[targetLength], &text[lastLine], lineLength);
3188 			targetLength += lineLength;
3189 
3190 			lastLine = index + 1;
3191 		}
3192 	}
3193 
3194 	// replace with quoted text
3195 	free(text);
3196 	Delete();
3197 
3198 	if (fColoredQuotes) {
3199 		const BFont *font = Font();
3200 		TextRunArray style(targetLength / 8 + 8);
3201 
3202 		FillInQuoteTextRuns(NULL, NULL, target, targetLength, font,
3203 			&style.Array(), style.MaxEntries());
3204 		Insert(target, targetLength, &style.Array());
3205 	} else
3206 		Insert(target, targetLength);
3207 
3208 	free(target);
3209 
3210 	// redo the old selection (compute the new start if necessary)
3211 	Select(start + quoteLength, finish + (targetLength - textLength));
3212 
3213 	ScrollTo(rect.LeftTop());
3214 }
3215 
3216 
3217 void
3218 TTextView::RemoveQuote(int32 start, int32 finish)
3219 {
3220 	BRect rect = Bounds();
3221 
3222 	GoToLine(CurrentLine());
3223 	int32 lineStart;
3224 	GetSelection(&lineStart, &lineStart);
3225 
3226 	// make sure that we're changing the whole last line, too
3227 	int32 lineEnd = finish > lineStart ? finish - 1 : finish;
3228 	const char *text = Text();
3229 	while (text[lineEnd] && text[lineEnd] != '\n')
3230 		lineEnd++;
3231 
3232 	Select(lineStart, lineEnd);
3233 
3234 	int32 length = lineEnd - lineStart;
3235 	char *target = (char *)malloc(length + 1);
3236 	if (target == NULL)
3237 		return;
3238 
3239 	int32 quoteLength = strlen(QUOTE);
3240 	int32 removed = 0;
3241 	text += lineStart;
3242 
3243 	for (int32 index = 0; index < length;) {
3244 		// find out the length of the current line
3245 		int32 lineLength = 0;
3246 		while (index + lineLength < length && text[lineLength] != '\n')
3247 			lineLength++;
3248 
3249 		// include the newline to be part of this line
3250 		if (text[lineLength] == '\n' && index + lineLength + 1 < length)
3251 			lineLength++;
3252 
3253 		if (!strncmp(text, QUOTE, quoteLength)) {
3254 			// remove quote
3255 			length -= quoteLength;
3256 			removed += quoteLength;
3257 
3258 			lineLength -= quoteLength;
3259 			text += quoteLength;
3260 		}
3261 
3262 		if (lineLength == 0) {
3263 			target[index] = '\0';
3264 			break;
3265 		}
3266 
3267 		memcpy(&target[index], text, lineLength);
3268 
3269 		text += lineLength;
3270 		index += lineLength;
3271 	}
3272 
3273 	if (removed) {
3274 		Delete();
3275 
3276 		if (fColoredQuotes) {
3277 			const BFont *font = Font();
3278 			TextRunArray style(length / 8 + 8);
3279 
3280 			FillInQuoteTextRuns(NULL, NULL, target, length, font,
3281 				&style.Array(), style.MaxEntries());
3282 			Insert(target, length, &style.Array());
3283 		} else
3284 			Insert(target, length);
3285 
3286 		// redo old selection
3287 		bool noSelection = start == finish;
3288 
3289 		if (start > lineStart + quoteLength)
3290 			start -= quoteLength;
3291 		else
3292 			start = lineStart;
3293 
3294 		if (noSelection)
3295 			finish = start;
3296 		else
3297 			finish -= removed;
3298 	}
3299 
3300 	free(target);
3301 
3302 	Select(start, finish);
3303 	ScrollTo(rect.LeftTop());
3304 }
3305 
3306 
3307 void
3308 TTextView::Undo(BClipboard */*clipboard*/)
3309 {
3310 	if (fInputMethodUndoState.active)
3311 		return;
3312 
3313 	int32 length, offset, cursorPos;
3314 	undo_type history;
3315 	char *text;
3316 	status_t status;
3317 
3318 	status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
3319 	if (status == B_OK) {
3320 		fUndoBuffer.Off();
3321 
3322 		switch (history) {
3323 			case K_INSERTED:
3324 				BTextView::Delete(offset, offset + length);
3325 				Select(offset, offset);
3326 				break;
3327 
3328 			case K_DELETED:
3329 				BTextView::Insert(offset, text, length);
3330 				Select(offset, offset + length);
3331 				break;
3332 
3333 			case K_REPLACED:
3334 				BTextView::Delete(offset, offset + length);
3335 				status = fUndoBuffer.Undo(&text, &length, &offset, &history, &cursorPos);
3336 				if (status == B_OK && history == K_DELETED) {
3337 					BTextView::Insert(offset, text, length);
3338 					Select(offset, offset + length);
3339 				} else {
3340 					::beep();
3341 					(new BAlert("",
3342 						B_TRANSLATE("Inconsistency occurred in the undo/redo "
3343 							"buffer."),
3344 						B_TRANSLATE("OK")))->Go();
3345 				}
3346 				break;
3347 		}
3348 		ScrollToSelection();
3349 		ContentChanged();
3350 		fUndoBuffer.On();
3351 	}
3352 }
3353 
3354 
3355 void
3356 TTextView::Redo()
3357 {
3358 	if (fInputMethodUndoState.active)
3359 		return;
3360 
3361 	int32 length, offset, cursorPos;
3362 	undo_type history;
3363 	char *text;
3364 	status_t status;
3365 	bool replaced;
3366 
3367 	status = fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
3368 	if (status == B_OK) {
3369 		fUndoBuffer.Off();
3370 
3371 		switch (history) {
3372 			case K_INSERTED:
3373 				BTextView::Insert(offset, text, length);
3374 				Select(offset, offset + length);
3375 				break;
3376 
3377 			case K_DELETED:
3378 				BTextView::Delete(offset, offset + length);
3379 				if (replaced) {
3380 					fUndoBuffer.Redo(&text, &length, &offset, &history, &cursorPos, &replaced);
3381 					BTextView::Insert(offset, text, length);
3382 				}
3383 				Select(offset, offset + length);
3384 				break;
3385 
3386 			case K_REPLACED:
3387 				::beep();
3388 				(new BAlert("",
3389 					B_TRANSLATE("Inconsistency occurred in the undo/redo "
3390 						"buffer."),
3391 					B_TRANSLATE("OK")))->Go();
3392 				break;
3393 		}
3394 		ScrollToSelection();
3395 		ContentChanged();
3396 		fUndoBuffer.On();
3397 	}
3398 }
3399 
3400