1 /* 2 * Copyright 2001-2015 Haiku, Inc. All rights reserved. 3 * Distributed under the terms of the MIT License. 4 * 5 * Authors: 6 * Stephan Aßmus, superstippi@gmx.de 7 * Stefano Ceccherini, stefano.ceccherini@gmail.com 8 * Marc Flerackers, mflerackers@androme.be 9 * Hiroshi Lockheimer (BTextView is based on his STEEngine) 10 * John Scipione, jscipione@gmail.com 11 * Oliver Tappe, zooey@hirschkaefer.de 12 */ 13 14 15 // TODOs: 16 // - Consider using BObjectList instead of BList 17 // for disallowed characters (it would remove a lot of reinterpret_casts) 18 // - Check for correctness and possible optimizations the calls to _Refresh(), 19 // to refresh only changed parts of text (currently we often redraw the whole 20 // text) 21 22 // Known Bugs: 23 // - Double buffering doesn't work well (disabled by default) 24 25 26 #include <TextView.h> 27 28 #include <new> 29 30 #include <stdio.h> 31 #include <stdlib.h> 32 33 #include <Application.h> 34 #include <Beep.h> 35 #include <Bitmap.h> 36 #include <Clipboard.h> 37 #include <Debug.h> 38 #include <Entry.h> 39 #include <Input.h> 40 #include <LayoutBuilder.h> 41 #include <LayoutUtils.h> 42 #include <MessageRunner.h> 43 #include <Path.h> 44 #include <PopUpMenu.h> 45 #include <PropertyInfo.h> 46 #include <Region.h> 47 #include <ScrollBar.h> 48 #include <SystemCatalog.h> 49 #include <Window.h> 50 51 #include <binary_compatibility/Interface.h> 52 53 #include "InlineInput.h" 54 #include "LineBuffer.h" 55 #include "StyleBuffer.h" 56 #include "TextGapBuffer.h" 57 #include "UndoBuffer.h" 58 #include "WidthBuffer.h" 59 60 61 using namespace std; 62 using BPrivate::gSystemCatalog; 63 64 65 #undef B_TRANSLATION_CONTEXT 66 #define B_TRANSLATION_CONTEXT "TextView" 67 68 69 #define TRANSLATE(str) \ 70 gSystemCatalog.GetString(B_TRANSLATE_MARK(str), "TextView") 71 72 #undef TRACE 73 #undef CALLED 74 //#define TRACE_TEXT_VIEW 75 #ifdef TRACE_TEXT_VIEW 76 # include <FunctionTracer.h> 77 static int32 sFunctionDepth = -1; 78 # define CALLED(x...) FunctionTracer _ft("BTextView", __FUNCTION__, \ 79 sFunctionDepth) 80 # define TRACE(x...) { BString _to; \ 81 _to.Append(' ', (sFunctionDepth + 1) * 2); \ 82 printf("%s", _to.String()); printf(x); } 83 #else 84 # define CALLED(x...) 85 # define TRACE(x...) 86 #endif 87 88 89 #define USE_WIDTHBUFFER 1 90 #define USE_DOUBLEBUFFERING 0 91 92 93 struct flattened_text_run { 94 int32 offset; 95 font_family family; 96 font_style style; 97 float size; 98 float shear; // typically 90.0 99 uint16 face; // typically 0 100 uint8 red; 101 uint8 green; 102 uint8 blue; 103 uint8 alpha; // 255 == opaque 104 uint16 _reserved_; // 0 105 }; 106 107 struct flattened_text_run_array { 108 uint32 magic; 109 uint32 version; 110 int32 count; 111 flattened_text_run styles[1]; 112 }; 113 114 static const uint32 kFlattenedTextRunArrayMagic = 'Ali!'; 115 static const uint32 kFlattenedTextRunArrayVersion = 0; 116 117 118 enum { 119 CHAR_CLASS_DEFAULT, 120 CHAR_CLASS_WHITESPACE, 121 CHAR_CLASS_GRAPHICAL, 122 CHAR_CLASS_QUOTE, 123 CHAR_CLASS_PUNCTUATION, 124 CHAR_CLASS_PARENS_OPEN, 125 CHAR_CLASS_PARENS_CLOSE, 126 CHAR_CLASS_END_OF_TEXT 127 }; 128 129 130 class BTextView::TextTrackState { 131 public: 132 TextTrackState(BMessenger messenger); 133 ~TextTrackState(); 134 135 void SimulateMouseMovement(BTextView* view); 136 137 public: 138 int32 clickOffset; 139 bool shiftDown; 140 BRect selectionRect; 141 BPoint where; 142 143 int32 anchor; 144 int32 selStart; 145 int32 selEnd; 146 147 private: 148 BMessageRunner* fRunner; 149 }; 150 151 152 struct BTextView::LayoutData { 153 LayoutData() 154 : leftInset(0), 155 topInset(0), 156 rightInset(0), 157 bottomInset(0), 158 valid(false) 159 { 160 } 161 162 void UpdateInsets(const BRect& bounds, const BRect& textRect) 163 { 164 // we disallow negative insets, as they would cause parts of the 165 // text to be hidden 166 leftInset = textRect.left >= bounds.left 167 ? textRect.left - bounds.left 168 : 0; 169 topInset = textRect.top >= bounds.top 170 ? textRect.top - bounds.top 171 : 0; 172 rightInset = bounds.right >= textRect.right 173 ? bounds.right - textRect.right 174 : leftInset; 175 bottomInset = bounds.bottom >= textRect.bottom 176 ? bounds.bottom - textRect.bottom 177 : topInset; 178 } 179 180 float leftInset; 181 float topInset; 182 float rightInset; 183 float bottomInset; 184 185 BSize min; 186 BSize preferred; 187 bool valid; 188 }; 189 190 191 static const rgb_color kBlackColor = { 0, 0, 0, 255 }; 192 static const rgb_color kBlueInputColor = { 152, 203, 255, 255 }; 193 static const rgb_color kRedInputColor = { 255, 152, 152, 255 }; 194 195 static const float kHorizontalScrollBarStep = 10.0; 196 static const float kVerticalScrollBarStep = 12.0; 197 198 static const int32 kMsgNavigateArrow = '_NvA'; 199 static const int32 kMsgNavigatePage = '_NvP'; 200 201 202 static property_info sPropertyList[] = { 203 { 204 "selection", 205 { B_GET_PROPERTY, 0 }, 206 { B_DIRECT_SPECIFIER, 0 }, 207 "Returns the current selection.", 0, 208 { B_INT32_TYPE, 0 } 209 }, 210 { 211 "selection", 212 { B_SET_PROPERTY, 0 }, 213 { B_DIRECT_SPECIFIER, 0 }, 214 "Sets the current selection.", 0, 215 { B_INT32_TYPE, 0 } 216 }, 217 { 218 "Text", 219 { B_COUNT_PROPERTIES, 0 }, 220 { B_DIRECT_SPECIFIER, 0 }, 221 "Returns the length of the text in bytes.", 0, 222 { B_INT32_TYPE, 0 } 223 }, 224 { 225 "Text", 226 { B_GET_PROPERTY, 0 }, 227 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 }, 228 "Returns the text in the specified range in the BTextView.", 0, 229 { B_STRING_TYPE, 0 } 230 }, 231 { 232 "Text", 233 { B_SET_PROPERTY, 0 }, 234 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 }, 235 "Removes or inserts text into the specified range in the BTextView.", 0, 236 { B_STRING_TYPE, 0 } 237 }, 238 { 239 "text_run_array", 240 { B_GET_PROPERTY, 0 }, 241 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 }, 242 "Returns the style information for the text in the specified range in " 243 "the BTextView.", 0, 244 { B_RAW_TYPE, 0 }, 245 }, 246 { 247 "text_run_array", 248 { B_SET_PROPERTY, 0 }, 249 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 }, 250 "Sets the style information for the text in the specified range in the " 251 "BTextView.", 0, 252 { B_RAW_TYPE, 0 }, 253 }, 254 255 { 0 } 256 }; 257 258 259 BTextView::BTextView(BRect frame, const char* name, BRect textRect, 260 uint32 resizeMask, uint32 flags) 261 : 262 BView(frame, name, resizeMask, 263 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE) 264 { 265 _InitObject(textRect, NULL, NULL); 266 } 267 268 269 BTextView::BTextView(BRect frame, const char* name, BRect textRect, 270 const BFont* initialFont, const rgb_color* initialColor, 271 uint32 resizeMask, uint32 flags) 272 : 273 BView(frame, name, resizeMask, 274 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE) 275 { 276 _InitObject(textRect, initialFont, initialColor); 277 } 278 279 280 BTextView::BTextView(const char* name, uint32 flags) 281 : 282 BView(name, 283 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE) 284 { 285 _InitObject(Bounds(), NULL, NULL); 286 } 287 288 289 BTextView::BTextView(const char* name, const BFont* initialFont, 290 const rgb_color* initialColor, uint32 flags) 291 : 292 BView(name, 293 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE) 294 { 295 _InitObject(Bounds(), initialFont, initialColor); 296 } 297 298 299 BTextView::BTextView(BMessage* archive) 300 : 301 BView(archive) 302 { 303 CALLED(); 304 BRect rect; 305 306 if (archive->FindRect("_trect", &rect) != B_OK) 307 rect.Set(0, 0, 0, 0); 308 309 _InitObject(rect, NULL, NULL); 310 311 const char* text = NULL; 312 if (archive->FindString("_text", &text) == B_OK) 313 SetText(text); 314 315 int32 flag, flag2; 316 if (archive->FindInt32("_align", &flag) == B_OK) 317 SetAlignment((alignment)flag); 318 319 float value; 320 321 if (archive->FindFloat("_tab", &value) == B_OK) 322 SetTabWidth(value); 323 324 if (archive->FindInt32("_col_sp", &flag) == B_OK) 325 SetColorSpace((color_space)flag); 326 327 if (archive->FindInt32("_max", &flag) == B_OK) 328 SetMaxBytes(flag); 329 330 if (archive->FindInt32("_sel", &flag) == B_OK && 331 archive->FindInt32("_sel", &flag2) == B_OK) 332 Select(flag, flag2); 333 334 bool toggle; 335 336 if (archive->FindBool("_stylable", &toggle) == B_OK) 337 SetStylable(toggle); 338 339 if (archive->FindBool("_auto_in", &toggle) == B_OK) 340 SetAutoindent(toggle); 341 342 if (archive->FindBool("_wrap", &toggle) == B_OK) 343 SetWordWrap(toggle); 344 345 if (archive->FindBool("_nsel", &toggle) == B_OK) 346 MakeSelectable(!toggle); 347 348 if (archive->FindBool("_nedit", &toggle) == B_OK) 349 MakeEditable(!toggle); 350 351 ssize_t disallowedCount = 0; 352 const int32* disallowedChars = NULL; 353 if (archive->FindData("_dis_ch", B_RAW_TYPE, 354 (const void**)&disallowedChars, &disallowedCount) == B_OK) { 355 356 fDisallowedChars = new BList; 357 disallowedCount /= sizeof(int32); 358 for (int32 x = 0; x < disallowedCount; x++) { 359 fDisallowedChars->AddItem( 360 reinterpret_cast<void*>(disallowedChars[x])); 361 } 362 } 363 364 ssize_t runSize = 0; 365 const void* flattenedRun = NULL; 366 367 if (archive->FindData("_runs", B_RAW_TYPE, &flattenedRun, &runSize) 368 == B_OK) { 369 text_run_array* runArray = UnflattenRunArray(flattenedRun, 370 (int32*)&runSize); 371 if (runArray) { 372 SetRunArray(0, TextLength(), runArray); 373 FreeRunArray(runArray); 374 } 375 } 376 } 377 378 379 BTextView::~BTextView() 380 { 381 _CancelInputMethod(); 382 _StopMouseTracking(); 383 _DeleteOffscreen(); 384 385 delete fText; 386 delete fLines; 387 delete fStyles; 388 delete fDisallowedChars; 389 delete fUndo; 390 delete fClickRunner; 391 delete fDragRunner; 392 delete fLayoutData; 393 } 394 395 396 BArchivable* 397 BTextView::Instantiate(BMessage* archive) 398 { 399 CALLED(); 400 if (validate_instantiation(archive, "BTextView")) 401 return new BTextView(archive); 402 return NULL; 403 } 404 405 406 status_t 407 BTextView::Archive(BMessage* data, bool deep) const 408 { 409 CALLED(); 410 status_t err = BView::Archive(data, deep); 411 if (err == B_OK) 412 err = data->AddString("_text", Text()); 413 if (err == B_OK) 414 err = data->AddInt32("_align", fAlignment); 415 if (err == B_OK) 416 err = data->AddFloat("_tab", fTabWidth); 417 if (err == B_OK) 418 err = data->AddInt32("_col_sp", fColorSpace); 419 if (err == B_OK) 420 err = data->AddRect("_trect", fTextRect); 421 if (err == B_OK) 422 err = data->AddInt32("_max", fMaxBytes); 423 if (err == B_OK) 424 err = data->AddInt32("_sel", fSelStart); 425 if (err == B_OK) 426 err = data->AddInt32("_sel", fSelEnd); 427 if (err == B_OK) 428 err = data->AddBool("_stylable", fStylable); 429 if (err == B_OK) 430 err = data->AddBool("_auto_in", fAutoindent); 431 if (err == B_OK) 432 err = data->AddBool("_wrap", fWrap); 433 if (err == B_OK) 434 err = data->AddBool("_nsel", !fSelectable); 435 if (err == B_OK) 436 err = data->AddBool("_nedit", !fEditable); 437 438 if (err == B_OK && fDisallowedChars != NULL && fDisallowedChars->CountItems() > 0) { 439 err = data->AddData("_dis_ch", B_RAW_TYPE, fDisallowedChars->Items(), 440 fDisallowedChars->CountItems() * sizeof(int32)); 441 } 442 443 if (err == B_OK) { 444 int32 runSize = 0; 445 text_run_array* runArray = RunArray(0, TextLength()); 446 447 void* flattened = FlattenRunArray(runArray, &runSize); 448 if (flattened != NULL) { 449 data->AddData("_runs", B_RAW_TYPE, flattened, runSize); 450 free(flattened); 451 } else 452 err = B_NO_MEMORY; 453 454 FreeRunArray(runArray); 455 } 456 457 return err; 458 } 459 460 461 void 462 BTextView::AttachedToWindow() 463 { 464 BView::AttachedToWindow(); 465 466 SetDrawingMode(B_OP_COPY); 467 468 Window()->SetPulseRate(500000); 469 470 fCaretVisible = false; 471 fCaretTime = 0; 472 fClickCount = 0; 473 fClickTime = 0; 474 fDragOffset = -1; 475 fActive = false; 476 477 _AutoResize(true); 478 479 _UpdateScrollbars(); 480 481 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); 482 } 483 484 485 void 486 BTextView::DetachedFromWindow() 487 { 488 BView::DetachedFromWindow(); 489 } 490 491 492 void 493 BTextView::Draw(BRect updateRect) 494 { 495 // what lines need to be drawn? 496 int32 startLine = _LineAt(BPoint(0.0, updateRect.top)); 497 int32 endLine = _LineAt(BPoint(0.0, updateRect.bottom)); 498 499 _DrawLines(startLine, endLine, -1, true); 500 } 501 502 503 void 504 BTextView::MouseDown(BPoint where) 505 { 506 // should we even bother? 507 if (!fEditable && !fSelectable) 508 return; 509 510 _CancelInputMethod(); 511 512 if (!IsFocus()) 513 MakeFocus(); 514 515 _HideCaret(); 516 517 _StopMouseTracking(); 518 519 int32 modifiers = 0; 520 uint32 buttons = 0; 521 BMessage* currentMessage = Window()->CurrentMessage(); 522 if (currentMessage != NULL) { 523 currentMessage->FindInt32("modifiers", &modifiers); 524 currentMessage->FindInt32("buttons", (int32*)&buttons); 525 } 526 527 if (buttons == B_SECONDARY_MOUSE_BUTTON) { 528 _ShowContextMenu(where); 529 return; 530 } 531 532 BMessenger messenger(this); 533 fTrackingMouse = new (nothrow) TextTrackState(messenger); 534 if (fTrackingMouse == NULL) 535 return; 536 537 fTrackingMouse->clickOffset = OffsetAt(where); 538 fTrackingMouse->shiftDown = modifiers & B_SHIFT_KEY; 539 fTrackingMouse->where = where; 540 541 bigtime_t clickTime = system_time(); 542 bigtime_t clickSpeed = 0; 543 get_click_speed(&clickSpeed); 544 bool multipleClick 545 = clickTime - fClickTime < clickSpeed 546 && fLastClickOffset == fTrackingMouse->clickOffset; 547 548 fWhere = where; 549 550 SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, 551 B_LOCK_WINDOW_FOCUS | B_NO_POINTER_HISTORY); 552 553 if (fSelStart != fSelEnd && !fTrackingMouse->shiftDown && !multipleClick) { 554 BRegion region; 555 GetTextRegion(fSelStart, fSelEnd, ®ion); 556 if (region.Contains(where)) { 557 // Setup things for dragging 558 fTrackingMouse->selectionRect = region.Frame(); 559 fClickCount = 1; 560 fClickTime = clickTime; 561 fLastClickOffset = OffsetAt(where); 562 return; 563 } 564 } 565 566 if (multipleClick) { 567 if (fClickCount > 3) { 568 fClickCount = 0; 569 fClickTime = 0; 570 } else { 571 fClickCount++; 572 fClickTime = clickTime; 573 } 574 } else if (!fTrackingMouse->shiftDown) { 575 // If no multiple click yet and shift is not pressed, this is an 576 // independent first click somewhere into the textview - we initialize 577 // the corresponding members for handling potential multiple clicks: 578 fLastClickOffset = fCaretOffset = fTrackingMouse->clickOffset; 579 fClickCount = 1; 580 fClickTime = clickTime; 581 582 // Deselect any previously selected text 583 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset); 584 } 585 586 if (fClickTime == clickTime) { 587 BMessage message(_PING_); 588 message.AddInt64("clickTime", clickTime); 589 delete fClickRunner; 590 591 BMessenger messenger(this); 592 fClickRunner = new (nothrow) BMessageRunner(messenger, &message, 593 clickSpeed, 1); 594 } 595 596 if (!fSelectable) { 597 _StopMouseTracking(); 598 return; 599 } 600 601 int32 offset = fSelStart; 602 if (fTrackingMouse->clickOffset > fSelStart) 603 offset = fSelEnd; 604 605 fTrackingMouse->anchor = offset; 606 607 MouseMoved(where, B_INSIDE_VIEW, NULL); 608 } 609 610 611 void 612 BTextView::MouseUp(BPoint where) 613 { 614 BView::MouseUp(where); 615 _PerformMouseUp(where); 616 617 delete fDragRunner; 618 fDragRunner = NULL; 619 } 620 621 622 void 623 BTextView::MouseMoved(BPoint where, uint32 code, const BMessage* dragMessage) 624 { 625 // check if it's a "click'n'move" 626 if (_PerformMouseMoved(where, code)) 627 return; 628 629 switch (code) { 630 case B_ENTERED_VIEW: 631 case B_INSIDE_VIEW: 632 _TrackMouse(where, dragMessage, true); 633 break; 634 635 case B_EXITED_VIEW: 636 _DragCaret(-1); 637 if (Window()->IsActive() && dragMessage == NULL) 638 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); 639 break; 640 641 default: 642 BView::MouseMoved(where, code, dragMessage); 643 } 644 } 645 646 647 void 648 BTextView::WindowActivated(bool active) 649 { 650 BView::WindowActivated(active); 651 652 if (active && IsFocus()) { 653 if (!fActive) 654 _Activate(); 655 } else { 656 if (fActive) 657 _Deactivate(); 658 } 659 660 BPoint where; 661 uint32 buttons; 662 GetMouse(&where, &buttons, false); 663 664 if (Bounds().Contains(where)) 665 _TrackMouse(where, NULL); 666 } 667 668 669 void 670 BTextView::KeyDown(const char* bytes, int32 numBytes) 671 { 672 const char keyPressed = bytes[0]; 673 674 if (!fEditable) { 675 // only arrow and page keys are allowed 676 // (no need to hide the cursor) 677 switch (keyPressed) { 678 case B_LEFT_ARROW: 679 case B_RIGHT_ARROW: 680 case B_UP_ARROW: 681 case B_DOWN_ARROW: 682 _HandleArrowKey(keyPressed); 683 break; 684 685 case B_HOME: 686 case B_END: 687 case B_PAGE_UP: 688 case B_PAGE_DOWN: 689 _HandlePageKey(keyPressed); 690 break; 691 692 default: 693 BView::KeyDown(bytes, numBytes); 694 break; 695 } 696 697 return; 698 } 699 700 // hide the cursor and caret 701 if (IsFocus()) 702 be_app->ObscureCursor(); 703 _HideCaret(); 704 705 switch (keyPressed) { 706 case B_BACKSPACE: 707 _HandleBackspace(); 708 break; 709 710 case B_LEFT_ARROW: 711 case B_RIGHT_ARROW: 712 case B_UP_ARROW: 713 case B_DOWN_ARROW: 714 _HandleArrowKey(keyPressed); 715 break; 716 717 case B_DELETE: 718 _HandleDelete(); 719 break; 720 721 case B_HOME: 722 case B_END: 723 case B_PAGE_UP: 724 case B_PAGE_DOWN: 725 _HandlePageKey(keyPressed); 726 break; 727 728 case B_ESCAPE: 729 case B_INSERT: 730 case B_FUNCTION_KEY: 731 // ignore, pass it up to superclass 732 BView::KeyDown(bytes, numBytes); 733 break; 734 735 default: 736 // bail out if the character is not allowed 737 if (fDisallowedChars 738 && fDisallowedChars->HasItem( 739 reinterpret_cast<void*>((uint32)keyPressed))) { 740 beep(); 741 return; 742 } 743 744 _HandleAlphaKey(bytes, numBytes); 745 break; 746 } 747 748 // draw the caret 749 if (fSelStart == fSelEnd) 750 _ShowCaret(); 751 } 752 753 754 void 755 BTextView::Pulse() 756 { 757 if (fActive && fEditable && fSelStart == fSelEnd) { 758 if (system_time() > (fCaretTime + 500000.0)) 759 _InvertCaret(); 760 } 761 } 762 763 764 void 765 BTextView::FrameResized(float newWidth, float newHeight) 766 { 767 BView::FrameResized(newWidth, newHeight); 768 _UpdateScrollbars(); 769 } 770 771 772 void 773 BTextView::MakeFocus(bool focus) 774 { 775 BView::MakeFocus(focus); 776 777 if (focus && Window() != NULL && Window()->IsActive()) { 778 if (!fActive) 779 _Activate(); 780 } else { 781 if (fActive) 782 _Deactivate(); 783 } 784 } 785 786 787 void 788 BTextView::MessageReceived(BMessage* message) 789 { 790 // ToDo: block input if not editable (Andrew) 791 792 // was this message dropped? 793 if (message->WasDropped()) { 794 BPoint dropOffset; 795 BPoint dropPoint = message->DropPoint(&dropOffset); 796 ConvertFromScreen(&dropPoint); 797 ConvertFromScreen(&dropOffset); 798 if (!_MessageDropped(message, dropPoint, dropOffset)) 799 BView::MessageReceived(message); 800 801 return; 802 } 803 804 switch (message->what) { 805 case B_CUT: 806 if (!IsTypingHidden()) 807 Cut(be_clipboard); 808 else 809 beep(); 810 break; 811 812 case B_COPY: 813 if (!IsTypingHidden()) 814 Copy(be_clipboard); 815 else 816 beep(); 817 break; 818 819 case B_PASTE: 820 Paste(be_clipboard); 821 break; 822 823 case B_UNDO: 824 Undo(be_clipboard); 825 break; 826 827 case B_SELECT_ALL: 828 SelectAll(); 829 break; 830 831 case B_INPUT_METHOD_EVENT: 832 { 833 int32 opcode; 834 if (message->FindInt32("be:opcode", &opcode) == B_OK) { 835 switch (opcode) { 836 case B_INPUT_METHOD_STARTED: 837 { 838 BMessenger messenger; 839 if (message->FindMessenger("be:reply_to", &messenger) 840 == B_OK) { 841 ASSERT(fInline == NULL); 842 fInline = new InlineInput(messenger); 843 } 844 break; 845 } 846 847 case B_INPUT_METHOD_STOPPED: 848 delete fInline; 849 fInline = NULL; 850 break; 851 852 case B_INPUT_METHOD_CHANGED: 853 if (fInline != NULL) 854 _HandleInputMethodChanged(message); 855 break; 856 857 case B_INPUT_METHOD_LOCATION_REQUEST: 858 if (fInline != NULL) 859 _HandleInputMethodLocationRequest(); 860 break; 861 862 default: 863 break; 864 } 865 } 866 break; 867 } 868 869 case B_SET_PROPERTY: 870 case B_GET_PROPERTY: 871 case B_COUNT_PROPERTIES: 872 { 873 BPropertyInfo propInfo(sPropertyList); 874 BMessage specifier; 875 const char* property; 876 877 if (message->GetCurrentSpecifier(NULL, &specifier) < B_OK 878 || specifier.FindString("property", &property) < B_OK) 879 return; 880 881 if (propInfo.FindMatch(message, 0, &specifier, specifier.what, 882 property) < B_OK) { 883 BView::MessageReceived(message); 884 break; 885 } 886 887 BMessage reply; 888 bool handled = false; 889 switch(message->what) { 890 case B_GET_PROPERTY: 891 handled = _GetProperty(&specifier, specifier.what, property, 892 &reply); 893 break; 894 895 case B_SET_PROPERTY: 896 handled = _SetProperty(&specifier, specifier.what, property, 897 &reply); 898 break; 899 900 case B_COUNT_PROPERTIES: 901 handled = _CountProperties(&specifier, specifier.what, 902 property, &reply); 903 break; 904 905 default: 906 break; 907 } 908 if (handled) 909 message->SendReply(&reply); 910 else 911 BView::MessageReceived(message); 912 break; 913 } 914 915 case _PING_: 916 { 917 if (message->HasInt64("clickTime")) { 918 bigtime_t clickTime; 919 message->FindInt64("clickTime", &clickTime); 920 if (clickTime == fClickTime) { 921 if (fSelStart != fSelEnd && fSelectable) { 922 BRegion region; 923 GetTextRegion(fSelStart, fSelEnd, ®ion); 924 if (region.Contains(fWhere)) 925 _TrackMouse(fWhere, NULL); 926 } 927 delete fClickRunner; 928 fClickRunner = NULL; 929 } 930 } else if (fTrackingMouse) { 931 fTrackingMouse->SimulateMouseMovement(this); 932 _PerformAutoScrolling(); 933 } 934 break; 935 } 936 937 case _DISPOSE_DRAG_: 938 if (fEditable) 939 _TrackDrag(fWhere); 940 break; 941 942 case kMsgNavigateArrow: 943 { 944 int32 key = message->GetInt32("key", 0); 945 int32 modifiers = message->GetInt32("modifiers", 0); 946 _HandleArrowKey(key, modifiers); 947 break; 948 } 949 950 case kMsgNavigatePage: 951 { 952 int32 key = message->GetInt32("key", 0); 953 int32 modifiers = message->GetInt32("modifiers", 0); 954 _HandlePageKey(key, modifiers); 955 break; 956 } 957 958 default: 959 BView::MessageReceived(message); 960 break; 961 } 962 } 963 964 965 BHandler* 966 BTextView::ResolveSpecifier(BMessage* message, int32 index, BMessage* specifier, 967 int32 what, const char* property) 968 { 969 BPropertyInfo propInfo(sPropertyList); 970 BHandler* target = this; 971 972 if (propInfo.FindMatch(message, index, specifier, what, property) < B_OK) { 973 target = BView::ResolveSpecifier(message, index, specifier, what, 974 property); 975 } 976 977 return target; 978 } 979 980 981 status_t 982 BTextView::GetSupportedSuites(BMessage* data) 983 { 984 if (data == NULL) 985 return B_BAD_VALUE; 986 987 status_t err = data->AddString("suites", "suite/vnd.Be-text-view"); 988 if (err != B_OK) 989 return err; 990 991 BPropertyInfo prop_info(sPropertyList); 992 err = data->AddFlat("messages", &prop_info); 993 994 if (err != B_OK) 995 return err; 996 return BView::GetSupportedSuites(data); 997 } 998 999 1000 status_t 1001 BTextView::Perform(perform_code code, void* _data) 1002 { 1003 switch (code) { 1004 case PERFORM_CODE_MIN_SIZE: 1005 ((perform_data_min_size*)_data)->return_value 1006 = BTextView::MinSize(); 1007 return B_OK; 1008 case PERFORM_CODE_MAX_SIZE: 1009 ((perform_data_max_size*)_data)->return_value 1010 = BTextView::MaxSize(); 1011 return B_OK; 1012 case PERFORM_CODE_PREFERRED_SIZE: 1013 ((perform_data_preferred_size*)_data)->return_value 1014 = BTextView::PreferredSize(); 1015 return B_OK; 1016 case PERFORM_CODE_LAYOUT_ALIGNMENT: 1017 ((perform_data_layout_alignment*)_data)->return_value 1018 = BTextView::LayoutAlignment(); 1019 return B_OK; 1020 case PERFORM_CODE_HAS_HEIGHT_FOR_WIDTH: 1021 ((perform_data_has_height_for_width*)_data)->return_value 1022 = BTextView::HasHeightForWidth(); 1023 return B_OK; 1024 case PERFORM_CODE_GET_HEIGHT_FOR_WIDTH: 1025 { 1026 perform_data_get_height_for_width* data 1027 = (perform_data_get_height_for_width*)_data; 1028 BTextView::GetHeightForWidth(data->width, &data->min, &data->max, 1029 &data->preferred); 1030 return B_OK; 1031 } 1032 case PERFORM_CODE_SET_LAYOUT: 1033 { 1034 perform_data_set_layout* data = (perform_data_set_layout*)_data; 1035 BTextView::SetLayout(data->layout); 1036 return B_OK; 1037 } 1038 case PERFORM_CODE_LAYOUT_INVALIDATED: 1039 { 1040 perform_data_layout_invalidated* data 1041 = (perform_data_layout_invalidated*)_data; 1042 BTextView::LayoutInvalidated(data->descendants); 1043 return B_OK; 1044 } 1045 case PERFORM_CODE_DO_LAYOUT: 1046 { 1047 BTextView::DoLayout(); 1048 return B_OK; 1049 } 1050 } 1051 1052 return BView::Perform(code, _data); 1053 } 1054 1055 1056 void 1057 BTextView::SetText(const char* text, const text_run_array* runs) 1058 { 1059 SetText(text, text ? strlen(text) : 0, runs); 1060 } 1061 1062 1063 void 1064 BTextView::SetText(const char* text, int32 length, const text_run_array* runs) 1065 { 1066 _CancelInputMethod(); 1067 1068 // hide the caret/unhighlight the selection 1069 if (fActive) { 1070 if (fSelStart != fSelEnd) { 1071 if (fSelectable) 1072 Highlight(fSelStart, fSelEnd); 1073 } else 1074 _HideCaret(); 1075 } 1076 1077 // remove data from buffer 1078 if (fText->Length() > 0) 1079 DeleteText(0, fText->Length()); 1080 1081 if (text != NULL && length > 0) 1082 InsertText(text, length, 0, runs); 1083 1084 // recalculate line breaks and draw the text 1085 _Refresh(0, length, false); 1086 fCaretOffset = fSelStart = fSelEnd = 0; 1087 ScrollTo(B_ORIGIN); 1088 1089 // draw the caret 1090 _ShowCaret(); 1091 } 1092 1093 1094 void 1095 BTextView::SetText(BFile* file, int32 offset, int32 length, 1096 const text_run_array* runs) 1097 { 1098 CALLED(); 1099 1100 _CancelInputMethod(); 1101 1102 if (file == NULL) 1103 return; 1104 1105 if (fText->Length() > 0) 1106 DeleteText(0, fText->Length()); 1107 1108 if (!fText->InsertText(file, offset, length, 0)) 1109 return; 1110 1111 // update the start offsets of each line below offset 1112 fLines->BumpOffset(length, _LineAt(offset) + 1); 1113 1114 // update the style runs 1115 fStyles->BumpOffset(length, fStyles->OffsetToRun(offset - 1) + 1); 1116 1117 if (fStylable && runs != NULL) 1118 SetRunArray(offset, offset + length, runs); 1119 else { 1120 // apply null-style to inserted text 1121 _ApplyStyleRange(offset, offset + length); 1122 } 1123 1124 // recalculate line breaks and draw the text 1125 _Refresh(0, length, false); 1126 fCaretOffset = fSelStart = fSelEnd = 0; 1127 ScrollToOffset(fSelStart); 1128 1129 // draw the caret 1130 _ShowCaret(); 1131 } 1132 1133 1134 void 1135 BTextView::Insert(const char* text, const text_run_array* runs) 1136 { 1137 if (text != NULL) 1138 _DoInsertText(text, strlen(text), fSelStart, runs); 1139 } 1140 1141 1142 void 1143 BTextView::Insert(const char* text, int32 length, const text_run_array* runs) 1144 { 1145 if (text != NULL && length > 0) 1146 _DoInsertText(text, strnlen(text, length), fSelStart, runs); 1147 } 1148 1149 1150 void 1151 BTextView::Insert(int32 offset, const char* text, int32 length, 1152 const text_run_array* runs) 1153 { 1154 // pin offset at reasonable values 1155 if (offset < 0) 1156 offset = 0; 1157 else if (offset > fText->Length()) 1158 offset = fText->Length(); 1159 1160 if (text != NULL && length > 0) 1161 _DoInsertText(text, strnlen(text, length), offset, runs); 1162 } 1163 1164 1165 void 1166 BTextView::Delete() 1167 { 1168 Delete(fSelStart, fSelEnd); 1169 } 1170 1171 1172 void 1173 BTextView::Delete(int32 startOffset, int32 endOffset) 1174 { 1175 CALLED(); 1176 1177 // pin offsets at reasonable values 1178 if (startOffset < 0) 1179 startOffset = 0; 1180 else if (startOffset > fText->Length()) 1181 startOffset = fText->Length(); 1182 if (endOffset < 0) 1183 endOffset = 0; 1184 else if (endOffset > fText->Length()) 1185 endOffset = fText->Length(); 1186 1187 // anything to delete? 1188 if (startOffset == endOffset) 1189 return; 1190 1191 // hide the caret/unhighlight the selection 1192 if (fActive) { 1193 if (fSelStart != fSelEnd) { 1194 if (fSelectable) 1195 Highlight(fSelStart, fSelEnd); 1196 } else 1197 _HideCaret(); 1198 } 1199 // remove data from buffer 1200 DeleteText(startOffset, endOffset); 1201 1202 // check if the caret needs to be moved 1203 if (fCaretOffset >= endOffset) 1204 fCaretOffset -= (endOffset - startOffset); 1205 else if (fCaretOffset >= startOffset && fCaretOffset < endOffset) 1206 fCaretOffset = startOffset; 1207 1208 fSelEnd = fSelStart = fCaretOffset; 1209 1210 // recalculate line breaks and draw what's left 1211 _Refresh(startOffset, endOffset, false); 1212 1213 // draw the caret 1214 _ShowCaret(); 1215 } 1216 1217 1218 const char* 1219 BTextView::Text() const 1220 { 1221 return fText->RealText(); 1222 } 1223 1224 1225 int32 1226 BTextView::TextLength() const 1227 { 1228 return fText->Length(); 1229 } 1230 1231 1232 void 1233 BTextView::GetText(int32 offset, int32 length, char* buffer) const 1234 { 1235 if (buffer != NULL) 1236 fText->GetString(offset, length, buffer); 1237 } 1238 1239 1240 uchar 1241 BTextView::ByteAt(int32 offset) const 1242 { 1243 if (offset < 0 || offset >= fText->Length()) 1244 return '\0'; 1245 1246 return fText->RealCharAt(offset); 1247 } 1248 1249 1250 int32 1251 BTextView::CountLines() const 1252 { 1253 return fLines->NumLines(); 1254 } 1255 1256 1257 int32 1258 BTextView::CurrentLine() const 1259 { 1260 return LineAt(fSelStart); 1261 } 1262 1263 1264 void 1265 BTextView::GoToLine(int32 index) 1266 { 1267 _CancelInputMethod(); 1268 _HideCaret(); 1269 fSelStart = fSelEnd = fCaretOffset = OffsetAt(index); 1270 _ShowCaret(); 1271 } 1272 1273 1274 void 1275 BTextView::Cut(BClipboard* clipboard) 1276 { 1277 _CancelInputMethod(); 1278 if (!fEditable) 1279 return; 1280 if (fUndo) { 1281 delete fUndo; 1282 fUndo = new CutUndoBuffer(this); 1283 } 1284 Copy(clipboard); 1285 Delete(); 1286 } 1287 1288 1289 void 1290 BTextView::Copy(BClipboard* clipboard) 1291 { 1292 _CancelInputMethod(); 1293 1294 if (clipboard->Lock()) { 1295 clipboard->Clear(); 1296 1297 BMessage* clip = clipboard->Data(); 1298 if (clip != NULL) { 1299 int32 numBytes = fSelEnd - fSelStart; 1300 const char* text = fText->GetString(fSelStart, &numBytes); 1301 clip->AddData("text/plain", B_MIME_TYPE, text, numBytes); 1302 1303 int32 size; 1304 if (fStylable) { 1305 text_run_array* runArray = RunArray(fSelStart, fSelEnd, &size); 1306 clip->AddData("application/x-vnd.Be-text_run_array", 1307 B_MIME_TYPE, runArray, size); 1308 FreeRunArray(runArray); 1309 } 1310 clipboard->Commit(); 1311 } 1312 clipboard->Unlock(); 1313 } 1314 } 1315 1316 1317 void 1318 BTextView::Paste(BClipboard* clipboard) 1319 { 1320 CALLED(); 1321 _CancelInputMethod(); 1322 1323 if (!fEditable || !clipboard->Lock()) 1324 return; 1325 1326 BMessage* clip = clipboard->Data(); 1327 if (clip != NULL) { 1328 const char* text = NULL; 1329 ssize_t length = 0; 1330 1331 if (clip->FindData("text/plain", B_MIME_TYPE, 1332 (const void**)&text, &length) == B_OK) { 1333 text_run_array* runArray = NULL; 1334 ssize_t runLength = 0; 1335 1336 if (fStylable) { 1337 clip->FindData("application/x-vnd.Be-text_run_array", 1338 B_MIME_TYPE, (const void**)&runArray, &runLength); 1339 } 1340 1341 _FilterDisallowedChars((char*)text, length, runArray); 1342 1343 if (length < 1) { 1344 beep(); 1345 clipboard->Unlock(); 1346 return; 1347 } 1348 1349 if (fUndo) { 1350 delete fUndo; 1351 fUndo = new PasteUndoBuffer(this, text, length, runArray, 1352 runLength); 1353 } 1354 1355 if (fSelStart != fSelEnd) 1356 Delete(); 1357 1358 Insert(text, length, runArray); 1359 ScrollToOffset(fSelEnd); 1360 } 1361 } 1362 1363 clipboard->Unlock(); 1364 } 1365 1366 1367 void 1368 BTextView::Clear() 1369 { 1370 // We always check for fUndo != NULL (not only here), 1371 // because when fUndo is NULL, undo is deactivated. 1372 if (fUndo) { 1373 delete fUndo; 1374 fUndo = new ClearUndoBuffer(this); 1375 } 1376 1377 Delete(); 1378 } 1379 1380 1381 bool 1382 BTextView::AcceptsPaste(BClipboard* clipboard) 1383 { 1384 bool result = false; 1385 1386 if (fEditable && clipboard && clipboard->Lock()) { 1387 BMessage* data = clipboard->Data(); 1388 result = data && data->HasData("text/plain", B_MIME_TYPE); 1389 clipboard->Unlock(); 1390 } 1391 1392 return result; 1393 } 1394 1395 1396 bool 1397 BTextView::AcceptsDrop(const BMessage* message) 1398 { 1399 return fEditable && message 1400 && message->HasData("text/plain", B_MIME_TYPE); 1401 } 1402 1403 1404 void 1405 BTextView::Select(int32 startOffset, int32 endOffset) 1406 { 1407 CALLED(); 1408 if (!fSelectable) 1409 return; 1410 1411 _CancelInputMethod(); 1412 1413 // pin offsets at reasonable values 1414 if (startOffset < 0) 1415 startOffset = 0; 1416 else if (startOffset > fText->Length()) 1417 startOffset = fText->Length(); 1418 if (endOffset < 0) 1419 endOffset = 0; 1420 else if (endOffset > fText->Length()) 1421 endOffset = fText->Length(); 1422 1423 // a negative selection? 1424 if (startOffset > endOffset) 1425 return; 1426 1427 // is the new selection any different from the current selection? 1428 if (startOffset == fSelStart && endOffset == fSelEnd) 1429 return; 1430 1431 fStyles->InvalidateNullStyle(); 1432 1433 _HideCaret(); 1434 1435 if (startOffset == endOffset) { 1436 if (fSelStart != fSelEnd) { 1437 // unhilite the selection 1438 if (fActive) 1439 Highlight(fSelStart, fSelEnd); 1440 } 1441 fSelStart = fSelEnd = fCaretOffset = startOffset; 1442 _ShowCaret(); 1443 } else { 1444 if (fActive) { 1445 // draw only those ranges that are different 1446 long start, end; 1447 if (startOffset != fSelStart) { 1448 // start of selection has changed 1449 if (startOffset > fSelStart) { 1450 start = fSelStart; 1451 end = startOffset; 1452 } else { 1453 start = startOffset; 1454 end = fSelStart; 1455 } 1456 Highlight(start, end); 1457 } 1458 1459 if (endOffset != fSelEnd) { 1460 // end of selection has changed 1461 if (endOffset > fSelEnd) { 1462 start = fSelEnd; 1463 end = endOffset; 1464 } else { 1465 start = endOffset; 1466 end = fSelEnd; 1467 } 1468 Highlight(start, end); 1469 } 1470 } 1471 fSelStart = startOffset; 1472 fSelEnd = endOffset; 1473 } 1474 } 1475 1476 1477 void 1478 BTextView::SelectAll() 1479 { 1480 Select(0, fText->Length()); 1481 } 1482 1483 1484 void 1485 BTextView::GetSelection(int32* _start, int32* _end) const 1486 { 1487 int32 start = 0; 1488 int32 end = 0; 1489 1490 if (fSelectable) { 1491 start = fSelStart; 1492 end = fSelEnd; 1493 } 1494 1495 if (_start) 1496 *_start = start; 1497 1498 if (_end) 1499 *_end = end; 1500 } 1501 1502 1503 void 1504 BTextView::SetFontAndColor(const BFont* font, uint32 mode, 1505 const rgb_color* color) 1506 { 1507 SetFontAndColor(fSelStart, fSelEnd, font, mode, color); 1508 } 1509 1510 1511 void 1512 BTextView::SetFontAndColor(int32 startOffset, int32 endOffset, 1513 const BFont* font, uint32 mode, const rgb_color* color) 1514 { 1515 CALLED(); 1516 1517 _HideCaret(); 1518 1519 const int32 textLength = fText->Length(); 1520 1521 if (!fStylable) { 1522 // When the text view is not stylable, we always set the whole text's 1523 // style and ignore the offsets 1524 startOffset = 0; 1525 endOffset = textLength; 1526 } else { 1527 // pin offsets at reasonable values 1528 if (startOffset < 0) 1529 startOffset = 0; 1530 else if (startOffset > textLength) 1531 startOffset = textLength; 1532 1533 if (endOffset < 0) 1534 endOffset = 0; 1535 else if (endOffset > textLength) 1536 endOffset = textLength; 1537 } 1538 1539 // apply the style to the style buffer 1540 fStyles->InvalidateNullStyle(); 1541 _ApplyStyleRange(startOffset, endOffset, mode, font, color); 1542 1543 if ((mode & (B_FONT_FAMILY_AND_STYLE | B_FONT_SIZE)) != 0) { 1544 // ToDo: maybe only invalidate the layout (depending on 1545 // B_SUPPORTS_LAYOUT) and have it _Refresh() automatically? 1546 InvalidateLayout(); 1547 // recalc the line breaks and redraw with new style 1548 _Refresh(startOffset, endOffset, false); 1549 } else { 1550 // the line breaks wont change, simply redraw 1551 _RequestDrawLines(_LineAt(startOffset), _LineAt(endOffset)); 1552 } 1553 1554 _ShowCaret(); 1555 } 1556 1557 1558 void 1559 BTextView::GetFontAndColor(int32 offset, BFont* _font, 1560 rgb_color* _color) const 1561 { 1562 fStyles->GetStyle(offset, _font, _color); 1563 } 1564 1565 1566 void 1567 BTextView::GetFontAndColor(BFont* _font, uint32* _mode, 1568 rgb_color* _color, bool* _sameColor) const 1569 { 1570 fStyles->ContinuousGetStyle(_font, _mode, _color, _sameColor, 1571 fSelStart, fSelEnd); 1572 } 1573 1574 1575 void 1576 BTextView::SetRunArray(int32 startOffset, int32 endOffset, 1577 const text_run_array* runs) 1578 { 1579 CALLED(); 1580 1581 _CancelInputMethod(); 1582 1583 text_run_array oneRun; 1584 1585 if (!fStylable) { 1586 // when the text view is not stylable, we always set the whole text's 1587 // style with the first run and ignore the offsets 1588 if (runs->count == 0) 1589 return; 1590 1591 startOffset = 0; 1592 endOffset = fText->Length(); 1593 oneRun.count = 1; 1594 oneRun.runs[0] = runs->runs[0]; 1595 oneRun.runs[0].offset = 0; 1596 runs = &oneRun; 1597 } else { 1598 // pin offsets at reasonable values 1599 if (startOffset < 0) 1600 startOffset = 0; 1601 else if (startOffset > fText->Length()) 1602 startOffset = fText->Length(); 1603 1604 if (endOffset < 0) 1605 endOffset = 0; 1606 else if (endOffset > fText->Length()) 1607 endOffset = fText->Length(); 1608 } 1609 1610 _SetRunArray(startOffset, endOffset, runs); 1611 1612 _Refresh(startOffset, endOffset, false); 1613 } 1614 1615 1616 text_run_array* 1617 BTextView::RunArray(int32 startOffset, int32 endOffset, int32* _size) const 1618 { 1619 // pin offsets at reasonable values 1620 if (startOffset < 0) 1621 startOffset = 0; 1622 else if (startOffset > fText->Length()) 1623 startOffset = fText->Length(); 1624 1625 if (endOffset < 0) 1626 endOffset = 0; 1627 else if (endOffset > fText->Length()) 1628 endOffset = fText->Length(); 1629 1630 STEStyleRange* styleRange 1631 = fStyles->GetStyleRange(startOffset, endOffset - 1); 1632 if (styleRange == NULL) 1633 return NULL; 1634 1635 text_run_array* runArray = AllocRunArray(styleRange->count, _size); 1636 if (runArray != NULL) { 1637 for (int32 i = 0; i < runArray->count; i++) { 1638 runArray->runs[i].offset = styleRange->runs[i].offset; 1639 runArray->runs[i].font = styleRange->runs[i].style.font; 1640 runArray->runs[i].color = styleRange->runs[i].style.color; 1641 } 1642 } 1643 1644 free(styleRange); 1645 1646 return runArray; 1647 } 1648 1649 1650 int32 1651 BTextView::LineAt(int32 offset) const 1652 { 1653 // pin offset at reasonable values 1654 if (offset < 0) 1655 offset = 0; 1656 else if (offset > fText->Length()) 1657 offset = fText->Length(); 1658 1659 int32 lineNum = _LineAt(offset); 1660 if (_IsOnEmptyLastLine(offset)) 1661 lineNum++; 1662 return lineNum; 1663 } 1664 1665 1666 int32 1667 BTextView::LineAt(BPoint point) const 1668 { 1669 int32 lineNum = _LineAt(point); 1670 if ((*fLines)[lineNum + 1]->origin <= point.y - fTextRect.top) 1671 lineNum++; 1672 1673 return lineNum; 1674 } 1675 1676 1677 BPoint 1678 BTextView::PointAt(int32 offset, float* _height) const 1679 { 1680 // pin offset at reasonable values 1681 if (offset < 0) 1682 offset = 0; 1683 else if (offset > fText->Length()) 1684 offset = fText->Length(); 1685 1686 // ToDo: Cleanup. 1687 int32 lineNum = _LineAt(offset); 1688 STELine* line = (*fLines)[lineNum]; 1689 float height = 0; 1690 1691 BPoint result; 1692 result.x = 0.0; 1693 result.y = line->origin + fTextRect.top; 1694 1695 bool onEmptyLastLine = _IsOnEmptyLastLine(offset); 1696 1697 if (fStyles->NumRuns() == 0) { 1698 // Handle the case where there is only one line (no text inserted) 1699 fStyles->SyncNullStyle(0); 1700 height = _NullStyleHeight(); 1701 } else { 1702 height = (line + 1)->origin - line->origin; 1703 1704 if (onEmptyLastLine) { 1705 // special case: go down one line if offset is at the newline 1706 // at the end of the buffer ... 1707 result.y += height; 1708 // ... and return the height of that (empty) line 1709 fStyles->SyncNullStyle(offset); 1710 height = _NullStyleHeight(); 1711 } else { 1712 int32 length = offset - line->offset; 1713 result.x += _TabExpandedStyledWidth(line->offset, length); 1714 } 1715 } 1716 1717 if (fAlignment != B_ALIGN_LEFT) { 1718 float lineWidth = onEmptyLastLine ? 0.0 : LineWidth(lineNum); 1719 float alignmentOffset = fTextRect.Width() - lineWidth; 1720 if (fAlignment == B_ALIGN_CENTER) 1721 alignmentOffset /= 2; 1722 result.x += alignmentOffset; 1723 } 1724 1725 // convert from text rect coordinates 1726 result.x += fTextRect.left; 1727 1728 // round up 1729 result.x = lroundf(result.x); 1730 result.y = lroundf(result.y); 1731 if (_height != NULL) 1732 *_height = height; 1733 1734 return result; 1735 } 1736 1737 1738 int32 1739 BTextView::OffsetAt(BPoint point) const 1740 { 1741 const int32 textLength = fText->Length(); 1742 1743 // should we even bother? 1744 if (point.y >= fTextRect.bottom) 1745 return textLength; 1746 else if (point.y < fTextRect.top) 1747 return 0; 1748 1749 int32 lineNum = _LineAt(point); 1750 STELine* line = (*fLines)[lineNum]; 1751 1752 #define COMPILE_PROBABLY_BAD_CODE 1 1753 1754 #if COMPILE_PROBABLY_BAD_CODE 1755 // special case: if point is within the text rect and PixelToLine() 1756 // tells us that it's on the last line, but if point is actually 1757 // lower than the bottom of the last line, return the last offset 1758 // (can happen for newlines) 1759 if (lineNum == (fLines->NumLines() - 1)) { 1760 if (point.y >= ((line + 1)->origin + fTextRect.top)) 1761 return textLength; 1762 } 1763 #endif 1764 1765 // convert to text rect coordinates 1766 if (fAlignment != B_ALIGN_LEFT) { 1767 float alignmentOffset = fTextRect.Width() - LineWidth(lineNum); 1768 if (fAlignment == B_ALIGN_CENTER) 1769 alignmentOffset /= 2; 1770 point.x -= alignmentOffset; 1771 } 1772 1773 point.x -= fTextRect.left; 1774 point.x = max_c(point.x, 0.0); 1775 1776 // ToDo: The following code isn't very efficient, because it always starts 1777 // from the left end, so when the point is near the right end it's very 1778 // slow. 1779 int32 offset = line->offset; 1780 const int32 limit = (line + 1)->offset; 1781 float location = 0; 1782 do { 1783 const int32 nextInitial = _NextInitialByte(offset); 1784 const int32 saveOffset = offset; 1785 float width = 0; 1786 if (ByteAt(offset) == B_TAB) 1787 width = _ActualTabWidth(location); 1788 else 1789 width = _StyledWidth(saveOffset, nextInitial - saveOffset); 1790 if (location + width > point.x) { 1791 if (fabs(location + width - point.x) < fabs(location - point.x)) 1792 offset = nextInitial; 1793 break; 1794 } 1795 1796 location += width; 1797 offset = nextInitial; 1798 } while (offset < limit); 1799 1800 if (offset == (line + 1)->offset) { 1801 // special case: newlines aren't visible 1802 // return the offset of the character preceding the newline 1803 if (ByteAt(offset - 1) == B_ENTER) 1804 return --offset; 1805 1806 // special case: return the offset preceding any spaces that 1807 // aren't at the end of the buffer 1808 if (offset != textLength && ByteAt(offset - 1) == B_SPACE) 1809 return --offset; 1810 } 1811 1812 return offset; 1813 } 1814 1815 1816 int32 1817 BTextView::OffsetAt(int32 line) const 1818 { 1819 if (line < 0) 1820 return 0; 1821 1822 if (line > fLines->NumLines()) 1823 return fText->Length(); 1824 1825 return (*fLines)[line]->offset; 1826 } 1827 1828 1829 void 1830 BTextView::FindWord(int32 offset, int32* _fromOffset, int32* _toOffset) 1831 { 1832 if (offset < 0) { 1833 if (_fromOffset) 1834 *_fromOffset = 0; 1835 1836 if (_toOffset) 1837 *_toOffset = 0; 1838 1839 return; 1840 } 1841 1842 if (offset > fText->Length()) { 1843 if (_fromOffset) 1844 *_fromOffset = fText->Length(); 1845 1846 if (_toOffset) 1847 *_toOffset = fText->Length(); 1848 1849 return; 1850 } 1851 1852 if (_fromOffset) 1853 *_fromOffset = _PreviousWordBoundary(offset); 1854 1855 if (_toOffset) 1856 *_toOffset = _NextWordBoundary(offset); 1857 } 1858 1859 1860 bool 1861 BTextView::CanEndLine(int32 offset) 1862 { 1863 if (offset < 0 || offset > fText->Length()) 1864 return false; 1865 1866 // TODO: This should be improved using the LocaleKit. 1867 uint32 classification = _CharClassification(offset); 1868 1869 // wrapping is always allowed at end of text and at newlines 1870 if (classification == CHAR_CLASS_END_OF_TEXT || ByteAt(offset) == B_ENTER) 1871 return true; 1872 1873 uint32 nextClassification = _CharClassification(offset + 1); 1874 if (nextClassification == CHAR_CLASS_END_OF_TEXT) 1875 return true; 1876 1877 // never separate a punctuation char from its preceeding word 1878 if (classification == CHAR_CLASS_DEFAULT 1879 && nextClassification == CHAR_CLASS_PUNCTUATION) { 1880 return false; 1881 } 1882 1883 if ((classification == CHAR_CLASS_WHITESPACE 1884 && nextClassification != CHAR_CLASS_WHITESPACE) 1885 || (classification != CHAR_CLASS_WHITESPACE 1886 && nextClassification == CHAR_CLASS_WHITESPACE)) { 1887 return true; 1888 } 1889 1890 // allow wrapping after whitespace, unless more whitespace (except for 1891 // newline) follows 1892 if (classification == CHAR_CLASS_WHITESPACE 1893 && (nextClassification != CHAR_CLASS_WHITESPACE 1894 || ByteAt(offset + 1) == B_ENTER)) { 1895 return true; 1896 } 1897 1898 // allow wrapping after punctuation chars, unless more punctuation, closing 1899 // parens or quotes follow 1900 if (classification == CHAR_CLASS_PUNCTUATION 1901 && nextClassification != CHAR_CLASS_PUNCTUATION 1902 && nextClassification != CHAR_CLASS_PARENS_CLOSE 1903 && nextClassification != CHAR_CLASS_QUOTE) { 1904 return true; 1905 } 1906 1907 // allow wrapping after quotes, graphical chars and closing parens only if 1908 // whitespace follows (not perfect, but seems to do the right thing most 1909 // of the time) 1910 if ((classification == CHAR_CLASS_QUOTE 1911 || classification == CHAR_CLASS_GRAPHICAL 1912 || classification == CHAR_CLASS_PARENS_CLOSE) 1913 && nextClassification == CHAR_CLASS_WHITESPACE) { 1914 return true; 1915 } 1916 1917 return false; 1918 } 1919 1920 1921 float 1922 BTextView::LineWidth(int32 lineNumber) const 1923 { 1924 if (lineNumber < 0 || lineNumber >= fLines->NumLines()) 1925 return 0; 1926 1927 STELine* line = (*fLines)[lineNumber]; 1928 int32 length = (line + 1)->offset - line->offset; 1929 1930 // skip newline at the end of the line, if any, as it does no contribute 1931 // to the width 1932 if (ByteAt((line + 1)->offset - 1) == B_ENTER) 1933 length--; 1934 1935 return _TabExpandedStyledWidth(line->offset, length); 1936 } 1937 1938 1939 float 1940 BTextView::LineHeight(int32 lineNumber) const 1941 { 1942 float lineHeight = TextHeight(lineNumber, lineNumber); 1943 if (lineHeight == 0.0) { 1944 // We probably don't have text content yet. Take the initial 1945 // style's font height or fall back to the plain font. 1946 const BFont* font; 1947 fStyles->GetNullStyle(&font, NULL); 1948 if (font == NULL) 1949 font = be_plain_font; 1950 1951 font_height fontHeight; 1952 font->GetHeight(&fontHeight); 1953 // This is how the height is calculated in _RecalculateLineBreaks(). 1954 lineHeight = ceilf(fontHeight.ascent + fontHeight.descent) + 1; 1955 } 1956 1957 return lineHeight; 1958 } 1959 1960 1961 float 1962 BTextView::TextHeight(int32 startLine, int32 endLine) const 1963 { 1964 const int32 numLines = fLines->NumLines(); 1965 if (startLine < 0) 1966 startLine = 0; 1967 else if (startLine > numLines - 1) 1968 startLine = numLines - 1; 1969 1970 if (endLine < 0) 1971 endLine = 0; 1972 else if (endLine > numLines - 1) 1973 endLine = numLines - 1; 1974 1975 float height = (*fLines)[endLine + 1]->origin 1976 - (*fLines)[startLine]->origin; 1977 1978 if (startLine != endLine && endLine == numLines - 1 1979 && fText->RealCharAt(fText->Length() - 1) == B_ENTER) { 1980 height += (*fLines)[endLine + 1]->origin - (*fLines)[endLine]->origin; 1981 } 1982 1983 return ceilf(height); 1984 } 1985 1986 1987 void 1988 BTextView::GetTextRegion(int32 startOffset, int32 endOffset, 1989 BRegion* outRegion) const 1990 { 1991 if (!outRegion) 1992 return; 1993 1994 outRegion->MakeEmpty(); 1995 1996 // pin offsets at reasonable values 1997 if (startOffset < 0) 1998 startOffset = 0; 1999 else if (startOffset > fText->Length()) 2000 startOffset = fText->Length(); 2001 if (endOffset < 0) 2002 endOffset = 0; 2003 else if (endOffset > fText->Length()) 2004 endOffset = fText->Length(); 2005 2006 // return an empty region if the range is invalid 2007 if (startOffset >= endOffset) 2008 return; 2009 2010 float startLineHeight = 0.0; 2011 float endLineHeight = 0.0; 2012 BPoint startPt = PointAt(startOffset, &startLineHeight); 2013 BPoint endPt = PointAt(endOffset, &endLineHeight); 2014 2015 startLineHeight = ceilf(startLineHeight); 2016 endLineHeight = ceilf(endLineHeight); 2017 2018 BRect selRect; 2019 2020 if (startPt.y == endPt.y) { 2021 // this is a one-line region 2022 selRect.left = max_c(startPt.x, fTextRect.left); 2023 selRect.top = startPt.y; 2024 selRect.right = endPt.x - 1.0; 2025 selRect.bottom = endPt.y + endLineHeight - 1.0; 2026 outRegion->Include(selRect); 2027 } else { 2028 // more than one line in the specified offset range 2029 selRect.left = max_c(startPt.x, fTextRect.left); 2030 selRect.top = startPt.y; 2031 selRect.right = fTextRect.right; 2032 selRect.bottom = startPt.y + startLineHeight - 1.0; 2033 outRegion->Include(selRect); 2034 2035 if (startPt.y + startLineHeight < endPt.y) { 2036 // more than two lines in the range 2037 selRect.left = fTextRect.left; 2038 selRect.top = startPt.y + startLineHeight; 2039 selRect.right = fTextRect.right; 2040 selRect.bottom = endPt.y - 1.0; 2041 outRegion->Include(selRect); 2042 } 2043 2044 selRect.left = fTextRect.left; 2045 selRect.top = endPt.y; 2046 selRect.right = endPt.x - 1.0; 2047 selRect.bottom = endPt.y + endLineHeight - 1.0; 2048 outRegion->Include(selRect); 2049 } 2050 } 2051 2052 2053 void 2054 BTextView::ScrollToOffset(int32 offset) 2055 { 2056 BRect bounds = Bounds(); 2057 float lineHeight = 0.0; 2058 float xDiff = 0.0; 2059 float yDiff = 0.0; 2060 BPoint point = PointAt(offset, &lineHeight); 2061 2062 // horizontal 2063 float extraSpace = fAlignment == B_ALIGN_LEFT ? 2064 ceilf(bounds.IntegerWidth() / 2) : 0.0; 2065 2066 if (point.x < bounds.left) 2067 xDiff = point.x - bounds.left - extraSpace; 2068 else if (point.x > bounds.right) 2069 xDiff = point.x - bounds.right + extraSpace; 2070 2071 // vertical 2072 if (point.y < bounds.top) 2073 yDiff = point.y - bounds.top; 2074 else if (point.y + lineHeight > bounds.bottom 2075 && point.y - lineHeight > bounds.top) { 2076 yDiff = point.y + lineHeight - bounds.bottom; 2077 } 2078 2079 // prevent negative scroll offset 2080 if (bounds.left + xDiff < 0.0) 2081 xDiff = -bounds.left; 2082 if (bounds.top + yDiff < 0.0) 2083 yDiff = -bounds.top; 2084 2085 ScrollBy(xDiff, yDiff); 2086 } 2087 2088 2089 void 2090 BTextView::ScrollToSelection() 2091 { 2092 ScrollToOffset(fSelStart); 2093 } 2094 2095 2096 void 2097 BTextView::Highlight(int32 startOffset, int32 endOffset) 2098 { 2099 // pin offsets at reasonable values 2100 if (startOffset < 0) 2101 startOffset = 0; 2102 else if (startOffset > fText->Length()) 2103 startOffset = fText->Length(); 2104 if (endOffset < 0) 2105 endOffset = 0; 2106 else if (endOffset > fText->Length()) 2107 endOffset = fText->Length(); 2108 2109 if (startOffset >= endOffset) 2110 return; 2111 2112 BRegion selRegion; 2113 GetTextRegion(startOffset, endOffset, &selRegion); 2114 2115 SetDrawingMode(B_OP_INVERT); 2116 FillRegion(&selRegion, B_SOLID_HIGH); 2117 SetDrawingMode(B_OP_COPY); 2118 } 2119 2120 2121 // #pragma mark - Configuration methods 2122 2123 2124 void 2125 BTextView::SetTextRect(BRect rect) 2126 { 2127 if (rect == fTextRect) 2128 return; 2129 2130 if (!fWrap) { 2131 rect.right = Bounds().right - fLayoutData->rightInset; 2132 rect.bottom = Bounds().bottom - fLayoutData->bottomInset; 2133 } 2134 2135 fLayoutData->UpdateInsets(Bounds().OffsetToCopy(B_ORIGIN), rect); 2136 2137 _ResetTextRect(); 2138 } 2139 2140 2141 BRect 2142 BTextView::TextRect() const 2143 { 2144 return fTextRect; 2145 } 2146 2147 2148 void 2149 BTextView::_ResetTextRect() 2150 { 2151 BRect oldTextRect(fTextRect); 2152 // reset text rect to bounds minus insets ... 2153 fTextRect = Bounds().OffsetToCopy(B_ORIGIN); 2154 fTextRect.left += fLayoutData->leftInset; 2155 fTextRect.top += fLayoutData->topInset; 2156 fTextRect.right -= fLayoutData->rightInset; 2157 fTextRect.bottom -= fLayoutData->bottomInset; 2158 2159 // and rewrap (potentially adjusting the right and the bottom of the text 2160 // rect) 2161 _Refresh(0, TextLength(), false); 2162 2163 // Make sure that the dirty area outside the text is redrawn too. 2164 BRegion invalid(oldTextRect | fTextRect); 2165 invalid.Exclude(fTextRect); 2166 Invalidate(&invalid); 2167 } 2168 2169 2170 void 2171 BTextView::SetInsets(float left, float top, float right, float bottom) 2172 { 2173 if (fLayoutData->leftInset == left 2174 && fLayoutData->topInset == top 2175 && fLayoutData->rightInset == right 2176 && fLayoutData->bottomInset == bottom) 2177 return; 2178 2179 fLayoutData->leftInset = left; 2180 fLayoutData->topInset = top; 2181 fLayoutData->rightInset = right; 2182 fLayoutData->bottomInset = bottom; 2183 2184 InvalidateLayout(); 2185 Invalidate(); 2186 } 2187 2188 2189 void 2190 BTextView::GetInsets(float* _left, float* _top, float* _right, 2191 float* _bottom) const 2192 { 2193 if (_left) 2194 *_left = fLayoutData->leftInset; 2195 if (_top) 2196 *_top = fLayoutData->topInset; 2197 if (_right) 2198 *_right = fLayoutData->rightInset; 2199 if (_bottom) 2200 *_bottom = fLayoutData->bottomInset; 2201 } 2202 2203 2204 void 2205 BTextView::SetStylable(bool stylable) 2206 { 2207 fStylable = stylable; 2208 } 2209 2210 2211 bool 2212 BTextView::IsStylable() const 2213 { 2214 return fStylable; 2215 } 2216 2217 2218 void 2219 BTextView::SetTabWidth(float width) 2220 { 2221 if (width == fTabWidth) 2222 return; 2223 2224 fTabWidth = width; 2225 2226 if (Window() != NULL) 2227 _Refresh(0, fText->Length(), false); 2228 } 2229 2230 2231 float 2232 BTextView::TabWidth() const 2233 { 2234 return fTabWidth; 2235 } 2236 2237 2238 void 2239 BTextView::MakeSelectable(bool selectable) 2240 { 2241 if (selectable == fSelectable) 2242 return; 2243 2244 fSelectable = selectable; 2245 2246 if (fActive && fSelStart != fSelEnd && Window() != NULL) 2247 Highlight(fSelStart, fSelEnd); 2248 } 2249 2250 2251 bool 2252 BTextView::IsSelectable() const 2253 { 2254 return fSelectable; 2255 } 2256 2257 2258 void 2259 BTextView::MakeEditable(bool editable) 2260 { 2261 if (editable == fEditable) 2262 return; 2263 2264 fEditable = editable; 2265 // TextControls change the color of the text when 2266 // they are made editable, so we need to invalidate 2267 // the NULL style here 2268 // TODO: it works well, but it could be caused by a bug somewhere else 2269 if (fEditable) 2270 fStyles->InvalidateNullStyle(); 2271 if (Window() != NULL && fActive) { 2272 if (!fEditable) { 2273 _HideCaret(); 2274 _CancelInputMethod(); 2275 } 2276 } 2277 } 2278 2279 2280 bool 2281 BTextView::IsEditable() const 2282 { 2283 return fEditable; 2284 } 2285 2286 2287 void 2288 BTextView::SetWordWrap(bool wrap) 2289 { 2290 if (wrap == fWrap) 2291 return; 2292 2293 bool updateOnScreen = fActive && Window() != NULL; 2294 if (updateOnScreen) { 2295 // hide the caret, unhilite the selection 2296 if (fSelStart != fSelEnd) { 2297 if (fSelectable) 2298 Highlight(fSelStart, fSelEnd); 2299 } else 2300 _HideCaret(); 2301 } 2302 2303 fWrap = wrap; 2304 if (wrap) 2305 _ResetTextRect(); 2306 _Refresh(0, fText->Length(), false); 2307 2308 if (updateOnScreen) { 2309 // show the caret, hilite the selection 2310 if (fSelStart != fSelEnd) { 2311 if (fSelectable) 2312 Highlight(fSelStart, fSelEnd); 2313 } else 2314 _ShowCaret(); 2315 } 2316 } 2317 2318 2319 bool 2320 BTextView::DoesWordWrap() const 2321 { 2322 return fWrap; 2323 } 2324 2325 2326 void 2327 BTextView::SetMaxBytes(int32 max) 2328 { 2329 const int32 textLength = fText->Length(); 2330 fMaxBytes = max; 2331 2332 if (fMaxBytes < textLength) { 2333 int32 offset = fMaxBytes; 2334 // Delete the text after fMaxBytes, but 2335 // respect multibyte characters boundaries. 2336 const int32 previousInitial = _PreviousInitialByte(offset); 2337 if (_NextInitialByte(previousInitial) != offset) 2338 offset = previousInitial; 2339 2340 Delete(offset, textLength); 2341 } 2342 } 2343 2344 2345 int32 2346 BTextView::MaxBytes() const 2347 { 2348 return fMaxBytes; 2349 } 2350 2351 2352 void 2353 BTextView::DisallowChar(uint32 character) 2354 { 2355 if (fDisallowedChars == NULL) 2356 fDisallowedChars = new BList; 2357 if (!fDisallowedChars->HasItem(reinterpret_cast<void*>(character))) 2358 fDisallowedChars->AddItem(reinterpret_cast<void*>(character)); 2359 } 2360 2361 2362 void 2363 BTextView::AllowChar(uint32 character) 2364 { 2365 if (fDisallowedChars != NULL) 2366 fDisallowedChars->RemoveItem(reinterpret_cast<void*>(character)); 2367 } 2368 2369 2370 void 2371 BTextView::SetAlignment(alignment align) 2372 { 2373 // Do a reality check 2374 if (fAlignment != align && 2375 (align == B_ALIGN_LEFT || 2376 align == B_ALIGN_RIGHT || 2377 align == B_ALIGN_CENTER)) { 2378 fAlignment = align; 2379 2380 // After setting new alignment, update the view/window 2381 if (Window() != NULL) 2382 Invalidate(); 2383 } 2384 } 2385 2386 2387 alignment 2388 BTextView::Alignment() const 2389 { 2390 return fAlignment; 2391 } 2392 2393 2394 void 2395 BTextView::SetAutoindent(bool state) 2396 { 2397 fAutoindent = state; 2398 } 2399 2400 2401 bool 2402 BTextView::DoesAutoindent() const 2403 { 2404 return fAutoindent; 2405 } 2406 2407 2408 void 2409 BTextView::SetColorSpace(color_space colors) 2410 { 2411 if (colors != fColorSpace && fOffscreen) { 2412 fColorSpace = colors; 2413 _DeleteOffscreen(); 2414 _NewOffscreen(); 2415 } 2416 } 2417 2418 2419 color_space 2420 BTextView::ColorSpace() const 2421 { 2422 return fColorSpace; 2423 } 2424 2425 2426 void 2427 BTextView::MakeResizable(bool resize, BView* resizeView) 2428 { 2429 if (resize) { 2430 fResizable = true; 2431 fContainerView = resizeView; 2432 2433 // Wrapping mode and resizable mode can't live together 2434 if (fWrap) { 2435 fWrap = false; 2436 2437 if (fActive && Window() != NULL) { 2438 if (fSelStart != fSelEnd) { 2439 if (fSelectable) 2440 Highlight(fSelStart, fSelEnd); 2441 } else 2442 _HideCaret(); 2443 } 2444 } 2445 // We need to reset the right inset, as otherwise the auto-resize would 2446 // get confused about just how wide the textview needs to be. 2447 // This seems to be an artefact of how Tracker creates the textview 2448 // during a rename action. 2449 fLayoutData->rightInset = fLayoutData->leftInset; 2450 } else { 2451 fResizable = false; 2452 fContainerView = NULL; 2453 if (fOffscreen) 2454 _DeleteOffscreen(); 2455 _NewOffscreen(); 2456 } 2457 2458 _Refresh(0, fText->Length(), false); 2459 } 2460 2461 2462 bool 2463 BTextView::IsResizable() const 2464 { 2465 return fResizable; 2466 } 2467 2468 2469 void 2470 BTextView::SetDoesUndo(bool undo) 2471 { 2472 if (undo && fUndo == NULL) 2473 fUndo = new UndoBuffer(this, B_UNDO_UNAVAILABLE); 2474 else if (!undo && fUndo != NULL) { 2475 delete fUndo; 2476 fUndo = NULL; 2477 } 2478 } 2479 2480 2481 bool 2482 BTextView::DoesUndo() const 2483 { 2484 return fUndo != NULL; 2485 } 2486 2487 2488 void 2489 BTextView::HideTyping(bool enabled) 2490 { 2491 if (enabled) 2492 Delete(0, fText->Length()); 2493 2494 fText->SetPasswordMode(enabled); 2495 } 2496 2497 2498 bool 2499 BTextView::IsTypingHidden() const 2500 { 2501 return fText->PasswordMode(); 2502 } 2503 2504 2505 // #pragma mark - Size methods 2506 2507 2508 void 2509 BTextView::ResizeToPreferred() 2510 { 2511 BView::ResizeToPreferred(); 2512 } 2513 2514 2515 void 2516 BTextView::GetPreferredSize(float* _width, float* _height) 2517 { 2518 CALLED(); 2519 2520 _ValidateLayoutData(); 2521 2522 if (_width) { 2523 float width = Bounds().Width(); 2524 if (width < fLayoutData->min.width 2525 || (Flags() & B_SUPPORTS_LAYOUT) != 0) { 2526 width = fLayoutData->min.width; 2527 } 2528 *_width = width; 2529 } 2530 2531 if (_height) { 2532 float height = Bounds().Height(); 2533 if (height < fLayoutData->min.height 2534 || (Flags() & B_SUPPORTS_LAYOUT) != 0) { 2535 height = fLayoutData->min.height; 2536 } 2537 *_height = height; 2538 } 2539 } 2540 2541 2542 BSize 2543 BTextView::MinSize() 2544 { 2545 CALLED(); 2546 2547 _ValidateLayoutData(); 2548 return BLayoutUtils::ComposeSize(ExplicitMinSize(), fLayoutData->min); 2549 } 2550 2551 2552 BSize 2553 BTextView::MaxSize() 2554 { 2555 CALLED(); 2556 2557 return BLayoutUtils::ComposeSize(ExplicitMaxSize(), 2558 BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED)); 2559 } 2560 2561 2562 BSize 2563 BTextView::PreferredSize() 2564 { 2565 CALLED(); 2566 2567 _ValidateLayoutData(); 2568 return BLayoutUtils::ComposeSize(ExplicitPreferredSize(), 2569 fLayoutData->preferred); 2570 } 2571 2572 2573 bool 2574 BTextView::HasHeightForWidth() 2575 { 2576 if (IsEditable()) 2577 return BView::HasHeightForWidth(); 2578 2579 // When not editable, we assume that all text is supposed to be visible. 2580 return true; 2581 } 2582 2583 2584 void 2585 BTextView::GetHeightForWidth(float width, float* min, float* max, 2586 float* preferred) 2587 { 2588 if (IsEditable()) { 2589 BView::GetHeightForWidth(width, min, max, preferred); 2590 return; 2591 } 2592 2593 // TODO: don't change the actual text rect! 2594 fTextRect.right = fTextRect.left + width; 2595 _Refresh(0, TextLength(), false); 2596 2597 if (min != NULL) 2598 *min = fTextRect.Height(); 2599 if (max != NULL) 2600 *max = B_SIZE_UNLIMITED; 2601 if (preferred != NULL) 2602 *preferred = fTextRect.Height(); 2603 } 2604 2605 2606 // #pragma mark - Layout methods 2607 2608 2609 void 2610 BTextView::LayoutInvalidated(bool descendants) 2611 { 2612 CALLED(); 2613 2614 fLayoutData->valid = false; 2615 } 2616 2617 2618 void 2619 BTextView::DoLayout() 2620 { 2621 // Bail out, if we shan't do layout. 2622 if (!(Flags() & B_SUPPORTS_LAYOUT)) 2623 return; 2624 2625 CALLED(); 2626 2627 // If the user set a layout, we let the base class version call its 2628 // hook. 2629 if (GetLayout()) { 2630 BView::DoLayout(); 2631 return; 2632 } 2633 2634 _ValidateLayoutData(); 2635 2636 // validate current size 2637 BSize size(Bounds().Size()); 2638 if (size.width < fLayoutData->min.width) 2639 size.width = fLayoutData->min.width; 2640 if (size.height < fLayoutData->min.height) 2641 size.height = fLayoutData->min.height; 2642 2643 _ResetTextRect(); 2644 } 2645 2646 2647 void 2648 BTextView::_ValidateLayoutData() 2649 { 2650 if (fLayoutData->valid) 2651 return; 2652 2653 CALLED(); 2654 2655 float lineHeight = ceilf(LineHeight(0)); 2656 TRACE("line height: %.2f\n", lineHeight); 2657 2658 // compute our minimal size 2659 BSize min(lineHeight * 3, lineHeight); 2660 min.width += fLayoutData->leftInset + fLayoutData->rightInset; 2661 min.height += fLayoutData->topInset + fLayoutData->bottomInset; 2662 2663 fLayoutData->min = min; 2664 2665 // compute our preferred size 2666 fLayoutData->preferred.height = fTextRect.Height() 2667 + fLayoutData->topInset + fLayoutData->bottomInset; 2668 2669 if (fWrap) 2670 fLayoutData->preferred.width = min.width + 5 * lineHeight; 2671 else { 2672 float maxWidth = fLines->MaxWidth(); 2673 if (maxWidth < min.width) 2674 maxWidth = min.width; 2675 2676 fLayoutData->preferred.width 2677 = maxWidth + fLayoutData->leftInset + fLayoutData->rightInset; 2678 } 2679 2680 fLayoutData->valid = true; 2681 ResetLayoutInvalidation(); 2682 2683 TRACE("width: %.2f, height: %.2f\n", min.width, min.height); 2684 } 2685 2686 2687 // #pragma mark - 2688 2689 2690 void 2691 BTextView::AllAttached() 2692 { 2693 BView::AllAttached(); 2694 } 2695 2696 2697 void 2698 BTextView::AllDetached() 2699 { 2700 BView::AllDetached(); 2701 } 2702 2703 2704 /* static */ 2705 text_run_array* 2706 BTextView::AllocRunArray(int32 entryCount, int32* outSize) 2707 { 2708 int32 size = sizeof(text_run_array) + (entryCount - 1) * sizeof(text_run); 2709 2710 text_run_array* runArray = (text_run_array*)calloc(size, 1); 2711 if (runArray == NULL) { 2712 if (outSize != NULL) 2713 *outSize = 0; 2714 return NULL; 2715 } 2716 2717 runArray->count = entryCount; 2718 2719 // Call constructors explicitly as the text_run_array 2720 // was allocated with malloc (and has to, for backwards 2721 // compatibility) 2722 for (int32 i = 0; i < runArray->count; i++) { 2723 new (&runArray->runs[i].font) BFont; 2724 } 2725 2726 if (outSize != NULL) 2727 *outSize = size; 2728 2729 return runArray; 2730 } 2731 2732 2733 /* static */ 2734 text_run_array* 2735 BTextView::CopyRunArray(const text_run_array* orig, int32 countDelta) 2736 { 2737 text_run_array* copy = AllocRunArray(countDelta, NULL); 2738 if (copy != NULL) { 2739 for (int32 i = 0; i < countDelta; i++) { 2740 copy->runs[i].offset = orig->runs[i].offset; 2741 copy->runs[i].font = orig->runs[i].font; 2742 copy->runs[i].color = orig->runs[i].color; 2743 } 2744 } 2745 return copy; 2746 } 2747 2748 2749 /* static */ 2750 void 2751 BTextView::FreeRunArray(text_run_array* array) 2752 { 2753 if (array == NULL) 2754 return; 2755 2756 // Call destructors explicitly 2757 for (int32 i = 0; i < array->count; i++) 2758 array->runs[i].font.~BFont(); 2759 2760 free(array); 2761 } 2762 2763 2764 /* static */ 2765 void* 2766 BTextView::FlattenRunArray(const text_run_array* runArray, int32* _size) 2767 { 2768 CALLED(); 2769 int32 size = sizeof(flattened_text_run_array) + (runArray->count - 1) 2770 * sizeof(flattened_text_run); 2771 2772 flattened_text_run_array* array = (flattened_text_run_array*)malloc(size); 2773 if (array == NULL) { 2774 if (_size) 2775 *_size = 0; 2776 return NULL; 2777 } 2778 2779 array->magic = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayMagic); 2780 array->version = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayVersion); 2781 array->count = B_HOST_TO_BENDIAN_INT32(runArray->count); 2782 2783 for (int32 i = 0; i < runArray->count; i++) { 2784 array->styles[i].offset = B_HOST_TO_BENDIAN_INT32( 2785 runArray->runs[i].offset); 2786 runArray->runs[i].font.GetFamilyAndStyle(&array->styles[i].family, 2787 &array->styles[i].style); 2788 array->styles[i].size = B_HOST_TO_BENDIAN_FLOAT( 2789 runArray->runs[i].font.Size()); 2790 array->styles[i].shear = B_HOST_TO_BENDIAN_FLOAT( 2791 runArray->runs[i].font.Shear()); 2792 array->styles[i].face = B_HOST_TO_BENDIAN_INT16( 2793 runArray->runs[i].font.Face()); 2794 array->styles[i].red = runArray->runs[i].color.red; 2795 array->styles[i].green = runArray->runs[i].color.green; 2796 array->styles[i].blue = runArray->runs[i].color.blue; 2797 array->styles[i].alpha = 255; 2798 array->styles[i]._reserved_ = 0; 2799 } 2800 2801 if (_size) 2802 *_size = size; 2803 2804 return array; 2805 } 2806 2807 2808 /* static */ 2809 text_run_array* 2810 BTextView::UnflattenRunArray(const void* data, int32* _size) 2811 { 2812 CALLED(); 2813 flattened_text_run_array* array = (flattened_text_run_array*)data; 2814 2815 if (B_BENDIAN_TO_HOST_INT32(array->magic) != kFlattenedTextRunArrayMagic 2816 || B_BENDIAN_TO_HOST_INT32(array->version) 2817 != kFlattenedTextRunArrayVersion) { 2818 if (_size) 2819 *_size = 0; 2820 2821 return NULL; 2822 } 2823 2824 int32 count = B_BENDIAN_TO_HOST_INT32(array->count); 2825 2826 text_run_array* runArray = AllocRunArray(count, _size); 2827 if (runArray == NULL) 2828 return NULL; 2829 2830 for (int32 i = 0; i < count; i++) { 2831 runArray->runs[i].offset = B_BENDIAN_TO_HOST_INT32( 2832 array->styles[i].offset); 2833 2834 // Set family and style independently from each other, so that 2835 // even if the family doesn't exist, we try to preserve the style 2836 runArray->runs[i].font.SetFamilyAndStyle(array->styles[i].family, NULL); 2837 runArray->runs[i].font.SetFamilyAndStyle(NULL, array->styles[i].style); 2838 2839 runArray->runs[i].font.SetSize(B_BENDIAN_TO_HOST_FLOAT( 2840 array->styles[i].size)); 2841 runArray->runs[i].font.SetShear(B_BENDIAN_TO_HOST_FLOAT( 2842 array->styles[i].shear)); 2843 2844 uint16 face = B_BENDIAN_TO_HOST_INT16(array->styles[i].face); 2845 if (face != B_REGULAR_FACE) { 2846 // Be's version doesn't seem to set this correctly 2847 runArray->runs[i].font.SetFace(face); 2848 } 2849 2850 runArray->runs[i].color.red = array->styles[i].red; 2851 runArray->runs[i].color.green = array->styles[i].green; 2852 runArray->runs[i].color.blue = array->styles[i].blue; 2853 runArray->runs[i].color.alpha = array->styles[i].alpha; 2854 } 2855 2856 return runArray; 2857 } 2858 2859 2860 void 2861 BTextView::InsertText(const char* text, int32 length, int32 offset, 2862 const text_run_array* runs) 2863 { 2864 CALLED(); 2865 2866 if (length < 0) 2867 length = 0; 2868 2869 if (offset < 0) 2870 offset = 0; 2871 else if (offset > fText->Length()) 2872 offset = fText->Length(); 2873 2874 if (length > 0) { 2875 // add the text to the buffer 2876 fText->InsertText(text, length, offset); 2877 2878 // update the start offsets of each line below offset 2879 fLines->BumpOffset(length, _LineAt(offset) + 1); 2880 2881 // update the style runs 2882 fStyles->BumpOffset(length, fStyles->OffsetToRun(offset - 1) + 1); 2883 2884 // offset the caret/selection, if the text was inserted before it 2885 if (offset <= fSelEnd) { 2886 fSelStart += length; 2887 fCaretOffset = fSelEnd = fSelStart; 2888 } 2889 } 2890 2891 if (fStylable && runs != NULL) { 2892 _SetRunArray(offset, offset + length, runs); 2893 } else { 2894 // apply null-style to inserted text 2895 _ApplyStyleRange(offset, offset + length); 2896 } 2897 } 2898 2899 2900 void 2901 BTextView::DeleteText(int32 fromOffset, int32 toOffset) 2902 { 2903 CALLED(); 2904 2905 if (fromOffset < 0) 2906 fromOffset = 0; 2907 else if (fromOffset > fText->Length()) 2908 fromOffset = fText->Length(); 2909 2910 if (toOffset < 0) 2911 toOffset = 0; 2912 else if (toOffset > fText->Length()) 2913 toOffset = fText->Length(); 2914 2915 if (fromOffset >= toOffset) 2916 return; 2917 2918 // set nullStyle to style at beginning of range 2919 fStyles->InvalidateNullStyle(); 2920 fStyles->SyncNullStyle(fromOffset); 2921 2922 // remove from the text buffer 2923 fText->RemoveRange(fromOffset, toOffset); 2924 2925 // remove any lines that have been obliterated 2926 fLines->RemoveLineRange(fromOffset, toOffset); 2927 2928 // remove any style runs that have been obliterated 2929 fStyles->RemoveStyleRange(fromOffset, toOffset); 2930 2931 // adjust the selection accordingly, assumes fSelEnd >= fSelStart! 2932 int32 range = toOffset - fromOffset; 2933 if (fSelStart >= toOffset) { 2934 // selection is behind the range that was removed 2935 fSelStart -= range; 2936 fSelEnd -= range; 2937 } else if (fSelStart >= fromOffset && fSelEnd <= toOffset) { 2938 // the selection is within the range that was removed 2939 fSelStart = fSelEnd = fromOffset; 2940 } else if (fSelStart >= fromOffset && fSelEnd > toOffset) { 2941 // the selection starts within and ends after the range 2942 // the remaining part is the part that was after the range 2943 fSelStart = fromOffset; 2944 fSelEnd = fromOffset + fSelEnd - toOffset; 2945 } else if (fSelStart < fromOffset && fSelEnd < toOffset) { 2946 // the selection starts before, but ends within the range 2947 fSelEnd = fromOffset; 2948 } else if (fSelStart < fromOffset && fSelEnd >= toOffset) { 2949 // the selection starts before and ends after the range 2950 fSelEnd -= range; 2951 } 2952 } 2953 2954 2955 /*! Undoes the last changes. 2956 2957 \param clipboard A \a clipboard to use for the undo operation. 2958 */ 2959 void 2960 BTextView::Undo(BClipboard* clipboard) 2961 { 2962 if (fUndo) 2963 fUndo->Undo(clipboard); 2964 } 2965 2966 2967 undo_state 2968 BTextView::UndoState(bool* isRedo) const 2969 { 2970 return fUndo == NULL ? B_UNDO_UNAVAILABLE : fUndo->State(isRedo); 2971 } 2972 2973 2974 // #pragma mark - GetDragParameters() is protected 2975 2976 2977 void 2978 BTextView::GetDragParameters(BMessage* drag, BBitmap** bitmap, BPoint* point, 2979 BHandler** handler) 2980 { 2981 CALLED(); 2982 if (drag == NULL) 2983 return; 2984 2985 // Add originator and action 2986 drag->AddPointer("be:originator", this); 2987 drag->AddInt32("be_actions", B_TRASH_TARGET); 2988 2989 // add the text 2990 int32 numBytes = fSelEnd - fSelStart; 2991 const char* text = fText->GetString(fSelStart, &numBytes); 2992 drag->AddData("text/plain", B_MIME_TYPE, text, numBytes); 2993 2994 // add the corresponding styles 2995 int32 size = 0; 2996 text_run_array* styles = RunArray(fSelStart, fSelEnd, &size); 2997 2998 if (styles != NULL) { 2999 drag->AddData("application/x-vnd.Be-text_run_array", B_MIME_TYPE, 3000 styles, size); 3001 3002 FreeRunArray(styles); 3003 } 3004 3005 if (bitmap != NULL) 3006 *bitmap = NULL; 3007 3008 if (handler != NULL) 3009 *handler = NULL; 3010 } 3011 3012 3013 // #pragma mark - FBC padding and forbidden methods 3014 3015 3016 void BTextView::_ReservedTextView3() {} 3017 void BTextView::_ReservedTextView4() {} 3018 void BTextView::_ReservedTextView5() {} 3019 void BTextView::_ReservedTextView6() {} 3020 void BTextView::_ReservedTextView7() {} 3021 void BTextView::_ReservedTextView8() {} 3022 void BTextView::_ReservedTextView9() {} 3023 void BTextView::_ReservedTextView10() {} 3024 void BTextView::_ReservedTextView11() {} 3025 void BTextView::_ReservedTextView12() {} 3026 3027 3028 // #pragma mark - Private methods 3029 3030 3031 /*! Inits the BTextView object. 3032 3033 \param textRect The BTextView's text rect. 3034 \param initialFont The font which the BTextView will use. 3035 \param initialColor The initial color of the text. 3036 */ 3037 void 3038 BTextView::_InitObject(BRect textRect, const BFont* initialFont, 3039 const rgb_color* initialColor) 3040 { 3041 BFont font; 3042 if (initialFont == NULL) 3043 GetFont(&font); 3044 else 3045 font = *initialFont; 3046 3047 _NormalizeFont(&font); 3048 3049 rgb_color documentTextColor = ui_color(B_DOCUMENT_TEXT_COLOR); 3050 3051 if (initialColor == NULL) 3052 initialColor = &documentTextColor; 3053 3054 fText = new BPrivate::TextGapBuffer; 3055 fLines = new LineBuffer; 3056 fStyles = new StyleBuffer(&font, initialColor); 3057 3058 fInstalledNavigateCommandWordwiseShortcuts = false; 3059 fInstalledNavigateOptionWordwiseShortcuts = false; 3060 fInstalledNavigateOptionLinewiseShortcuts = false; 3061 fInstalledNavigateHomeEndDocwiseShortcuts = false; 3062 3063 fInstalledSelectCommandWordwiseShortcuts = false; 3064 fInstalledSelectOptionWordwiseShortcuts = false; 3065 fInstalledSelectOptionLinewiseShortcuts = false; 3066 fInstalledSelectHomeEndDocwiseShortcuts = false; 3067 3068 // We put these here instead of in the constructor initializer list 3069 // to have less code duplication, and a single place where to do changes 3070 // if needed. 3071 fTextRect = textRect; 3072 // NOTE: The only places where text rect is changed: 3073 // * width is possibly adjusted in _AutoResize(), 3074 // * height is adjusted in _RecalculateLineBreaks(). 3075 // When used within the layout management framework, the 3076 // text rect is changed to maintain constant insets. 3077 fMinTextRectWidth = fTextRect.Width(); 3078 // see SetTextRect() 3079 fSelStart = fSelEnd = 0; 3080 fCaretVisible = false; 3081 fCaretTime = 0; 3082 fCaretOffset = 0; 3083 fClickCount = 0; 3084 fClickTime = 0; 3085 fDragOffset = -1; 3086 fCursor = 0; 3087 fActive = false; 3088 fStylable = false; 3089 fTabWidth = 28.0; 3090 fSelectable = true; 3091 fEditable = true; 3092 fWrap = true; 3093 fMaxBytes = INT32_MAX; 3094 fDisallowedChars = NULL; 3095 fAlignment = B_ALIGN_LEFT; 3096 fAutoindent = false; 3097 fOffscreen = NULL; 3098 fColorSpace = B_CMAP8; 3099 fResizable = false; 3100 fContainerView = NULL; 3101 fUndo = NULL; 3102 fInline = NULL; 3103 fDragRunner = NULL; 3104 fClickRunner = NULL; 3105 fTrackingMouse = NULL; 3106 3107 fLayoutData = new LayoutData; 3108 fLayoutData->UpdateInsets(Bounds().OffsetToCopy(B_ORIGIN), fTextRect); 3109 3110 fLastClickOffset = -1; 3111 3112 SetDoesUndo(true); 3113 SetViewUIColor(B_DOCUMENT_BACKGROUND_COLOR); 3114 } 3115 3116 3117 //! Handles when Backspace key is pressed. 3118 void 3119 BTextView::_HandleBackspace() 3120 { 3121 if (fUndo) { 3122 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>( 3123 fUndo); 3124 if (!undoBuffer) { 3125 delete fUndo; 3126 fUndo = undoBuffer = new TypingUndoBuffer(this); 3127 } 3128 undoBuffer->BackwardErase(); 3129 } 3130 3131 if (fSelStart == fSelEnd) { 3132 if (fSelStart == 0) 3133 return; 3134 else 3135 fSelStart = _PreviousInitialByte(fSelStart); 3136 } else 3137 Highlight(fSelStart, fSelEnd); 3138 3139 DeleteText(fSelStart, fSelEnd); 3140 fCaretOffset = fSelEnd = fSelStart; 3141 3142 _Refresh(fSelStart, fSelEnd, true); 3143 } 3144 3145 3146 //! Handles when an arrow key is pressed. 3147 void 3148 BTextView::_HandleArrowKey(uint32 arrowKey, int32 modifiers) 3149 { 3150 // return if there's nowhere to go 3151 if (fText->Length() == 0) 3152 return; 3153 3154 int32 selStart = fSelStart; 3155 int32 selEnd = fSelEnd; 3156 3157 if (modifiers < 0) { 3158 BMessage* currentMessage = Window()->CurrentMessage(); 3159 if (currentMessage == NULL 3160 || currentMessage->FindInt32("modifiers", &modifiers) != B_OK) { 3161 modifiers = 0; 3162 } 3163 } 3164 3165 bool shiftKeyDown = (modifiers & B_SHIFT_KEY) != 0; 3166 bool controlKeyDown = (modifiers & B_CONTROL_KEY) != 0; 3167 bool optionKeyDown = (modifiers & B_OPTION_KEY) != 0; 3168 bool commandKeyDown = (modifiers & B_COMMAND_KEY) != 0; 3169 3170 int32 lastClickOffset = fCaretOffset; 3171 3172 switch (arrowKey) { 3173 case B_LEFT_ARROW: 3174 if (!fEditable) 3175 _ScrollBy(-1 * kHorizontalScrollBarStep, 0); 3176 else if (fSelStart != fSelEnd && !shiftKeyDown) 3177 fCaretOffset = fSelStart; 3178 else { 3179 if ((commandKeyDown || optionKeyDown) && !controlKeyDown) 3180 fCaretOffset = _PreviousWordStart(fCaretOffset - 1); 3181 else 3182 fCaretOffset = _PreviousInitialByte(fCaretOffset); 3183 3184 if (shiftKeyDown && fCaretOffset != lastClickOffset) { 3185 if (fCaretOffset < fSelStart) { 3186 // extend selection to the left 3187 selStart = fCaretOffset; 3188 if (lastClickOffset > fSelStart) { 3189 // caret has jumped across "anchor" 3190 selEnd = fSelStart; 3191 } 3192 } else { 3193 // shrink selection from the right 3194 selEnd = fCaretOffset; 3195 } 3196 } 3197 } 3198 break; 3199 3200 case B_RIGHT_ARROW: 3201 if (!fEditable) 3202 _ScrollBy(kHorizontalScrollBarStep, 0); 3203 else if (fSelStart != fSelEnd && !shiftKeyDown) 3204 fCaretOffset = fSelEnd; 3205 else { 3206 if ((commandKeyDown || optionKeyDown) && !controlKeyDown) 3207 fCaretOffset = _NextWordEnd(fCaretOffset); 3208 else 3209 fCaretOffset = _NextInitialByte(fCaretOffset); 3210 3211 if (shiftKeyDown && fCaretOffset != lastClickOffset) { 3212 if (fCaretOffset > fSelEnd) { 3213 // extend selection to the right 3214 selEnd = fCaretOffset; 3215 if (lastClickOffset < fSelEnd) { 3216 // caret has jumped across "anchor" 3217 selStart = fSelEnd; 3218 } 3219 } else { 3220 // shrink selection from the left 3221 selStart = fCaretOffset; 3222 } 3223 } 3224 } 3225 break; 3226 3227 case B_UP_ARROW: 3228 { 3229 if (!fEditable) 3230 _ScrollBy(0, -1 * kVerticalScrollBarStep); 3231 else if (fSelStart != fSelEnd && !shiftKeyDown) 3232 fCaretOffset = fSelStart; 3233 else { 3234 if (optionKeyDown && !commandKeyDown && !controlKeyDown) 3235 fCaretOffset = _PreviousLineStart(fCaretOffset); 3236 else if (commandKeyDown && !optionKeyDown && !controlKeyDown) { 3237 _ScrollTo(0, 0); 3238 fCaretOffset = 0; 3239 } else { 3240 float height; 3241 BPoint point = PointAt(fCaretOffset, &height); 3242 // find the caret position on the previous 3243 // line by gently stepping onto this line 3244 for (int i = 1; i <= height; i++) { 3245 point.y--; 3246 int32 offset = OffsetAt(point); 3247 if (offset < fCaretOffset || i == height) { 3248 fCaretOffset = offset; 3249 break; 3250 } 3251 } 3252 } 3253 3254 if (shiftKeyDown && fCaretOffset != lastClickOffset) { 3255 if (fCaretOffset < fSelStart) { 3256 // extend selection to the top 3257 selStart = fCaretOffset; 3258 if (lastClickOffset > fSelStart) { 3259 // caret has jumped across "anchor" 3260 selEnd = fSelStart; 3261 } 3262 } else { 3263 // shrink selection from the bottom 3264 selEnd = fCaretOffset; 3265 } 3266 } 3267 } 3268 break; 3269 } 3270 3271 case B_DOWN_ARROW: 3272 { 3273 if (!fEditable) 3274 _ScrollBy(0, kVerticalScrollBarStep); 3275 else if (fSelStart != fSelEnd && !shiftKeyDown) 3276 fCaretOffset = fSelEnd; 3277 else { 3278 if (optionKeyDown && !commandKeyDown && !controlKeyDown) 3279 fCaretOffset = _NextLineEnd(fCaretOffset); 3280 else if (commandKeyDown && !optionKeyDown && !controlKeyDown) { 3281 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset); 3282 fCaretOffset = fText->Length(); 3283 } else { 3284 float height; 3285 BPoint point = PointAt(fCaretOffset, &height); 3286 point.y += height; 3287 fCaretOffset = OffsetAt(point); 3288 } 3289 3290 if (shiftKeyDown && fCaretOffset != lastClickOffset) { 3291 if (fCaretOffset > fSelEnd) { 3292 // extend selection to the bottom 3293 selEnd = fCaretOffset; 3294 if (lastClickOffset < fSelEnd) { 3295 // caret has jumped across "anchor" 3296 selStart = fSelEnd; 3297 } 3298 } else { 3299 // shrink selection from the top 3300 selStart = fCaretOffset; 3301 } 3302 } 3303 } 3304 break; 3305 } 3306 } 3307 3308 fStyles->InvalidateNullStyle(); 3309 3310 if (fEditable) { 3311 if (shiftKeyDown) 3312 Select(selStart, selEnd); 3313 else 3314 Select(fCaretOffset, fCaretOffset); 3315 3316 // scroll if needed 3317 ScrollToOffset(fCaretOffset); 3318 } 3319 } 3320 3321 3322 //! Handles when the Delete key is pressed. 3323 void 3324 BTextView::_HandleDelete() 3325 { 3326 if (fUndo) { 3327 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>( 3328 fUndo); 3329 if (!undoBuffer) { 3330 delete fUndo; 3331 fUndo = undoBuffer = new TypingUndoBuffer(this); 3332 } 3333 undoBuffer->ForwardErase(); 3334 } 3335 3336 if (fSelStart == fSelEnd) { 3337 if (fSelEnd == fText->Length()) 3338 return; 3339 else 3340 fSelEnd = _NextInitialByte(fSelEnd); 3341 } else 3342 Highlight(fSelStart, fSelEnd); 3343 3344 DeleteText(fSelStart, fSelEnd); 3345 fCaretOffset = fSelEnd = fSelStart; 3346 3347 _Refresh(fSelStart, fSelEnd, true); 3348 } 3349 3350 3351 //! Handles when the Page Up, Page Down, Home, or End key is pressed. 3352 void 3353 BTextView::_HandlePageKey(uint32 pageKey, int32 modifiers) 3354 { 3355 if (modifiers < 0) { 3356 BMessage* currentMessage = Window()->CurrentMessage(); 3357 if (currentMessage == NULL 3358 || currentMessage->FindInt32("modifiers", &modifiers) != B_OK) { 3359 modifiers = 0; 3360 } 3361 } 3362 3363 bool shiftKeyDown = (modifiers & B_SHIFT_KEY) != 0; 3364 bool controlKeyDown = (modifiers & B_CONTROL_KEY) != 0; 3365 bool optionKeyDown = (modifiers & B_OPTION_KEY) != 0; 3366 bool commandKeyDown = (modifiers & B_COMMAND_KEY) != 0; 3367 3368 STELine* line = NULL; 3369 int32 selStart = fSelStart; 3370 int32 selEnd = fSelEnd; 3371 3372 int32 lastClickOffset = fCaretOffset; 3373 switch (pageKey) { 3374 case B_HOME: 3375 if (!fEditable) { 3376 fCaretOffset = 0; 3377 _ScrollTo(0, 0); 3378 break; 3379 } else { 3380 if (commandKeyDown && !optionKeyDown && !controlKeyDown) { 3381 _ScrollTo(0, 0); 3382 fCaretOffset = 0; 3383 } else { 3384 // get the start of the last line if caret is on it 3385 line = (*fLines)[_LineAt(lastClickOffset)]; 3386 fCaretOffset = line->offset; 3387 } 3388 3389 if (!shiftKeyDown) 3390 selStart = selEnd = fCaretOffset; 3391 else if (fCaretOffset != lastClickOffset) { 3392 if (fCaretOffset < fSelStart) { 3393 // extend selection to the left 3394 selStart = fCaretOffset; 3395 if (lastClickOffset > fSelStart) { 3396 // caret has jumped across "anchor" 3397 selEnd = fSelStart; 3398 } 3399 } else { 3400 // shrink selection from the right 3401 selEnd = fCaretOffset; 3402 } 3403 } 3404 } 3405 break; 3406 3407 case B_END: 3408 if (!fEditable) { 3409 fCaretOffset = fText->Length(); 3410 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset); 3411 break; 3412 } else { 3413 if (commandKeyDown && !optionKeyDown && !controlKeyDown) { 3414 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset); 3415 fCaretOffset = fText->Length(); 3416 } else { 3417 // If we are on the last line, just go to the last 3418 // character in the buffer, otherwise get the starting 3419 // offset of the next line, and go to the previous character 3420 int32 currentLine = _LineAt(lastClickOffset); 3421 if (currentLine + 1 < fLines->NumLines()) { 3422 line = (*fLines)[currentLine + 1]; 3423 fCaretOffset = _PreviousInitialByte(line->offset); 3424 } else { 3425 // This check is needed to avoid moving the cursor 3426 // when the cursor is on the last line, and that line 3427 // is empty 3428 if (fCaretOffset != fText->Length()) { 3429 fCaretOffset = fText->Length(); 3430 if (ByteAt(fCaretOffset - 1) == B_ENTER) 3431 fCaretOffset--; 3432 } 3433 } 3434 } 3435 3436 if (!shiftKeyDown) 3437 selStart = selEnd = fCaretOffset; 3438 else if (fCaretOffset != lastClickOffset) { 3439 if (fCaretOffset > fSelEnd) { 3440 // extend selection to the right 3441 selEnd = fCaretOffset; 3442 if (lastClickOffset < fSelEnd) { 3443 // caret has jumped across "anchor" 3444 selStart = fSelEnd; 3445 } 3446 } else { 3447 // shrink selection from the left 3448 selStart = fCaretOffset; 3449 } 3450 } 3451 } 3452 break; 3453 3454 case B_PAGE_UP: 3455 { 3456 float lineHeight; 3457 BPoint currentPos = PointAt(fCaretOffset, &lineHeight); 3458 BPoint nextPos(currentPos.x, 3459 currentPos.y + lineHeight - Bounds().Height()); 3460 fCaretOffset = OffsetAt(nextPos); 3461 nextPos = PointAt(fCaretOffset); 3462 _ScrollBy(0, nextPos.y - currentPos.y); 3463 3464 if (!fEditable) 3465 break; 3466 3467 if (!shiftKeyDown) 3468 selStart = selEnd = fCaretOffset; 3469 else if (fCaretOffset != lastClickOffset) { 3470 if (fCaretOffset < fSelStart) { 3471 // extend selection to the top 3472 selStart = fCaretOffset; 3473 if (lastClickOffset > fSelStart) { 3474 // caret has jumped across "anchor" 3475 selEnd = fSelStart; 3476 } 3477 } else { 3478 // shrink selection from the bottom 3479 selEnd = fCaretOffset; 3480 } 3481 } 3482 3483 break; 3484 } 3485 3486 case B_PAGE_DOWN: 3487 { 3488 BPoint currentPos = PointAt(fCaretOffset); 3489 BPoint nextPos(currentPos.x, currentPos.y + Bounds().Height()); 3490 fCaretOffset = OffsetAt(nextPos); 3491 nextPos = PointAt(fCaretOffset); 3492 _ScrollBy(0, nextPos.y - currentPos.y); 3493 3494 if (!fEditable) 3495 break; 3496 3497 if (!shiftKeyDown) 3498 selStart = selEnd = fCaretOffset; 3499 else if (fCaretOffset != lastClickOffset) { 3500 if (fCaretOffset > fSelEnd) { 3501 // extend selection to the bottom 3502 selEnd = fCaretOffset; 3503 if (lastClickOffset < fSelEnd) { 3504 // caret has jumped across "anchor" 3505 selStart = fSelEnd; 3506 } 3507 } else { 3508 // shrink selection from the top 3509 selStart = fCaretOffset; 3510 } 3511 } 3512 3513 break; 3514 } 3515 } 3516 3517 if (fEditable) { 3518 if (shiftKeyDown) 3519 Select(selStart, selEnd); 3520 else 3521 Select(fCaretOffset, fCaretOffset); 3522 3523 ScrollToOffset(fCaretOffset); 3524 } 3525 } 3526 3527 3528 /*! Handles when an alpha-numeric key is pressed. 3529 3530 \param bytes The string or character associated with the key. 3531 \param numBytes The amount of bytes containes in "bytes". 3532 */ 3533 void 3534 BTextView::_HandleAlphaKey(const char* bytes, int32 numBytes) 3535 { 3536 // TODO: block input if not editable (Andrew) 3537 if (fUndo) { 3538 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>(fUndo); 3539 if (!undoBuffer) { 3540 delete fUndo; 3541 fUndo = undoBuffer = new TypingUndoBuffer(this); 3542 } 3543 undoBuffer->InputCharacter(numBytes); 3544 } 3545 3546 if (fSelStart != fSelEnd) { 3547 Highlight(fSelStart, fSelEnd); 3548 DeleteText(fSelStart, fSelEnd); 3549 } 3550 3551 if (fAutoindent && numBytes == 1 && *bytes == B_ENTER) { 3552 int32 start, offset; 3553 start = offset = OffsetAt(_LineAt(fSelStart)); 3554 3555 while (ByteAt(offset) != '\0' && 3556 (ByteAt(offset) == B_TAB || ByteAt(offset) == B_SPACE) 3557 && offset < fSelStart) 3558 offset++; 3559 3560 _DoInsertText(bytes, numBytes, fSelStart, NULL); 3561 if (start != offset) 3562 _DoInsertText(Text() + start, offset - start, fSelStart, NULL); 3563 } else 3564 _DoInsertText(bytes, numBytes, fSelStart, NULL); 3565 3566 fCaretOffset = fSelEnd; 3567 3568 ScrollToOffset(fCaretOffset); 3569 } 3570 3571 3572 /*! Redraw the text between the two given offsets, recalculating line-breaks 3573 if needed. 3574 3575 \param fromOffset The offset from where to refresh. 3576 \param toOffset The offset where to refresh to. 3577 \param scroll If \c true, scroll the view to the end offset. 3578 */ 3579 void 3580 BTextView::_Refresh(int32 fromOffset, int32 toOffset, bool scroll) 3581 { 3582 // TODO: Cleanup 3583 float saveHeight = fTextRect.Height(); 3584 float saveWidth = fTextRect.Width(); 3585 int32 fromLine = _LineAt(fromOffset); 3586 int32 toLine = _LineAt(toOffset); 3587 int32 saveFromLine = fromLine; 3588 int32 saveToLine = toLine; 3589 3590 _RecalculateLineBreaks(&fromLine, &toLine); 3591 3592 // TODO: Maybe there is still something we can do without a window... 3593 if (!Window()) 3594 return; 3595 3596 BRect bounds = Bounds(); 3597 float newHeight = fTextRect.Height(); 3598 3599 // if the line breaks have changed, force an erase 3600 if (fromLine != saveFromLine || toLine != saveToLine 3601 || newHeight != saveHeight) { 3602 fromOffset = -1; 3603 } 3604 3605 if (newHeight != saveHeight) { 3606 // the text area has changed 3607 if (newHeight < saveHeight) 3608 toLine = _LineAt(BPoint(0.0f, saveHeight + fTextRect.top)); 3609 else 3610 toLine = _LineAt(BPoint(0.0f, newHeight + fTextRect.top)); 3611 } 3612 3613 // draw only those lines that are visible 3614 int32 fromVisible = _LineAt(BPoint(0.0f, bounds.top)); 3615 int32 toVisible = _LineAt(BPoint(0.0f, bounds.bottom)); 3616 fromLine = max_c(fromVisible, fromLine); 3617 toLine = min_c(toLine, toVisible); 3618 3619 _AutoResize(false); 3620 3621 _RequestDrawLines(fromLine, toLine); 3622 3623 // erase the area below the text 3624 BRect eraseRect = bounds; 3625 eraseRect.top = fTextRect.top + (*fLines)[fLines->NumLines()]->origin; 3626 eraseRect.bottom = fTextRect.top + saveHeight; 3627 if (eraseRect.bottom > eraseRect.top && eraseRect.Intersects(bounds)) { 3628 SetLowColor(ViewColor()); 3629 FillRect(eraseRect, B_SOLID_LOW); 3630 } 3631 3632 // update the scroll bars if the text area has changed 3633 if (newHeight != saveHeight || fMinTextRectWidth != saveWidth) 3634 _UpdateScrollbars(); 3635 3636 if (scroll) 3637 ScrollToOffset(fSelEnd); 3638 3639 Flush(); 3640 } 3641 3642 3643 /*! Recalculate line breaks between two lines. 3644 3645 \param startLine The line number to start recalculating line breaks. 3646 \param endLine The line number to stop recalculating line breaks. 3647 */ 3648 void 3649 BTextView::_RecalculateLineBreaks(int32* startLine, int32* endLine) 3650 { 3651 CALLED(); 3652 3653 // are we insane? 3654 *startLine = (*startLine < 0) ? 0 : *startLine; 3655 *endLine = (*endLine > fLines->NumLines() - 1) ? fLines->NumLines() - 1 3656 : *endLine; 3657 3658 int32 textLength = fText->Length(); 3659 int32 lineIndex = (*startLine > 0) ? *startLine - 1 : 0; 3660 int32 recalThreshold = (*fLines)[*endLine + 1]->offset; 3661 float width = max_c(fTextRect.Width(), 10); 3662 // TODO: The minimum width of 10 is a work around for the following 3663 // problem: If the text rect is too small, we are not calculating any 3664 // line heights, not even for the first line. Maybe this is a bug 3665 // in the algorithm, but other places in the class rely on at least 3666 // the first line to return a valid height. Maybe "10" should really 3667 // be the width of the very first glyph instead. 3668 STELine* curLine = (*fLines)[lineIndex]; 3669 STELine* nextLine = curLine + 1; 3670 3671 do { 3672 float ascent, descent; 3673 int32 fromOffset = curLine->offset; 3674 int32 toOffset = _FindLineBreak(fromOffset, &ascent, &descent, &width); 3675 3676 curLine->ascent = ascent; 3677 curLine->width = width; 3678 3679 // we want to advance at least by one character 3680 int32 nextOffset = _NextInitialByte(fromOffset); 3681 if (toOffset < nextOffset && fromOffset < textLength) 3682 toOffset = nextOffset; 3683 3684 lineIndex++; 3685 STELine saveLine = *nextLine; 3686 if (lineIndex > fLines->NumLines() || toOffset < nextLine->offset) { 3687 // the new line comes before the old line start, add a line 3688 STELine newLine; 3689 newLine.offset = toOffset; 3690 newLine.origin = ceilf(curLine->origin + ascent + descent) + 1; 3691 newLine.ascent = 0; 3692 fLines->InsertLine(&newLine, lineIndex); 3693 } else { 3694 // update the existing line 3695 nextLine->offset = toOffset; 3696 nextLine->origin = ceilf(curLine->origin + ascent + descent) + 1; 3697 3698 // remove any lines that start before the current line 3699 while (lineIndex < fLines->NumLines() 3700 && toOffset >= ((*fLines)[lineIndex] + 1)->offset) { 3701 fLines->RemoveLines(lineIndex + 1); 3702 } 3703 3704 nextLine = (*fLines)[lineIndex]; 3705 if (nextLine->offset == saveLine.offset) { 3706 if (nextLine->offset >= recalThreshold) { 3707 if (nextLine->origin != saveLine.origin) 3708 fLines->BumpOrigin(nextLine->origin - saveLine.origin, 3709 lineIndex + 1); 3710 break; 3711 } 3712 } else { 3713 if (lineIndex > 0 && lineIndex == *startLine) 3714 *startLine = lineIndex - 1; 3715 } 3716 } 3717 3718 curLine = (*fLines)[lineIndex]; 3719 nextLine = curLine + 1; 3720 } while (curLine->offset < textLength); 3721 3722 // make sure that the sentinel line (which starts at the end of the buffer) 3723 // has always a width of 0 3724 (*fLines)[fLines->NumLines()]->width = 0; 3725 3726 // update the text rect 3727 float newHeight = TextHeight(0, fLines->NumLines() - 1); 3728 fTextRect.bottom = fTextRect.top + newHeight; 3729 if (!fWrap) { 3730 fMinTextRectWidth = fLines->MaxWidth(); 3731 fTextRect.right = ceilf(fTextRect.left + fMinTextRectWidth); 3732 } 3733 3734 *endLine = lineIndex - 1; 3735 *startLine = min_c(*startLine, *endLine); 3736 } 3737 3738 3739 int32 3740 BTextView::_FindLineBreak(int32 fromOffset, float* _ascent, float* _descent, 3741 float* inOutWidth) 3742 { 3743 *_ascent = 0.0; 3744 *_descent = 0.0; 3745 3746 const int32 limit = fText->Length(); 3747 3748 // is fromOffset at the end? 3749 if (fromOffset >= limit) { 3750 // try to return valid height info anyway 3751 if (fStyles->NumRuns() > 0) { 3752 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, _ascent, 3753 _descent); 3754 } else { 3755 if (fStyles->IsValidNullStyle()) { 3756 const BFont* font = NULL; 3757 fStyles->GetNullStyle(&font, NULL); 3758 3759 font_height fh; 3760 font->GetHeight(&fh); 3761 *_ascent = fh.ascent; 3762 *_descent = fh.descent + fh.leading; 3763 } 3764 } 3765 *inOutWidth = 0; 3766 3767 return limit; 3768 } 3769 3770 int32 offset = fromOffset; 3771 3772 if (!fWrap) { 3773 // Text wrapping is turned off. 3774 // Just find the offset of the first \n character 3775 offset = limit - fromOffset; 3776 fText->FindChar(B_ENTER, fromOffset, &offset); 3777 offset += fromOffset; 3778 int32 toOffset = (offset < limit) ? offset : limit; 3779 3780 *inOutWidth = _TabExpandedStyledWidth(fromOffset, toOffset - fromOffset, 3781 _ascent, _descent); 3782 3783 return offset < limit ? offset + 1 : limit; 3784 } 3785 3786 bool done = false; 3787 float ascent = 0.0; 3788 float descent = 0.0; 3789 int32 delta = 0; 3790 float deltaWidth = 0.0; 3791 float strWidth = 0.0; 3792 uchar theChar; 3793 3794 // wrap the text 3795 while (offset < limit && !done) { 3796 // find the next line break candidate 3797 for (; (offset + delta) < limit; delta++) { 3798 if (CanEndLine(offset + delta)) { 3799 theChar = fText->RealCharAt(offset + delta); 3800 if (theChar != B_SPACE && theChar != B_TAB 3801 && theChar != B_ENTER) { 3802 // we are scanning for trailing whitespace below, so we 3803 // have to skip non-whitespace characters, that can end 3804 // the line, here 3805 delta++; 3806 } 3807 break; 3808 } 3809 } 3810 3811 int32 deltaBeforeWhitespace = delta; 3812 // now skip over trailing whitespace, if any 3813 for (; (offset + delta) < limit; delta++) { 3814 theChar = fText->RealCharAt(offset + delta); 3815 if (theChar == B_ENTER) { 3816 // found a newline, we're done! 3817 done = true; 3818 delta++; 3819 break; 3820 } else if (theChar != B_SPACE && theChar != B_TAB) { 3821 // stop at anything else than trailing whitespace 3822 break; 3823 } 3824 } 3825 3826 delta = max_c(delta, 1); 3827 3828 // do not include B_ENTER-terminator into width & height calculations 3829 deltaWidth = _TabExpandedStyledWidth(offset, 3830 done ? delta - 1 : delta, &ascent, &descent); 3831 strWidth += deltaWidth; 3832 3833 if (strWidth >= *inOutWidth) { 3834 // we've found where the line will wrap 3835 done = true; 3836 3837 // we have included trailing whitespace in the width computation 3838 // above, but that is not being shown anyway, so we try again 3839 // without the trailing whitespace 3840 if (delta == deltaBeforeWhitespace) { 3841 // there is no trailing whitespace, no point in trying 3842 break; 3843 } 3844 3845 // reset string width to start of current run ... 3846 strWidth -= deltaWidth; 3847 3848 // ... and compute the resulting width (of visible characters) 3849 strWidth += _StyledWidth(offset, deltaBeforeWhitespace, NULL, NULL); 3850 if (strWidth >= *inOutWidth) { 3851 // width of visible characters exceeds line, we need to wrap 3852 // before the current "word" 3853 break; 3854 } 3855 } 3856 3857 *_ascent = max_c(ascent, *_ascent); 3858 *_descent = max_c(descent, *_descent); 3859 3860 offset += delta; 3861 delta = 0; 3862 } 3863 3864 if (offset - fromOffset < 1) { 3865 // there weren't any words that fit entirely in this line 3866 // force a break in the middle of a word 3867 *_ascent = 0.0; 3868 *_descent = 0.0; 3869 strWidth = 0.0; 3870 3871 int32 current = fromOffset; 3872 for (offset = _NextInitialByte(current); current < limit; 3873 current = offset, offset = _NextInitialByte(offset)) { 3874 strWidth += _StyledWidth(current, offset - current, &ascent, 3875 &descent); 3876 if (strWidth >= *inOutWidth) { 3877 offset = _PreviousInitialByte(offset); 3878 break; 3879 } 3880 3881 *_ascent = max_c(ascent, *_ascent); 3882 *_descent = max_c(descent, *_descent); 3883 } 3884 } 3885 3886 return min_c(offset, limit); 3887 } 3888 3889 3890 int32 3891 BTextView::_PreviousLineStart(int32 offset) 3892 { 3893 if (offset <= 0) 3894 return 0; 3895 3896 while (offset > 0) { 3897 offset = _PreviousInitialByte(offset); 3898 if (_CharClassification(offset) == CHAR_CLASS_WHITESPACE 3899 && ByteAt(offset) == B_ENTER) { 3900 return offset + 1; 3901 } 3902 } 3903 3904 return offset; 3905 } 3906 3907 3908 int32 3909 BTextView::_NextLineEnd(int32 offset) 3910 { 3911 int32 textLen = fText->Length(); 3912 if (offset >= textLen) 3913 return textLen; 3914 3915 while (offset < textLen) { 3916 if (_CharClassification(offset) == CHAR_CLASS_WHITESPACE 3917 && ByteAt(offset) == B_ENTER) { 3918 break; 3919 } 3920 offset = _NextInitialByte(offset); 3921 } 3922 3923 return offset; 3924 } 3925 3926 3927 int32 3928 BTextView::_PreviousWordBoundary(int32 offset) 3929 { 3930 uint32 charType = _CharClassification(offset); 3931 int32 previous; 3932 while (offset > 0) { 3933 previous = _PreviousInitialByte(offset); 3934 if (_CharClassification(previous) != charType) 3935 break; 3936 offset = previous; 3937 } 3938 3939 return offset; 3940 } 3941 3942 3943 int32 3944 BTextView::_NextWordBoundary(int32 offset) 3945 { 3946 int32 textLen = fText->Length(); 3947 uint32 charType = _CharClassification(offset); 3948 while (offset < textLen) { 3949 offset = _NextInitialByte(offset); 3950 if (_CharClassification(offset) != charType) 3951 break; 3952 } 3953 3954 return offset; 3955 } 3956 3957 3958 int32 3959 BTextView::_PreviousWordStart(int32 offset) 3960 { 3961 if (offset <= 1) 3962 return 0; 3963 3964 --offset; 3965 // need to look at previous char 3966 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) { 3967 // skip non-word characters 3968 while (offset > 0) { 3969 offset = _PreviousInitialByte(offset); 3970 if (_CharClassification(offset) == CHAR_CLASS_DEFAULT) 3971 break; 3972 } 3973 } 3974 while (offset > 0) { 3975 // skip to start of word 3976 int32 previous = _PreviousInitialByte(offset); 3977 if (_CharClassification(previous) != CHAR_CLASS_DEFAULT) 3978 break; 3979 offset = previous; 3980 } 3981 3982 return offset; 3983 } 3984 3985 3986 int32 3987 BTextView::_NextWordEnd(int32 offset) 3988 { 3989 int32 textLen = fText->Length(); 3990 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) { 3991 // skip non-word characters 3992 while (offset < textLen) { 3993 offset = _NextInitialByte(offset); 3994 if (_CharClassification(offset) == CHAR_CLASS_DEFAULT) 3995 break; 3996 } 3997 } 3998 while (offset < textLen) { 3999 // skip to end of word 4000 offset = _NextInitialByte(offset); 4001 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) 4002 break; 4003 } 4004 4005 return offset; 4006 } 4007 4008 4009 /*! Returns the width used by the characters starting at the given 4010 offset with the given length, expanding all tab characters as needed. 4011 */ 4012 float 4013 BTextView::_TabExpandedStyledWidth(int32 offset, int32 length, float* _ascent, 4014 float* _descent) const 4015 { 4016 float ascent = 0.0; 4017 float descent = 0.0; 4018 float maxAscent = 0.0; 4019 float maxDescent = 0.0; 4020 4021 float width = 0.0; 4022 int32 numBytes = length; 4023 bool foundTab = false; 4024 do { 4025 foundTab = fText->FindChar(B_TAB, offset, &numBytes); 4026 width += _StyledWidth(offset, numBytes, &ascent, &descent); 4027 4028 if (maxAscent < ascent) 4029 maxAscent = ascent; 4030 if (maxDescent < descent) 4031 maxDescent = descent; 4032 4033 if (foundTab) { 4034 width += _ActualTabWidth(width); 4035 numBytes++; 4036 } 4037 4038 offset += numBytes; 4039 length -= numBytes; 4040 numBytes = length; 4041 } while (foundTab && length > 0); 4042 4043 if (_ascent != NULL) 4044 *_ascent = maxAscent; 4045 if (_descent != NULL) 4046 *_descent = maxDescent; 4047 4048 return width; 4049 } 4050 4051 4052 /*! Calculate the width of the text within the given limits. 4053 4054 \param fromOffset The offset where to start. 4055 \param length The length of the text to examine. 4056 \param _ascent A pointer to a float which will contain the maximum ascent. 4057 \param _descent A pointer to a float which will contain the maximum descent. 4058 4059 \return The width for the text within the given limits. 4060 */ 4061 float 4062 BTextView::_StyledWidth(int32 fromOffset, int32 length, float* _ascent, 4063 float* _descent) const 4064 { 4065 if (length == 0) { 4066 // determine height of char at given offset, but return empty width 4067 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, _ascent, 4068 _descent); 4069 return 0.0; 4070 } 4071 4072 float result = 0.0; 4073 float ascent = 0.0; 4074 float descent = 0.0; 4075 float maxAscent = 0.0; 4076 float maxDescent = 0.0; 4077 4078 // iterate through the style runs 4079 const BFont* font = NULL; 4080 int32 numBytes; 4081 while ((numBytes = fStyles->Iterate(fromOffset, length, fInline, &font, 4082 NULL, &ascent, &descent)) != 0) { 4083 maxAscent = max_c(ascent, maxAscent); 4084 maxDescent = max_c(descent, maxDescent); 4085 4086 #if USE_WIDTHBUFFER 4087 // Use _BWidthBuffer_ if possible 4088 if (BPrivate::gWidthBuffer != NULL) { 4089 result += BPrivate::gWidthBuffer->StringWidth(*fText, fromOffset, 4090 numBytes, font); 4091 } else { 4092 #endif 4093 const char* text = fText->GetString(fromOffset, &numBytes); 4094 result += font->StringWidth(text, numBytes); 4095 4096 #if USE_WIDTHBUFFER 4097 } 4098 #endif 4099 4100 fromOffset += numBytes; 4101 length -= numBytes; 4102 } 4103 4104 if (_ascent != NULL) 4105 *_ascent = maxAscent; 4106 if (_descent != NULL) 4107 *_descent = maxDescent; 4108 4109 return result; 4110 } 4111 4112 4113 //! Calculate the actual tab width for the given location. 4114 float 4115 BTextView::_ActualTabWidth(float location) const 4116 { 4117 float tabWidth = fTabWidth - fmod(location, fTabWidth); 4118 if (round(tabWidth) == 0) 4119 tabWidth = fTabWidth; 4120 4121 return tabWidth; 4122 } 4123 4124 4125 void 4126 BTextView::_DoInsertText(const char* text, int32 length, int32 offset, 4127 const text_run_array* runs) 4128 { 4129 _CancelInputMethod(); 4130 4131 if (TextLength() + length > MaxBytes()) 4132 return; 4133 4134 if (fSelStart != fSelEnd) 4135 Select(fSelStart, fSelStart); 4136 4137 const int32 textLength = TextLength(); 4138 if (offset > textLength) 4139 offset = textLength; 4140 4141 // copy data into buffer 4142 InsertText(text, length, offset, runs); 4143 4144 // recalc line breaks and draw the text 4145 _Refresh(offset, offset + length, false); 4146 } 4147 4148 4149 void 4150 BTextView::_DoDeleteText(int32 fromOffset, int32 toOffset) 4151 { 4152 CALLED(); 4153 } 4154 4155 4156 void 4157 BTextView::_DrawLine(BView* view, const int32 &lineNum, 4158 const int32 &startOffset, const bool &erase, BRect &eraseRect, 4159 BRegion &inputRegion) 4160 { 4161 STELine* line = (*fLines)[lineNum]; 4162 float startLeft = fTextRect.left; 4163 if (startOffset != -1) { 4164 if (ByteAt(startOffset) == B_ENTER) { 4165 // StartOffset is a newline 4166 startLeft = PointAt(line->offset).x; 4167 } else 4168 startLeft = PointAt(startOffset).x; 4169 } 4170 else if (fAlignment != B_ALIGN_LEFT) { 4171 float alignmentOffset = fTextRect.Width() - LineWidth(lineNum); 4172 if (fAlignment == B_ALIGN_CENTER) 4173 alignmentOffset /= 2; 4174 startLeft = fTextRect.left + alignmentOffset; 4175 } 4176 4177 int32 length = (line + 1)->offset; 4178 if (startOffset != -1) 4179 length -= startOffset; 4180 else 4181 length -= line->offset; 4182 4183 // DrawString() chokes if you draw a newline 4184 if (ByteAt((line + 1)->offset - 1) == B_ENTER) 4185 length--; 4186 4187 view->MovePenTo(startLeft, line->origin + line->ascent + fTextRect.top + 1); 4188 4189 if (erase) { 4190 eraseRect.top = line->origin + fTextRect.top; 4191 eraseRect.bottom = (line + 1)->origin + fTextRect.top; 4192 view->FillRect(eraseRect, B_SOLID_LOW); 4193 } 4194 4195 // do we have any text to draw? 4196 if (length <= 0) 4197 return; 4198 4199 bool foundTab = false; 4200 int32 tabChars = 0; 4201 int32 numTabs = 0; 4202 int32 offset = startOffset != -1 ? startOffset : line->offset; 4203 const BFont* font = NULL; 4204 const rgb_color* color = NULL; 4205 int32 numBytes; 4206 drawing_mode defaultTextRenderingMode = DrawingMode(); 4207 // iterate through each style on this line 4208 while ((numBytes = fStyles->Iterate(offset, length, fInline, &font, 4209 &color)) != 0) { 4210 view->SetFont(font); 4211 view->SetHighColor(*color); 4212 4213 tabChars = min_c(numBytes, length); 4214 do { 4215 foundTab = fText->FindChar(B_TAB, offset, &tabChars); 4216 if (foundTab) { 4217 do { 4218 numTabs++; 4219 if (ByteAt(offset + tabChars + numTabs) != B_TAB) 4220 break; 4221 } while ((tabChars + numTabs) < numBytes); 4222 } 4223 4224 drawing_mode textRenderingMode = defaultTextRenderingMode; 4225 4226 if (inputRegion.CountRects() > 0 4227 && ((offset <= fInline->Offset() 4228 && fInline->Offset() < offset + tabChars) 4229 || (fInline->Offset() <= offset 4230 && offset < fInline->Offset() + fInline->Length()))) { 4231 4232 textRenderingMode = B_OP_OVER; 4233 4234 BRegion textRegion; 4235 GetTextRegion(offset, offset + length, &textRegion); 4236 4237 textRegion.IntersectWith(&inputRegion); 4238 view->PushState(); 4239 4240 // Highlight in blue the inputted text 4241 view->SetHighColor(kBlueInputColor); 4242 view->FillRect(textRegion.Frame()); 4243 4244 // Highlight in red the selected part 4245 if (fInline->SelectionLength() > 0) { 4246 BRegion selectedRegion; 4247 GetTextRegion(fInline->Offset() 4248 + fInline->SelectionOffset(), fInline->Offset() 4249 + fInline->SelectionOffset() 4250 + fInline->SelectionLength(), &selectedRegion); 4251 4252 textRegion.IntersectWith(&selectedRegion); 4253 4254 view->SetHighColor(kRedInputColor); 4255 view->FillRect(textRegion.Frame()); 4256 } 4257 4258 view->PopState(); 4259 } 4260 4261 int32 returnedBytes = tabChars; 4262 const char* stringToDraw = fText->GetString(offset, &returnedBytes); 4263 view->SetDrawingMode(textRenderingMode); 4264 view->DrawString(stringToDraw, returnedBytes); 4265 if (foundTab) { 4266 float penPos = PenLocation().x - fTextRect.left; 4267 float tabWidth = _ActualTabWidth(penPos); 4268 if (numTabs > 1) 4269 tabWidth += ((numTabs - 1) * fTabWidth); 4270 4271 view->MovePenBy(tabWidth, 0.0); 4272 tabChars += numTabs; 4273 } 4274 4275 offset += tabChars; 4276 length -= tabChars; 4277 numBytes -= tabChars; 4278 tabChars = min_c(numBytes, length); 4279 numTabs = 0; 4280 } while (foundTab && tabChars > 0); 4281 } 4282 } 4283 4284 4285 void 4286 BTextView::_DrawLines(int32 startLine, int32 endLine, int32 startOffset, 4287 bool erase) 4288 { 4289 if (!Window()) 4290 return; 4291 4292 // clip the text 4293 BRect textRect(fTextRect); 4294 float minWidth 4295 = Bounds().Width() - fLayoutData->leftInset - fLayoutData->rightInset; 4296 if (textRect.Width() < minWidth) 4297 textRect.right = textRect.left + minWidth; 4298 BRect clipRect = Bounds() & textRect; 4299 clipRect.InsetBy(-1, -1); 4300 4301 BRegion newClip; 4302 newClip.Set(clipRect); 4303 ConstrainClippingRegion(&newClip); 4304 4305 // set the low color to the view color so that 4306 // drawing to a non-white background will work 4307 SetLowColor(ViewColor()); 4308 4309 BView* view = NULL; 4310 if (fOffscreen == NULL) 4311 view = this; 4312 else { 4313 fOffscreen->Lock(); 4314 view = fOffscreen->ChildAt(0); 4315 view->SetLowColor(ViewColor()); 4316 view->FillRect(view->Bounds(), B_SOLID_LOW); 4317 } 4318 4319 long maxLine = fLines->NumLines() - 1; 4320 if (startLine < 0) 4321 startLine = 0; 4322 if (endLine > maxLine) 4323 endLine = maxLine; 4324 4325 // TODO: See if we can avoid this 4326 if (fAlignment != B_ALIGN_LEFT) 4327 erase = true; 4328 4329 BRect eraseRect = clipRect; 4330 int32 startEraseLine = startLine; 4331 STELine* line = (*fLines)[startLine]; 4332 4333 if (erase && startOffset != -1 && fAlignment == B_ALIGN_LEFT) { 4334 // erase only to the right of startOffset 4335 startEraseLine++; 4336 int32 startErase = startOffset; 4337 4338 BPoint erasePoint = PointAt(startErase); 4339 eraseRect.left = erasePoint.x; 4340 eraseRect.top = erasePoint.y; 4341 eraseRect.bottom = (line + 1)->origin + fTextRect.top; 4342 4343 view->FillRect(eraseRect, B_SOLID_LOW); 4344 4345 eraseRect = clipRect; 4346 } 4347 4348 BRegion inputRegion; 4349 if (fInline != NULL && fInline->IsActive()) { 4350 GetTextRegion(fInline->Offset(), fInline->Offset() + fInline->Length(), 4351 &inputRegion); 4352 } 4353 4354 //BPoint leftTop(startLeft, line->origin); 4355 for (int32 lineNum = startLine; lineNum <= endLine; lineNum++) { 4356 const bool eraseThisLine = erase && lineNum >= startEraseLine; 4357 _DrawLine(view, lineNum, startOffset, eraseThisLine, eraseRect, 4358 inputRegion); 4359 startOffset = -1; 4360 // Set this to -1 so the next iteration will use the line offset 4361 } 4362 4363 // draw the caret/hilite the selection 4364 if (fActive) { 4365 if (fSelStart != fSelEnd) { 4366 if (fSelectable) 4367 Highlight(fSelStart, fSelEnd); 4368 } else { 4369 if (fCaretVisible) 4370 _DrawCaret(fSelStart, true); 4371 } 4372 } 4373 4374 if (fOffscreen != NULL) { 4375 view->Sync(); 4376 /*BPoint penLocation = view->PenLocation(); 4377 BRect drawRect(leftTop.x, leftTop.y, penLocation.x, penLocation.y); 4378 DrawBitmap(fOffscreen, drawRect, drawRect);*/ 4379 fOffscreen->Unlock(); 4380 } 4381 4382 ConstrainClippingRegion(NULL); 4383 } 4384 4385 4386 void 4387 BTextView::_RequestDrawLines(int32 startLine, int32 endLine) 4388 { 4389 if (!Window()) 4390 return; 4391 4392 long maxLine = fLines->NumLines() - 1; 4393 4394 STELine* from = (*fLines)[startLine]; 4395 STELine* to = endLine == maxLine ? NULL : (*fLines)[endLine + 1]; 4396 BRect invalidRect(Bounds().left, from->origin + fTextRect.top, 4397 Bounds().right, 4398 to != NULL ? to->origin + fTextRect.top : fTextRect.bottom); 4399 Invalidate(invalidRect); 4400 Window()->UpdateIfNeeded(); 4401 } 4402 4403 4404 void 4405 BTextView::_DrawCaret(int32 offset, bool visible) 4406 { 4407 float lineHeight; 4408 BPoint caretPoint = PointAt(offset, &lineHeight); 4409 caretPoint.x = min_c(caretPoint.x, fTextRect.right); 4410 4411 BRect caretRect; 4412 caretRect.left = caretRect.right = caretPoint.x; 4413 caretRect.top = caretPoint.y; 4414 caretRect.bottom = caretPoint.y + lineHeight - 1; 4415 4416 if (visible) 4417 InvertRect(caretRect); 4418 else 4419 Invalidate(caretRect); 4420 } 4421 4422 4423 inline void 4424 BTextView::_ShowCaret() 4425 { 4426 if (fActive && !fCaretVisible && fEditable && fSelStart == fSelEnd) 4427 _InvertCaret(); 4428 } 4429 4430 4431 inline void 4432 BTextView::_HideCaret() 4433 { 4434 if (fCaretVisible && fSelStart == fSelEnd) 4435 _InvertCaret(); 4436 } 4437 4438 4439 //! Hides the caret if it is being shown, and if it's hidden, shows it. 4440 void 4441 BTextView::_InvertCaret() 4442 { 4443 fCaretVisible = !fCaretVisible; 4444 _DrawCaret(fSelStart, fCaretVisible); 4445 fCaretTime = system_time(); 4446 } 4447 4448 4449 /*! Place the dragging caret at the given offset. 4450 4451 \param offset The offset (zero based within the object's text) where to 4452 place the dragging caret. If it's -1, hide the caret. 4453 */ 4454 void 4455 BTextView::_DragCaret(int32 offset) 4456 { 4457 // does the caret need to move? 4458 if (offset == fDragOffset) 4459 return; 4460 4461 // hide the previous drag caret 4462 if (fDragOffset != -1) 4463 _DrawCaret(fDragOffset, false); 4464 4465 // do we have a new location? 4466 if (offset != -1) { 4467 if (fActive) { 4468 // ignore if offset is within active selection 4469 if (offset >= fSelStart && offset <= fSelEnd) { 4470 fDragOffset = -1; 4471 return; 4472 } 4473 } 4474 4475 _DrawCaret(offset, true); 4476 } 4477 4478 fDragOffset = offset; 4479 } 4480 4481 4482 void 4483 BTextView::_StopMouseTracking() 4484 { 4485 delete fTrackingMouse; 4486 fTrackingMouse = NULL; 4487 } 4488 4489 4490 bool 4491 BTextView::_PerformMouseUp(BPoint where) 4492 { 4493 if (fTrackingMouse == NULL) 4494 return false; 4495 4496 if (fTrackingMouse->selectionRect.IsValid()) 4497 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset); 4498 4499 _StopMouseTracking(); 4500 // adjust cursor if necessary 4501 _TrackMouse(where, NULL, true); 4502 4503 return true; 4504 } 4505 4506 4507 bool 4508 BTextView::_PerformMouseMoved(BPoint where, uint32 code) 4509 { 4510 fWhere = where; 4511 4512 if (fTrackingMouse == NULL) 4513 return false; 4514 4515 int32 currentOffset = OffsetAt(where); 4516 if (fTrackingMouse->selectionRect.IsValid()) { 4517 // we are tracking the mouse for drag action, if the mouse has moved 4518 // to another index or more than three pixels from where it was clicked, 4519 // we initiate a drag now: 4520 if (currentOffset != fTrackingMouse->clickOffset 4521 || fabs(fTrackingMouse->where.x - where.x) > 3 4522 || fabs(fTrackingMouse->where.y - where.y) > 3) { 4523 _StopMouseTracking(); 4524 _InitiateDrag(); 4525 return true; 4526 } 4527 return false; 4528 } 4529 4530 switch (fClickCount) { 4531 case 3: 4532 // triple click, extend selection linewise 4533 if (currentOffset <= fTrackingMouse->anchor) { 4534 fTrackingMouse->selStart 4535 = (*fLines)[_LineAt(currentOffset)]->offset; 4536 fTrackingMouse->selEnd = fTrackingMouse->shiftDown 4537 ? fSelEnd 4538 : (*fLines)[_LineAt(fTrackingMouse->anchor) + 1]->offset; 4539 } else { 4540 fTrackingMouse->selStart 4541 = fTrackingMouse->shiftDown 4542 ? fSelStart 4543 : (*fLines)[_LineAt(fTrackingMouse->anchor)]->offset; 4544 fTrackingMouse->selEnd 4545 = (*fLines)[_LineAt(currentOffset) + 1]->offset; 4546 } 4547 break; 4548 4549 case 2: 4550 // double click, extend selection wordwise 4551 if (currentOffset <= fTrackingMouse->anchor) { 4552 fTrackingMouse->selStart = _PreviousWordBoundary(currentOffset); 4553 fTrackingMouse->selEnd 4554 = fTrackingMouse->shiftDown 4555 ? fSelEnd 4556 : _NextWordBoundary(fTrackingMouse->anchor); 4557 } else { 4558 fTrackingMouse->selStart 4559 = fTrackingMouse->shiftDown 4560 ? fSelStart 4561 : _PreviousWordBoundary(fTrackingMouse->anchor); 4562 fTrackingMouse->selEnd = _NextWordBoundary(currentOffset); 4563 } 4564 break; 4565 4566 default: 4567 // new click, extend selection char by char 4568 if (currentOffset <= fTrackingMouse->anchor) { 4569 fTrackingMouse->selStart = currentOffset; 4570 fTrackingMouse->selEnd 4571 = fTrackingMouse->shiftDown 4572 ? fSelEnd : fTrackingMouse->anchor; 4573 } else { 4574 fTrackingMouse->selStart 4575 = fTrackingMouse->shiftDown 4576 ? fSelStart : fTrackingMouse->anchor; 4577 fTrackingMouse->selEnd = currentOffset; 4578 } 4579 break; 4580 } 4581 4582 // position caret to follow the direction of the selection 4583 if (fTrackingMouse->selEnd != fSelEnd) 4584 fCaretOffset = fTrackingMouse->selEnd; 4585 else if (fTrackingMouse->selStart != fSelStart) 4586 fCaretOffset = fTrackingMouse->selStart; 4587 4588 Select(fTrackingMouse->selStart, fTrackingMouse->selEnd); 4589 _TrackMouse(where, NULL); 4590 4591 return true; 4592 } 4593 4594 4595 /*! Tracks the mouse position, doing special actions like changing the 4596 view cursor. 4597 4598 \param where The point where the mouse has moved. 4599 \param message The dragging message, if there is any. 4600 \param force Passed as second parameter of SetViewCursor() 4601 */ 4602 void 4603 BTextView::_TrackMouse(BPoint where, const BMessage* message, bool force) 4604 { 4605 BRegion textRegion; 4606 GetTextRegion(fSelStart, fSelEnd, &textRegion); 4607 4608 if (message && AcceptsDrop(message)) 4609 _TrackDrag(where); 4610 else if ((fSelectable || fEditable) 4611 && (fTrackingMouse != NULL || !textRegion.Contains(where))) { 4612 SetViewCursor(B_CURSOR_I_BEAM, force); 4613 } else 4614 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT, force); 4615 } 4616 4617 4618 //! Tracks the mouse position when the user is dragging some data. 4619 void 4620 BTextView::_TrackDrag(BPoint where) 4621 { 4622 CALLED(); 4623 if (Bounds().Contains(where)) 4624 _DragCaret(OffsetAt(where)); 4625 } 4626 4627 4628 //! Initiates a drag operation. 4629 void 4630 BTextView::_InitiateDrag() 4631 { 4632 BMessage dragMessage(B_MIME_DATA); 4633 BBitmap* dragBitmap = NULL; 4634 BPoint bitmapPoint; 4635 BHandler* dragHandler = NULL; 4636 4637 GetDragParameters(&dragMessage, &dragBitmap, &bitmapPoint, &dragHandler); 4638 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); 4639 4640 if (dragBitmap != NULL) 4641 DragMessage(&dragMessage, dragBitmap, bitmapPoint, dragHandler); 4642 else { 4643 BRegion region; 4644 GetTextRegion(fSelStart, fSelEnd, ®ion); 4645 BRect bounds = Bounds(); 4646 BRect dragRect = region.Frame(); 4647 if (!bounds.Contains(dragRect)) 4648 dragRect = bounds & dragRect; 4649 4650 DragMessage(&dragMessage, dragRect, dragHandler); 4651 } 4652 4653 BMessenger messenger(this); 4654 BMessage message(_DISPOSE_DRAG_); 4655 fDragRunner = new (nothrow) BMessageRunner(messenger, &message, 100000); 4656 } 4657 4658 4659 //! Handles when some data is dropped on the view. 4660 bool 4661 BTextView::_MessageDropped(BMessage* message, BPoint where, BPoint offset) 4662 { 4663 ASSERT(message); 4664 4665 void* from = NULL; 4666 bool internalDrop = false; 4667 if (message->FindPointer("be:originator", &from) == B_OK 4668 && from == this && fSelEnd != fSelStart) 4669 internalDrop = true; 4670 4671 _DragCaret(-1); 4672 4673 delete fDragRunner; 4674 fDragRunner = NULL; 4675 4676 _TrackMouse(where, NULL); 4677 4678 // are we sure we like this message? 4679 if (!AcceptsDrop(message)) 4680 return false; 4681 4682 int32 dropOffset = OffsetAt(where); 4683 if (dropOffset > TextLength()) 4684 dropOffset = TextLength(); 4685 4686 // if this view initiated the drag, move instead of copy 4687 if (internalDrop) { 4688 // dropping onto itself? 4689 if (dropOffset >= fSelStart && dropOffset <= fSelEnd) 4690 return true; 4691 } 4692 4693 ssize_t dataLength = 0; 4694 const char* text = NULL; 4695 entry_ref ref; 4696 if (message->FindData("text/plain", B_MIME_TYPE, (const void**)&text, 4697 &dataLength) == B_OK) { 4698 text_run_array* runArray = NULL; 4699 ssize_t runLength = 0; 4700 if (fStylable) { 4701 message->FindData("application/x-vnd.Be-text_run_array", 4702 B_MIME_TYPE, (const void**)&runArray, &runLength); 4703 } 4704 4705 _FilterDisallowedChars((char*)text, dataLength, runArray); 4706 4707 if (dataLength < 1) { 4708 beep(); 4709 return true; 4710 } 4711 4712 if (fUndo) { 4713 delete fUndo; 4714 fUndo = new DropUndoBuffer(this, text, dataLength, runArray, 4715 runLength, dropOffset, internalDrop); 4716 } 4717 4718 if (internalDrop) { 4719 if (dropOffset > fSelEnd) 4720 dropOffset -= dataLength; 4721 Delete(); 4722 } 4723 4724 Insert(dropOffset, text, dataLength, runArray); 4725 } 4726 4727 return true; 4728 } 4729 4730 4731 void 4732 BTextView::_PerformAutoScrolling() 4733 { 4734 // Scroll the view a bit if mouse is outside the view bounds 4735 BRect bounds = Bounds(); 4736 BPoint scrollBy(B_ORIGIN); 4737 4738 // R5 does a pretty soft auto-scroll, we try to do the same by 4739 // simply scrolling the distance between cursor and border 4740 if (fWhere.x > bounds.right) { 4741 scrollBy.x = fWhere.x - bounds.right; 4742 } else if (fWhere.x < bounds.left) { 4743 scrollBy.x = fWhere.x - bounds.left; // negative value 4744 } 4745 4746 // prevent from scrolling out of view 4747 if (scrollBy.x != 0.0) { 4748 float rightMax = floorf(fTextRect.right + fLayoutData->rightInset); 4749 if (bounds.right + scrollBy.x > rightMax) 4750 scrollBy.x = rightMax - bounds.right; 4751 if (bounds.left + scrollBy.x < 0) 4752 scrollBy.x = -bounds.left; 4753 } 4754 4755 if (CountLines() > 1) { 4756 // scroll in Y only if multiple lines! 4757 if (fWhere.y > bounds.bottom) { 4758 scrollBy.y = fWhere.y - bounds.bottom; 4759 } else if (fWhere.y < bounds.top) { 4760 scrollBy.y = fWhere.y - bounds.top; // negative value 4761 } 4762 4763 // prevent from scrolling out of view 4764 if (scrollBy.y != 0.0) { 4765 float bottomMax = floorf(fTextRect.bottom 4766 + fLayoutData->bottomInset); 4767 if (bounds.bottom + scrollBy.y > bottomMax) 4768 scrollBy.y = bottomMax - bounds.bottom; 4769 if (bounds.top + scrollBy.y < 0) 4770 scrollBy.y = -bounds.top; 4771 } 4772 } 4773 4774 if (scrollBy != B_ORIGIN) 4775 ScrollBy(scrollBy.x, scrollBy.y); 4776 } 4777 4778 4779 //! Updates the scrollbars associated with the object (if any). 4780 void 4781 BTextView::_UpdateScrollbars() 4782 { 4783 BRect bounds(Bounds()); 4784 BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL); 4785 BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL); 4786 4787 // do we have a horizontal scroll bar? 4788 if (horizontalScrollBar != NULL) { 4789 long viewWidth = bounds.IntegerWidth(); 4790 long dataWidth = (long)ceilf(fTextRect.IntegerWidth() 4791 + fLayoutData->leftInset + fLayoutData->rightInset); 4792 4793 long maxRange = dataWidth - viewWidth; 4794 maxRange = max_c(maxRange, 0); 4795 4796 horizontalScrollBar->SetRange(0, (float)maxRange); 4797 horizontalScrollBar->SetProportion((float)viewWidth / (float)dataWidth); 4798 horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10); 4799 } 4800 4801 // how about a vertical scroll bar? 4802 if (verticalScrollBar != NULL) { 4803 long viewHeight = bounds.IntegerHeight(); 4804 long dataHeight = (long)ceilf(fTextRect.IntegerHeight() 4805 + fLayoutData->topInset + fLayoutData->bottomInset); 4806 4807 long maxRange = dataHeight - viewHeight; 4808 maxRange = max_c(maxRange, 0); 4809 4810 verticalScrollBar->SetRange(0, maxRange); 4811 verticalScrollBar->SetProportion((float)viewHeight / (float)dataHeight); 4812 verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight); 4813 } 4814 } 4815 4816 4817 //! Scrolls by the given offsets 4818 void 4819 BTextView::_ScrollBy(float horizontal, float vertical) 4820 { 4821 BRect bounds = Bounds(); 4822 _ScrollTo(bounds.left + horizontal, bounds.top + vertical); 4823 } 4824 4825 4826 //! Scrolls to the given position, making sure not to scroll out of bounds. 4827 void 4828 BTextView::_ScrollTo(float x, float y) 4829 { 4830 BRect bounds = Bounds(); 4831 long viewWidth = bounds.IntegerWidth(); 4832 long viewHeight = bounds.IntegerHeight(); 4833 4834 if (x > fTextRect.right - viewWidth) 4835 x = fTextRect.right - viewWidth; 4836 if (x < 0.0) 4837 x = 0.0; 4838 4839 if (y > fTextRect.bottom + fLayoutData->bottomInset - viewHeight) 4840 y = fTextRect.bottom + fLayoutData->bottomInset - viewHeight; 4841 if (y < 0.0) 4842 y = 0.0; 4843 4844 ScrollTo(x, y); 4845 } 4846 4847 4848 //! Autoresizes the view to fit the contained text. 4849 void 4850 BTextView::_AutoResize(bool redraw) 4851 { 4852 if (!fResizable) 4853 return; 4854 4855 BRect bounds = Bounds(); 4856 float oldWidth = bounds.Width(); 4857 float newWidth = ceilf(fLayoutData->leftInset + fTextRect.Width() 4858 + fLayoutData->rightInset); 4859 4860 if (fContainerView != NULL) { 4861 // NOTE: This container view thing is only used by Tracker. 4862 // move container view if not left aligned 4863 if (fAlignment == B_ALIGN_CENTER) { 4864 if (fmod(ceilf(newWidth - oldWidth), 2.0) != 0.0) 4865 newWidth += 1; 4866 fContainerView->MoveBy(ceilf(oldWidth - newWidth) / 2, 0); 4867 } else if (fAlignment == B_ALIGN_RIGHT) { 4868 fContainerView->MoveBy(ceilf(oldWidth - newWidth), 0); 4869 } 4870 // resize container view 4871 fContainerView->ResizeBy(ceilf(newWidth - oldWidth), 0); 4872 } 4873 4874 4875 if (redraw) 4876 _RequestDrawLines(0, 0); 4877 4878 // erase any potential left over outside the text rect 4879 // (can only be on right hand side) 4880 BRect dirty(fTextRect.right + 1, fTextRect.top, bounds.right, 4881 fTextRect.bottom); 4882 if (dirty.IsValid()) { 4883 SetLowColor(ViewColor()); 4884 FillRect(dirty, B_SOLID_LOW); 4885 } 4886 } 4887 4888 4889 //! Creates a new offscreen BBitmap with an associated BView. 4890 void 4891 BTextView::_NewOffscreen(float padding) 4892 { 4893 if (fOffscreen != NULL) 4894 _DeleteOffscreen(); 4895 4896 #if USE_DOUBLEBUFFERING 4897 BRect bitmapRect(0, 0, fTextRect.Width() + padding, fTextRect.Height()); 4898 fOffscreen = new BBitmap(bitmapRect, fColorSpace, true, false); 4899 if (fOffscreen != NULL && fOffscreen->Lock()) { 4900 BView* bufferView = new BView(bitmapRect, "drawing view", 0, 0); 4901 fOffscreen->AddChild(bufferView); 4902 fOffscreen->Unlock(); 4903 } 4904 #endif 4905 } 4906 4907 4908 //! Deletes the textview's offscreen bitmap, if any. 4909 void 4910 BTextView::_DeleteOffscreen() 4911 { 4912 if (fOffscreen != NULL && fOffscreen->Lock()) { 4913 delete fOffscreen; 4914 fOffscreen = NULL; 4915 } 4916 } 4917 4918 4919 /*! Creates a new offscreen bitmap, highlight the selection, and set the 4920 cursor to \c B_CURSOR_I_BEAM. 4921 */ 4922 void 4923 BTextView::_Activate() 4924 { 4925 fActive = true; 4926 4927 // Create a new offscreen BBitmap 4928 _NewOffscreen(); 4929 4930 if (fSelStart != fSelEnd) { 4931 if (fSelectable) 4932 Highlight(fSelStart, fSelEnd); 4933 } else 4934 _ShowCaret(); 4935 4936 BPoint where; 4937 uint32 buttons; 4938 GetMouse(&where, &buttons, false); 4939 if (Bounds().Contains(where)) 4940 _TrackMouse(where, NULL); 4941 4942 if (Window() != NULL) { 4943 BMessage* message; 4944 4945 if (!Window()->HasShortcut(B_LEFT_ARROW, B_COMMAND_KEY) 4946 && !Window()->HasShortcut(B_RIGHT_ARROW, B_COMMAND_KEY)) { 4947 message = new BMessage(kMsgNavigateArrow); 4948 message->AddInt32("key", B_LEFT_ARROW); 4949 message->AddInt32("modifiers", B_COMMAND_KEY); 4950 Window()->AddShortcut(B_LEFT_ARROW, B_COMMAND_KEY, message, this); 4951 4952 message = new BMessage(kMsgNavigateArrow); 4953 message->AddInt32("key", B_RIGHT_ARROW); 4954 message->AddInt32("modifiers", B_COMMAND_KEY); 4955 Window()->AddShortcut(B_RIGHT_ARROW, B_COMMAND_KEY, message, this); 4956 4957 fInstalledNavigateCommandWordwiseShortcuts = true; 4958 } 4959 if (!Window()->HasShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY) 4960 && !Window()->HasShortcut(B_RIGHT_ARROW, 4961 B_COMMAND_KEY | B_SHIFT_KEY)) { 4962 message = new BMessage(kMsgNavigateArrow); 4963 message->AddInt32("key", B_LEFT_ARROW); 4964 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY); 4965 Window()->AddShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY, 4966 message, this); 4967 4968 message = new BMessage(kMsgNavigateArrow); 4969 message->AddInt32("key", B_RIGHT_ARROW); 4970 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY); 4971 Window()->AddShortcut(B_RIGHT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY, 4972 message, this); 4973 4974 fInstalledSelectCommandWordwiseShortcuts = true; 4975 } 4976 4977 if (!Window()->HasShortcut(B_LEFT_ARROW, B_OPTION_KEY) 4978 && !Window()->HasShortcut(B_RIGHT_ARROW, B_OPTION_KEY)) { 4979 message = new BMessage(kMsgNavigateArrow); 4980 message->AddInt32("key", B_LEFT_ARROW); 4981 message->AddInt32("modifiers", B_OPTION_KEY); 4982 Window()->AddShortcut(B_LEFT_ARROW, B_OPTION_KEY, message, this); 4983 4984 message = new BMessage(kMsgNavigateArrow); 4985 message->AddInt32("key", B_RIGHT_ARROW); 4986 message->AddInt32("modifiers", B_OPTION_KEY); 4987 Window()->AddShortcut(B_RIGHT_ARROW, B_OPTION_KEY, message, this); 4988 4989 fInstalledNavigateOptionWordwiseShortcuts = true; 4990 } 4991 if (!Window()->HasShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY) 4992 && !Window()->HasShortcut(B_RIGHT_ARROW, 4993 B_OPTION_KEY | B_SHIFT_KEY)) { 4994 message = new BMessage(kMsgNavigateArrow); 4995 message->AddInt32("key", B_LEFT_ARROW); 4996 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY); 4997 Window()->AddShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY, 4998 message, this); 4999 5000 message = new BMessage(kMsgNavigateArrow); 5001 message->AddInt32("key", B_RIGHT_ARROW); 5002 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY); 5003 Window()->AddShortcut(B_RIGHT_ARROW, B_OPTION_KEY | B_SHIFT_KEY, 5004 message, this); 5005 5006 fInstalledSelectOptionWordwiseShortcuts = true; 5007 } 5008 5009 if (!Window()->HasShortcut(B_UP_ARROW, B_OPTION_KEY) 5010 && !Window()->HasShortcut(B_DOWN_ARROW, B_OPTION_KEY)) { 5011 message = new BMessage(kMsgNavigateArrow); 5012 message->AddInt32("key", B_UP_ARROW); 5013 message->AddInt32("modifiers", B_OPTION_KEY); 5014 Window()->AddShortcut(B_UP_ARROW, B_OPTION_KEY, message, this); 5015 5016 message = new BMessage(kMsgNavigateArrow); 5017 message->AddInt32("key", B_DOWN_ARROW); 5018 message->AddInt32("modifiers", B_OPTION_KEY); 5019 Window()->AddShortcut(B_DOWN_ARROW, B_OPTION_KEY, message, this); 5020 5021 fInstalledNavigateOptionLinewiseShortcuts = true; 5022 } 5023 if (!Window()->HasShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY) 5024 && !Window()->HasShortcut(B_DOWN_ARROW, 5025 B_OPTION_KEY | B_SHIFT_KEY)) { 5026 message = new BMessage(kMsgNavigateArrow); 5027 message->AddInt32("key", B_UP_ARROW); 5028 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY); 5029 Window()->AddShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY, 5030 message, this); 5031 5032 message = new BMessage(kMsgNavigateArrow); 5033 message->AddInt32("key", B_DOWN_ARROW); 5034 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY); 5035 Window()->AddShortcut(B_DOWN_ARROW, B_OPTION_KEY | B_SHIFT_KEY, 5036 message, this); 5037 5038 fInstalledSelectOptionLinewiseShortcuts = true; 5039 } 5040 5041 if (!Window()->HasShortcut(B_HOME, B_COMMAND_KEY) 5042 && !Window()->HasShortcut(B_END, B_COMMAND_KEY)) { 5043 message = new BMessage(kMsgNavigatePage); 5044 message->AddInt32("key", B_HOME); 5045 message->AddInt32("modifiers", B_COMMAND_KEY); 5046 Window()->AddShortcut(B_HOME, B_COMMAND_KEY, message, this); 5047 5048 message = new BMessage(kMsgNavigatePage); 5049 message->AddInt32("key", B_END); 5050 message->AddInt32("modifiers", B_COMMAND_KEY); 5051 Window()->AddShortcut(B_END, B_COMMAND_KEY, message, this); 5052 5053 fInstalledNavigateHomeEndDocwiseShortcuts = true; 5054 } 5055 if (!Window()->HasShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY) 5056 && !Window()->HasShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY)) { 5057 message = new BMessage(kMsgNavigatePage); 5058 message->AddInt32("key", B_HOME); 5059 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY); 5060 Window()->AddShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY, 5061 message, this); 5062 5063 message = new BMessage(kMsgNavigatePage); 5064 message->AddInt32("key", B_END); 5065 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY); 5066 Window()->AddShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY, 5067 message, this); 5068 5069 fInstalledSelectHomeEndDocwiseShortcuts = true; 5070 } 5071 } 5072 } 5073 5074 5075 //! Unhilights the selection, set the cursor to \c B_CURSOR_SYSTEM_DEFAULT. 5076 void 5077 BTextView::_Deactivate() 5078 { 5079 fActive = false; 5080 5081 _CancelInputMethod(); 5082 _DeleteOffscreen(); 5083 5084 if (fSelStart != fSelEnd) { 5085 if (fSelectable) 5086 Highlight(fSelStart, fSelEnd); 5087 } else 5088 _HideCaret(); 5089 5090 if (Window() != NULL) { 5091 if (fInstalledNavigateCommandWordwiseShortcuts) { 5092 Window()->RemoveShortcut(B_LEFT_ARROW, B_COMMAND_KEY); 5093 Window()->RemoveShortcut(B_RIGHT_ARROW, B_COMMAND_KEY); 5094 fInstalledNavigateCommandWordwiseShortcuts = false; 5095 } 5096 if (fInstalledSelectCommandWordwiseShortcuts) { 5097 Window()->RemoveShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY); 5098 Window()->RemoveShortcut(B_RIGHT_ARROW, 5099 B_COMMAND_KEY | B_SHIFT_KEY); 5100 fInstalledSelectCommandWordwiseShortcuts = false; 5101 } 5102 5103 if (fInstalledNavigateOptionWordwiseShortcuts) { 5104 Window()->RemoveShortcut(B_LEFT_ARROW, B_OPTION_KEY); 5105 Window()->RemoveShortcut(B_RIGHT_ARROW, B_OPTION_KEY); 5106 fInstalledNavigateOptionWordwiseShortcuts = false; 5107 } 5108 if (fInstalledSelectOptionWordwiseShortcuts) { 5109 Window()->RemoveShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY); 5110 Window()->RemoveShortcut(B_RIGHT_ARROW, B_OPTION_KEY | B_SHIFT_KEY); 5111 fInstalledSelectOptionWordwiseShortcuts = false; 5112 } 5113 5114 if (fInstalledNavigateOptionLinewiseShortcuts) { 5115 Window()->RemoveShortcut(B_UP_ARROW, B_OPTION_KEY); 5116 Window()->RemoveShortcut(B_DOWN_ARROW, B_OPTION_KEY); 5117 fInstalledNavigateOptionLinewiseShortcuts = false; 5118 } 5119 if (fInstalledSelectOptionLinewiseShortcuts) { 5120 Window()->RemoveShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY); 5121 Window()->RemoveShortcut(B_DOWN_ARROW, B_OPTION_KEY | B_SHIFT_KEY); 5122 fInstalledSelectOptionLinewiseShortcuts = false; 5123 } 5124 5125 if (fInstalledNavigateHomeEndDocwiseShortcuts) { 5126 Window()->RemoveShortcut(B_HOME, B_COMMAND_KEY); 5127 Window()->RemoveShortcut(B_END, B_COMMAND_KEY); 5128 fInstalledNavigateHomeEndDocwiseShortcuts = false; 5129 } 5130 if (fInstalledSelectHomeEndDocwiseShortcuts) { 5131 Window()->RemoveShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY); 5132 Window()->RemoveShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY); 5133 fInstalledSelectHomeEndDocwiseShortcuts = false; 5134 } 5135 } 5136 } 5137 5138 5139 /*! Changes the passed in font to be displayable by the object. 5140 5141 Set font rotation to 0, removes any font flag, set font spacing 5142 to \c B_BITMAP_SPACING and font encoding to \c B_UNICODE_UTF8. 5143 */ 5144 void 5145 BTextView::_NormalizeFont(BFont* font) 5146 { 5147 if (font) { 5148 font->SetRotation(0.0f); 5149 font->SetFlags(0); 5150 font->SetSpacing(B_BITMAP_SPACING); 5151 font->SetEncoding(B_UNICODE_UTF8); 5152 } 5153 } 5154 5155 5156 void 5157 BTextView::_SetRunArray(int32 startOffset, int32 endOffset, 5158 const text_run_array* runs) 5159 { 5160 const int32 numStyles = runs->count; 5161 if (numStyles > 0) { 5162 const text_run* theRun = &runs->runs[0]; 5163 for (int32 index = 0; index < numStyles; index++) { 5164 int32 fromOffset = theRun->offset + startOffset; 5165 int32 toOffset = endOffset; 5166 if (index + 1 < numStyles) { 5167 toOffset = (theRun + 1)->offset + startOffset; 5168 toOffset = (toOffset > endOffset) ? endOffset : toOffset; 5169 } 5170 5171 _ApplyStyleRange(fromOffset, toOffset, B_FONT_ALL, &theRun->font, 5172 &theRun->color, false); 5173 5174 theRun++; 5175 } 5176 fStyles->InvalidateNullStyle(); 5177 } 5178 } 5179 5180 5181 /*! Returns the character class of the character at the given offset. 5182 5183 \param offset The offset where the wanted character can be found. 5184 5185 \return A value which represents the character's classification. 5186 */ 5187 uint32 5188 BTextView::_CharClassification(int32 offset) const 5189 { 5190 // TODO: Should check against a list of characters containing also 5191 // japanese word breakers. 5192 // And what about other languages ? Isn't there a better way to check 5193 // for separator characters ? 5194 // Andrew suggested to have a look at UnicodeBlockObject.h 5195 switch (fText->RealCharAt(offset)) { 5196 case '\0': 5197 return CHAR_CLASS_END_OF_TEXT; 5198 5199 case B_SPACE: 5200 case B_TAB: 5201 case B_ENTER: 5202 return CHAR_CLASS_WHITESPACE; 5203 5204 case '=': 5205 case '+': 5206 case '@': 5207 case '#': 5208 case '$': 5209 case '%': 5210 case '^': 5211 case '&': 5212 case '*': 5213 case '\\': 5214 case '|': 5215 case '<': 5216 case '>': 5217 case '/': 5218 case '~': 5219 return CHAR_CLASS_GRAPHICAL; 5220 5221 case '\'': 5222 case '"': 5223 return CHAR_CLASS_QUOTE; 5224 5225 case ',': 5226 case '.': 5227 case '?': 5228 case '!': 5229 case ';': 5230 case ':': 5231 case '-': 5232 return CHAR_CLASS_PUNCTUATION; 5233 5234 case '(': 5235 case '[': 5236 case '{': 5237 return CHAR_CLASS_PARENS_OPEN; 5238 5239 case ')': 5240 case ']': 5241 case '}': 5242 return CHAR_CLASS_PARENS_CLOSE; 5243 5244 default: 5245 return CHAR_CLASS_DEFAULT; 5246 } 5247 } 5248 5249 5250 /*! Returns the offset of the next UTF-8 character. 5251 5252 \param offset The offset where to start looking. 5253 5254 \return The offset of the next UTF-8 character. 5255 */ 5256 int32 5257 BTextView::_NextInitialByte(int32 offset) const 5258 { 5259 if (offset >= fText->Length()) 5260 return offset; 5261 5262 for (++offset; (ByteAt(offset) & 0xC0) == 0x80; ++offset) 5263 ; 5264 5265 return offset; 5266 } 5267 5268 5269 /*! Returns the offset of the previous UTF-8 character. 5270 5271 \param offset The offset where to start looking. 5272 5273 \return The offset of the previous UTF-8 character. 5274 */ 5275 int32 5276 BTextView::_PreviousInitialByte(int32 offset) const 5277 { 5278 if (offset <= 0) 5279 return 0; 5280 5281 int32 count = 6; 5282 5283 for (--offset; offset > 0 && count; --offset, --count) { 5284 if ((ByteAt(offset) & 0xC0) != 0x80) 5285 break; 5286 } 5287 5288 return count ? offset : 0; 5289 } 5290 5291 5292 bool 5293 BTextView::_GetProperty(BMessage* specifier, int32 form, const char* property, 5294 BMessage* reply) 5295 { 5296 CALLED(); 5297 if (strcmp(property, "selection") == 0) { 5298 reply->what = B_REPLY; 5299 reply->AddInt32("result", fSelStart); 5300 reply->AddInt32("result", fSelEnd); 5301 reply->AddInt32("error", B_OK); 5302 5303 return true; 5304 } else if (strcmp(property, "Text") == 0) { 5305 if (IsTypingHidden()) { 5306 // Do not allow stealing passwords via scripting 5307 beep(); 5308 return false; 5309 } 5310 5311 int32 index, range; 5312 specifier->FindInt32("index", &index); 5313 specifier->FindInt32("range", &range); 5314 5315 char* buffer = new char[range + 1]; 5316 GetText(index, range, buffer); 5317 5318 reply->what = B_REPLY; 5319 reply->AddString("result", buffer); 5320 reply->AddInt32("error", B_OK); 5321 5322 delete[] buffer; 5323 5324 return true; 5325 } else if (strcmp(property, "text_run_array") == 0) 5326 return false; 5327 5328 return false; 5329 } 5330 5331 5332 bool 5333 BTextView::_SetProperty(BMessage* specifier, int32 form, const char* property, 5334 BMessage* reply) 5335 { 5336 CALLED(); 5337 if (strcmp(property, "selection") == 0) { 5338 int32 index, range; 5339 5340 specifier->FindInt32("index", &index); 5341 specifier->FindInt32("range", &range); 5342 5343 Select(index, index + range); 5344 5345 reply->what = B_REPLY; 5346 reply->AddInt32("error", B_OK); 5347 5348 return true; 5349 } else if (strcmp(property, "Text") == 0) { 5350 int32 index, range; 5351 specifier->FindInt32("index", &index); 5352 specifier->FindInt32("range", &range); 5353 5354 const char* buffer = NULL; 5355 if (specifier->FindString("data", &buffer) == B_OK) 5356 InsertText(buffer, range, index, NULL); 5357 else 5358 DeleteText(index, range); 5359 5360 reply->what = B_REPLY; 5361 reply->AddInt32("error", B_OK); 5362 5363 return true; 5364 } else if (strcmp(property, "text_run_array") == 0) 5365 return false; 5366 5367 return false; 5368 } 5369 5370 5371 bool 5372 BTextView::_CountProperties(BMessage* specifier, int32 form, 5373 const char* property, BMessage* reply) 5374 { 5375 CALLED(); 5376 if (strcmp(property, "Text") == 0) { 5377 reply->what = B_REPLY; 5378 reply->AddInt32("result", TextLength()); 5379 reply->AddInt32("error", B_OK); 5380 return true; 5381 } 5382 5383 return false; 5384 } 5385 5386 5387 //! Called when the object receives a \c B_INPUT_METHOD_CHANGED message. 5388 void 5389 BTextView::_HandleInputMethodChanged(BMessage* message) 5390 { 5391 // TODO: block input if not editable (Andrew) 5392 ASSERT(fInline != NULL); 5393 5394 const char* string = NULL; 5395 if (message->FindString("be:string", &string) < B_OK || string == NULL) 5396 return; 5397 5398 _HideCaret(); 5399 5400 if (IsFocus()) 5401 be_app->ObscureCursor(); 5402 5403 // If we find the "be:confirmed" boolean (and the boolean is true), 5404 // it means it's over for now, so the current InlineInput object 5405 // should become inactive. We will probably receive a 5406 // B_INPUT_METHOD_STOPPED message after this one. 5407 bool confirmed; 5408 if (message->FindBool("be:confirmed", &confirmed) != B_OK) 5409 confirmed = false; 5410 5411 // Delete the previously inserted text (if any) 5412 if (fInline->IsActive()) { 5413 const int32 oldOffset = fInline->Offset(); 5414 DeleteText(oldOffset, oldOffset + fInline->Length()); 5415 if (confirmed) 5416 fInline->SetActive(false); 5417 fCaretOffset = fSelStart = fSelEnd = oldOffset; 5418 } 5419 5420 const int32 stringLen = strlen(string); 5421 5422 fInline->SetOffset(fSelStart); 5423 fInline->SetLength(stringLen); 5424 fInline->ResetClauses(); 5425 5426 if (!confirmed && !fInline->IsActive()) 5427 fInline->SetActive(true); 5428 5429 // Get the clauses, and pass them to the InlineInput object 5430 // TODO: Find out if what we did it's ok, currently we don't consider 5431 // clauses at all, while the bebook says we should; though the visual 5432 // effect we obtained seems correct. Weird. 5433 int32 clauseCount = 0; 5434 int32 clauseStart; 5435 int32 clauseEnd; 5436 while (message->FindInt32("be:clause_start", clauseCount, &clauseStart) 5437 == B_OK 5438 && message->FindInt32("be:clause_end", clauseCount, &clauseEnd) 5439 == B_OK) { 5440 if (!fInline->AddClause(clauseStart, clauseEnd)) 5441 break; 5442 clauseCount++; 5443 } 5444 5445 if (confirmed) { 5446 _Refresh(fSelStart, fSelEnd, true); 5447 _ShowCaret(); 5448 5449 // now we need to feed ourselves the individual characters as if the 5450 // user would have pressed them now - this lets KeyDown() pick out all 5451 // the special characters like B_BACKSPACE, cursor keys and the like: 5452 const char* currPos = string; 5453 const char* prevPos = currPos; 5454 while (*currPos != '\0') { 5455 if ((*currPos & 0xC0) == 0xC0) { 5456 // found the start of an UTF-8 char, we collect while it lasts 5457 ++currPos; 5458 while ((*currPos & 0xC0) == 0x80) 5459 ++currPos; 5460 } else if ((*currPos & 0xC0) == 0x80) { 5461 // illegal: character starts with utf-8 intermediate byte, 5462 // skip it 5463 prevPos = ++currPos; 5464 } else { 5465 // single byte character/code, just feed that 5466 ++currPos; 5467 } 5468 KeyDown(prevPos, currPos - prevPos); 5469 prevPos = currPos; 5470 } 5471 5472 _Refresh(fSelStart, fSelEnd, true); 5473 } else { 5474 // temporarily show transient state of inline input 5475 int32 selectionStart = 0; 5476 int32 selectionEnd = 0; 5477 message->FindInt32("be:selection", 0, &selectionStart); 5478 message->FindInt32("be:selection", 1, &selectionEnd); 5479 5480 fInline->SetSelectionOffset(selectionStart); 5481 fInline->SetSelectionLength(selectionEnd - selectionStart); 5482 5483 const int32 inlineOffset = fInline->Offset(); 5484 InsertText(string, stringLen, fSelStart, NULL); 5485 5486 _Refresh(inlineOffset, fSelEnd, true); 5487 _ShowCaret(); 5488 } 5489 5490 } 5491 5492 5493 /*! Called when the object receives a \c B_INPUT_METHOD_LOCATION_REQUEST 5494 message. 5495 */ 5496 void 5497 BTextView::_HandleInputMethodLocationRequest() 5498 { 5499 ASSERT(fInline != NULL); 5500 5501 int32 offset = fInline->Offset(); 5502 const int32 limit = offset + fInline->Length(); 5503 5504 BMessage message(B_INPUT_METHOD_EVENT); 5505 message.AddInt32("be:opcode", B_INPUT_METHOD_LOCATION_REQUEST); 5506 5507 // Add the location of the UTF8 characters 5508 while (offset < limit) { 5509 float height; 5510 BPoint where = PointAt(offset, &height); 5511 ConvertToScreen(&where); 5512 5513 message.AddPoint("be:location_reply", where); 5514 message.AddFloat("be:height_reply", height); 5515 5516 offset = _NextInitialByte(offset); 5517 } 5518 5519 fInline->Method()->SendMessage(&message); 5520 } 5521 5522 5523 //! Tells the Input Server method add-on to stop the current transaction. 5524 void 5525 BTextView::_CancelInputMethod() 5526 { 5527 if (!fInline) 5528 return; 5529 5530 InlineInput* inlineInput = fInline; 5531 fInline = NULL; 5532 5533 if (inlineInput->IsActive() && Window()) { 5534 _Refresh(inlineInput->Offset(), fText->Length() - inlineInput->Offset(), 5535 false); 5536 5537 BMessage message(B_INPUT_METHOD_EVENT); 5538 message.AddInt32("be:opcode", B_INPUT_METHOD_STOPPED); 5539 inlineInput->Method()->SendMessage(&message); 5540 } 5541 5542 delete inlineInput; 5543 } 5544 5545 5546 /*! Returns the line number of the character at the given \a offset. 5547 5548 \note This will never return the last line (use LineAt() if you 5549 need to be correct about that.) N.B. 5550 5551 \param offset The offset of the wanted character. 5552 5553 \return The line number of the character at the given \a offset as an int32. 5554 */ 5555 int32 5556 BTextView::_LineAt(int32 offset) const 5557 { 5558 return fLines->OffsetToLine(offset); 5559 } 5560 5561 5562 /*! Returns the line number that the given \a point is on. 5563 5564 \note This will never return the last line (use LineAt() if you 5565 need to be correct about that.) N.B. 5566 5567 \param point The \a point the get the line number of. 5568 5569 \return The line number of the given \a point as an int32. 5570 */ 5571 int32 5572 BTextView::_LineAt(const BPoint& point) const 5573 { 5574 return fLines->PixelToLine(point.y - fTextRect.top); 5575 } 5576 5577 5578 /*! Returns whether or not the given \a offset is on the empty line at the end 5579 of the buffer. 5580 */ 5581 bool 5582 BTextView::_IsOnEmptyLastLine(int32 offset) const 5583 { 5584 return (offset == TextLength() && offset > 0 5585 && fText->RealCharAt(offset - 1) == B_ENTER); 5586 } 5587 5588 5589 void 5590 BTextView::_ApplyStyleRange(int32 fromOffset, int32 toOffset, uint32 mode, 5591 const BFont* font, const rgb_color* color, bool syncNullStyle) 5592 { 5593 if (font != NULL) { 5594 // if a font has been given, normalize it 5595 BFont normalized = *font; 5596 _NormalizeFont(&normalized); 5597 font = &normalized; 5598 } 5599 5600 if (!fStylable) { 5601 // always apply font and color to full range for non-stylable textviews 5602 fromOffset = 0; 5603 toOffset = fText->Length(); 5604 } 5605 5606 if (syncNullStyle) 5607 fStyles->SyncNullStyle(fromOffset); 5608 5609 fStyles->SetStyleRange(fromOffset, toOffset, fText->Length(), mode, 5610 font, color); 5611 } 5612 5613 5614 float 5615 BTextView::_NullStyleHeight() const 5616 { 5617 const BFont* font = NULL; 5618 fStyles->GetNullStyle(&font, NULL); 5619 5620 font_height fontHeight; 5621 font->GetHeight(&fontHeight); 5622 return ceilf(fontHeight.ascent + fontHeight.descent + 1); 5623 } 5624 5625 5626 void 5627 BTextView::_ShowContextMenu(BPoint where) 5628 { 5629 bool isRedo; 5630 undo_state state = UndoState(&isRedo); 5631 bool isUndo = state != B_UNDO_UNAVAILABLE && !isRedo; 5632 5633 int32 start; 5634 int32 finish; 5635 GetSelection(&start, &finish); 5636 5637 bool canEdit = IsEditable(); 5638 int32 length = TextLength(); 5639 5640 BPopUpMenu* menu = new BPopUpMenu(B_EMPTY_STRING, false, false); 5641 5642 BLayoutBuilder::Menu<>(menu) 5643 .AddItem(TRANSLATE("Undo"), B_UNDO/*, 'Z'*/) 5644 .SetEnabled(canEdit && isUndo) 5645 .AddItem(TRANSLATE("Redo"), B_UNDO/*, 'Z', B_SHIFT_KEY*/) 5646 .SetEnabled(canEdit && isRedo) 5647 .AddSeparator() 5648 .AddItem(TRANSLATE("Cut"), B_CUT, 'X') 5649 .SetEnabled(canEdit && start != finish) 5650 .AddItem(TRANSLATE("Copy"), B_COPY, 'C') 5651 .SetEnabled(start != finish) 5652 .AddItem(TRANSLATE("Paste"), B_PASTE, 'V') 5653 .SetEnabled(canEdit && be_clipboard->SystemCount() > 0) 5654 .AddSeparator() 5655 .AddItem(TRANSLATE("Select all"), B_SELECT_ALL, 'A') 5656 .SetEnabled(!(start == 0 && finish == length)) 5657 ; 5658 5659 menu->SetTargetForItems(this); 5660 ConvertToScreen(&where); 5661 menu->Go(where, true, true, true); 5662 } 5663 5664 5665 void 5666 BTextView::_FilterDisallowedChars(char* text, ssize_t& length, 5667 text_run_array* runArray) 5668 { 5669 if (!fDisallowedChars) 5670 return; 5671 5672 if (fDisallowedChars->IsEmpty() || !text) 5673 return; 5674 5675 ssize_t stringIndex = 0; 5676 if (runArray) { 5677 ssize_t remNext = 0; 5678 5679 for (int i = 0; i < runArray->count; i++) { 5680 runArray->runs[i].offset -= remNext; 5681 while (stringIndex < runArray->runs[i].offset 5682 && stringIndex < length) { 5683 if (fDisallowedChars->HasItem( 5684 reinterpret_cast<void*>(text[stringIndex]))) { 5685 memmove(text + stringIndex, text + stringIndex + 1, 5686 length - stringIndex - 1); 5687 length--; 5688 runArray->runs[i].offset--; 5689 remNext++; 5690 } else 5691 stringIndex++; 5692 } 5693 } 5694 } 5695 5696 while (stringIndex < length) { 5697 if (fDisallowedChars->HasItem( 5698 reinterpret_cast<void*>(text[stringIndex]))) { 5699 memmove(text + stringIndex, text + stringIndex + 1, 5700 length - stringIndex - 1); 5701 length--; 5702 } else 5703 stringIndex++; 5704 } 5705 } 5706 5707 5708 // #pragma mark - BTextView::TextTrackState 5709 5710 5711 BTextView::TextTrackState::TextTrackState(BMessenger messenger) 5712 : 5713 clickOffset(0), 5714 shiftDown(false), 5715 anchor(0), 5716 selStart(0), 5717 selEnd(0), 5718 fRunner(NULL) 5719 { 5720 BMessage message(_PING_); 5721 const bigtime_t scrollSpeed = 25 * 1000; // 40 scroll steps per second 5722 fRunner = new (nothrow) BMessageRunner(messenger, &message, scrollSpeed); 5723 } 5724 5725 5726 BTextView::TextTrackState::~TextTrackState() 5727 { 5728 delete fRunner; 5729 } 5730 5731 5732 void 5733 BTextView::TextTrackState::SimulateMouseMovement(BTextView* textView) 5734 { 5735 BPoint where; 5736 uint32 buttons; 5737 // When the mouse cursor is still and outside the textview, 5738 // no B_MOUSE_MOVED message are sent, obviously. But scrolling 5739 // has to work neverthless, so we "fake" a MouseMoved() call here. 5740 textView->GetMouse(&where, &buttons); 5741 textView->_PerformMouseMoved(where, B_INSIDE_VIEW); 5742 } 5743 5744 5745 // #pragma mark - Binary ABI compat 5746 5747 5748 extern "C" void 5749 B_IF_GCC_2(InvalidateLayout__9BTextViewb, _ZN9BTextView16InvalidateLayoutEb)( 5750 BTextView* view, bool descendants) 5751 { 5752 perform_data_layout_invalidated data; 5753 data.descendants = descendants; 5754 5755 view->Perform(PERFORM_CODE_LAYOUT_INVALIDATED, &data); 5756 } 5757