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