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