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