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