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