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