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