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