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