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*)calloc(size, 1); 2700 if (runArray == NULL) { 2701 if (outSize != NULL) 2702 *outSize = 0; 2703 return NULL; 2704 } 2705 2706 runArray->count = entryCount; 2707 2708 // Call constructors explicitly as the text_run_array 2709 // was allocated with malloc (and has to, for backwards 2710 // compatibility) 2711 for (int32 i = 0; i < runArray->count; i++) { 2712 new (&runArray->runs[i].font) BFont; 2713 } 2714 2715 if (outSize != NULL) 2716 *outSize = size; 2717 2718 return runArray; 2719 } 2720 2721 2722 /* static */ 2723 text_run_array* 2724 BTextView::CopyRunArray(const text_run_array* orig, int32 countDelta) 2725 { 2726 text_run_array* copy = AllocRunArray(countDelta, NULL); 2727 if (copy != NULL) { 2728 for (int32 i = 0; i < countDelta; i++) { 2729 copy->runs[i].offset = orig->runs[i].offset; 2730 copy->runs[i].font = orig->runs[i].font; 2731 copy->runs[i].color = orig->runs[i].color; 2732 } 2733 } 2734 return copy; 2735 } 2736 2737 2738 /* static */ 2739 void 2740 BTextView::FreeRunArray(text_run_array* array) 2741 { 2742 if (array == NULL) 2743 return; 2744 2745 // Call destructors explicitly 2746 for (int32 i = 0; i < array->count; i++) 2747 array->runs[i].font.~BFont(); 2748 2749 free(array); 2750 } 2751 2752 2753 /* static */ 2754 void* 2755 BTextView::FlattenRunArray(const text_run_array* runArray, int32* _size) 2756 { 2757 CALLED(); 2758 int32 size = sizeof(flattened_text_run_array) + (runArray->count - 1) 2759 * sizeof(flattened_text_run); 2760 2761 flattened_text_run_array* array = (flattened_text_run_array*)malloc(size); 2762 if (array == NULL) { 2763 if (_size) 2764 *_size = 0; 2765 return NULL; 2766 } 2767 2768 array->magic = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayMagic); 2769 array->version = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayVersion); 2770 array->count = B_HOST_TO_BENDIAN_INT32(runArray->count); 2771 2772 for (int32 i = 0; i < runArray->count; i++) { 2773 array->styles[i].offset = B_HOST_TO_BENDIAN_INT32( 2774 runArray->runs[i].offset); 2775 runArray->runs[i].font.GetFamilyAndStyle(&array->styles[i].family, 2776 &array->styles[i].style); 2777 array->styles[i].size = B_HOST_TO_BENDIAN_FLOAT( 2778 runArray->runs[i].font.Size()); 2779 array->styles[i].shear = B_HOST_TO_BENDIAN_FLOAT( 2780 runArray->runs[i].font.Shear()); 2781 array->styles[i].face = B_HOST_TO_BENDIAN_INT16( 2782 runArray->runs[i].font.Face()); 2783 array->styles[i].red = runArray->runs[i].color.red; 2784 array->styles[i].green = runArray->runs[i].color.green; 2785 array->styles[i].blue = runArray->runs[i].color.blue; 2786 array->styles[i].alpha = 255; 2787 array->styles[i]._reserved_ = 0; 2788 } 2789 2790 if (_size) 2791 *_size = size; 2792 2793 return array; 2794 } 2795 2796 2797 /* static */ 2798 text_run_array* 2799 BTextView::UnflattenRunArray(const void* data, int32* _size) 2800 { 2801 CALLED(); 2802 flattened_text_run_array* array = (flattened_text_run_array*)data; 2803 2804 if (B_BENDIAN_TO_HOST_INT32(array->magic) != kFlattenedTextRunArrayMagic 2805 || B_BENDIAN_TO_HOST_INT32(array->version) 2806 != kFlattenedTextRunArrayVersion) { 2807 if (_size) 2808 *_size = 0; 2809 2810 return NULL; 2811 } 2812 2813 int32 count = B_BENDIAN_TO_HOST_INT32(array->count); 2814 2815 text_run_array* runArray = AllocRunArray(count, _size); 2816 if (runArray == NULL) 2817 return NULL; 2818 2819 for (int32 i = 0; i < count; i++) { 2820 runArray->runs[i].offset = B_BENDIAN_TO_HOST_INT32( 2821 array->styles[i].offset); 2822 2823 // Set family and style independently from each other, so that 2824 // even if the family doesn't exist, we try to preserve the style 2825 runArray->runs[i].font.SetFamilyAndStyle(array->styles[i].family, NULL); 2826 runArray->runs[i].font.SetFamilyAndStyle(NULL, array->styles[i].style); 2827 2828 runArray->runs[i].font.SetSize(B_BENDIAN_TO_HOST_FLOAT( 2829 array->styles[i].size)); 2830 runArray->runs[i].font.SetShear(B_BENDIAN_TO_HOST_FLOAT( 2831 array->styles[i].shear)); 2832 2833 uint16 face = B_BENDIAN_TO_HOST_INT16(array->styles[i].face); 2834 if (face != B_REGULAR_FACE) { 2835 // Be's version doesn't seem to set this correctly 2836 runArray->runs[i].font.SetFace(face); 2837 } 2838 2839 runArray->runs[i].color.red = array->styles[i].red; 2840 runArray->runs[i].color.green = array->styles[i].green; 2841 runArray->runs[i].color.blue = array->styles[i].blue; 2842 runArray->runs[i].color.alpha = array->styles[i].alpha; 2843 } 2844 2845 return runArray; 2846 } 2847 2848 2849 void 2850 BTextView::InsertText(const char* text, int32 length, int32 offset, 2851 const text_run_array* runs) 2852 { 2853 CALLED(); 2854 2855 if (length < 0) 2856 length = 0; 2857 2858 if (offset < 0) 2859 offset = 0; 2860 else if (offset > fText->Length()) 2861 offset = fText->Length(); 2862 2863 if (length > 0) { 2864 // add the text to the buffer 2865 fText->InsertText(text, length, offset); 2866 2867 // update the start offsets of each line below offset 2868 fLines->BumpOffset(length, _LineAt(offset) + 1); 2869 2870 // update the style runs 2871 fStyles->BumpOffset(length, fStyles->OffsetToRun(offset - 1) + 1); 2872 2873 // offset the caret/selection, if the text was inserted before it 2874 if (offset <= fSelEnd) { 2875 fSelStart += length; 2876 fCaretOffset = fSelEnd = fSelStart; 2877 } 2878 } 2879 2880 if (fStylable && runs != NULL) { 2881 _SetRunArray(offset, offset + length, runs); 2882 } else { 2883 // apply null-style to inserted text 2884 _ApplyStyleRange(offset, offset + length); 2885 } 2886 } 2887 2888 2889 void 2890 BTextView::DeleteText(int32 fromOffset, int32 toOffset) 2891 { 2892 CALLED(); 2893 2894 if (fromOffset < 0) 2895 fromOffset = 0; 2896 else if (fromOffset > fText->Length()) 2897 fromOffset = fText->Length(); 2898 2899 if (toOffset < 0) 2900 toOffset = 0; 2901 else if (toOffset > fText->Length()) 2902 toOffset = fText->Length(); 2903 2904 if (fromOffset >= toOffset) 2905 return; 2906 2907 // set nullStyle to style at beginning of range 2908 fStyles->InvalidateNullStyle(); 2909 fStyles->SyncNullStyle(fromOffset); 2910 2911 // remove from the text buffer 2912 fText->RemoveRange(fromOffset, toOffset); 2913 2914 // remove any lines that have been obliterated 2915 fLines->RemoveLineRange(fromOffset, toOffset); 2916 2917 // remove any style runs that have been obliterated 2918 fStyles->RemoveStyleRange(fromOffset, toOffset); 2919 2920 // adjust the selection accordingly, assumes fSelEnd >= fSelStart! 2921 int32 range = toOffset - fromOffset; 2922 if (fSelStart >= toOffset) { 2923 // selection is behind the range that was removed 2924 fSelStart -= range; 2925 fSelEnd -= range; 2926 } else if (fSelStart >= fromOffset && fSelEnd <= toOffset) { 2927 // the selection is within the range that was removed 2928 fSelStart = fSelEnd = fromOffset; 2929 } else if (fSelStart >= fromOffset && fSelEnd > toOffset) { 2930 // the selection starts within and ends after the range 2931 // the remaining part is the part that was after the range 2932 fSelStart = fromOffset; 2933 fSelEnd = fromOffset + fSelEnd - toOffset; 2934 } else if (fSelStart < fromOffset && fSelEnd < toOffset) { 2935 // the selection starts before, but ends within the range 2936 fSelEnd = fromOffset; 2937 } else if (fSelStart < fromOffset && fSelEnd >= toOffset) { 2938 // the selection starts before and ends after the range 2939 fSelEnd -= range; 2940 } 2941 } 2942 2943 2944 /*! Undoes the last changes. 2945 2946 \param clipboard A \a clipboard to use for the undo operation. 2947 */ 2948 void 2949 BTextView::Undo(BClipboard* clipboard) 2950 { 2951 if (fUndo) 2952 fUndo->Undo(clipboard); 2953 } 2954 2955 2956 undo_state 2957 BTextView::UndoState(bool* isRedo) const 2958 { 2959 return fUndo == NULL ? B_UNDO_UNAVAILABLE : fUndo->State(isRedo); 2960 } 2961 2962 2963 // #pragma mark - GetDragParameters() is protected 2964 2965 2966 void 2967 BTextView::GetDragParameters(BMessage* drag, BBitmap** bitmap, BPoint* point, 2968 BHandler** handler) 2969 { 2970 CALLED(); 2971 if (drag == NULL) 2972 return; 2973 2974 // Add originator and action 2975 drag->AddPointer("be:originator", this); 2976 drag->AddInt32("be_actions", B_TRASH_TARGET); 2977 2978 // add the text 2979 int32 numBytes = fSelEnd - fSelStart; 2980 const char* text = fText->GetString(fSelStart, &numBytes); 2981 drag->AddData("text/plain", B_MIME_TYPE, text, numBytes); 2982 2983 // add the corresponding styles 2984 int32 size = 0; 2985 text_run_array* styles = RunArray(fSelStart, fSelEnd, &size); 2986 2987 if (styles != NULL) { 2988 drag->AddData("application/x-vnd.Be-text_run_array", B_MIME_TYPE, 2989 styles, size); 2990 2991 FreeRunArray(styles); 2992 } 2993 2994 if (bitmap != NULL) 2995 *bitmap = NULL; 2996 2997 if (handler != NULL) 2998 *handler = NULL; 2999 } 3000 3001 3002 // #pragma mark - FBC padding and forbidden methods 3003 3004 3005 void BTextView::_ReservedTextView3() {} 3006 void BTextView::_ReservedTextView4() {} 3007 void BTextView::_ReservedTextView5() {} 3008 void BTextView::_ReservedTextView6() {} 3009 void BTextView::_ReservedTextView7() {} 3010 void BTextView::_ReservedTextView8() {} 3011 void BTextView::_ReservedTextView9() {} 3012 void BTextView::_ReservedTextView10() {} 3013 void BTextView::_ReservedTextView11() {} 3014 void BTextView::_ReservedTextView12() {} 3015 3016 3017 // #pragma mark - Private methods 3018 3019 3020 /*! Inits the BTextView object. 3021 3022 \param textRect The BTextView's text rect. 3023 \param initialFont The font which the BTextView will use. 3024 \param initialColor The initial color of the text. 3025 */ 3026 void 3027 BTextView::_InitObject(BRect textRect, const BFont* initialFont, 3028 const rgb_color* initialColor) 3029 { 3030 BFont font; 3031 if (initialFont == NULL) 3032 GetFont(&font); 3033 else 3034 font = *initialFont; 3035 3036 _NormalizeFont(&font); 3037 3038 if (initialColor == NULL) 3039 initialColor = &kBlackColor; 3040 3041 fText = new BPrivate::TextGapBuffer; 3042 fLines = new LineBuffer; 3043 fStyles = new StyleBuffer(&font, initialColor); 3044 3045 fInstalledNavigateCommandWordwiseShortcuts = false; 3046 fInstalledNavigateOptionWordwiseShortcuts = false; 3047 fInstalledNavigateOptionLinewiseShortcuts = false; 3048 fInstalledNavigateHomeEndDocwiseShortcuts = false; 3049 3050 fInstalledSelectCommandWordwiseShortcuts = false; 3051 fInstalledSelectOptionWordwiseShortcuts = false; 3052 fInstalledSelectOptionLinewiseShortcuts = false; 3053 fInstalledSelectHomeEndDocwiseShortcuts = false; 3054 3055 // We put these here instead of in the constructor initializer list 3056 // to have less code duplication, and a single place where to do changes 3057 // if needed. 3058 fTextRect = textRect; 3059 // NOTE: The only places where text rect is changed: 3060 // * width is possibly adjusted in _AutoResize(), 3061 // * height is adjusted in _RecalculateLineBreaks(). 3062 // When used within the layout management framework, the 3063 // text rect is changed to maintain constant insets. 3064 fMinTextRectWidth = fTextRect.Width(); 3065 // see SetTextRect() 3066 fSelStart = fSelEnd = 0; 3067 fCaretVisible = false; 3068 fCaretTime = 0; 3069 fCaretOffset = 0; 3070 fClickCount = 0; 3071 fClickTime = 0; 3072 fDragOffset = -1; 3073 fCursor = 0; 3074 fActive = false; 3075 fStylable = false; 3076 fTabWidth = 28.0; 3077 fSelectable = true; 3078 fEditable = true; 3079 fWrap = true; 3080 fMaxBytes = INT32_MAX; 3081 fDisallowedChars = NULL; 3082 fAlignment = B_ALIGN_LEFT; 3083 fAutoindent = false; 3084 fOffscreen = NULL; 3085 fColorSpace = B_CMAP8; 3086 fResizable = false; 3087 fContainerView = NULL; 3088 fUndo = NULL; 3089 fInline = NULL; 3090 fDragRunner = NULL; 3091 fClickRunner = NULL; 3092 fTrackingMouse = NULL; 3093 3094 fLayoutData = new LayoutData; 3095 fLayoutData->UpdateInsets(Bounds().OffsetToCopy(B_ORIGIN), fTextRect); 3096 3097 fLastClickOffset = -1; 3098 3099 SetDoesUndo(true); 3100 } 3101 3102 3103 //! Handles when Backspace key is pressed. 3104 void 3105 BTextView::_HandleBackspace() 3106 { 3107 if (fUndo) { 3108 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>( 3109 fUndo); 3110 if (!undoBuffer) { 3111 delete fUndo; 3112 fUndo = undoBuffer = new TypingUndoBuffer(this); 3113 } 3114 undoBuffer->BackwardErase(); 3115 } 3116 3117 if (fSelStart == fSelEnd) { 3118 if (fSelStart == 0) 3119 return; 3120 else 3121 fSelStart = _PreviousInitialByte(fSelStart); 3122 } else 3123 Highlight(fSelStart, fSelEnd); 3124 3125 DeleteText(fSelStart, fSelEnd); 3126 fCaretOffset = fSelEnd = fSelStart; 3127 3128 _Refresh(fSelStart, fSelEnd, true); 3129 } 3130 3131 3132 //! Handles when an arrow key is pressed. 3133 void 3134 BTextView::_HandleArrowKey(uint32 arrowKey, int32 modifiers) 3135 { 3136 // return if there's nowhere to go 3137 if (fText->Length() == 0) 3138 return; 3139 3140 int32 selStart = fSelStart; 3141 int32 selEnd = fSelEnd; 3142 3143 if (modifiers < 0) { 3144 BMessage* currentMessage = Window()->CurrentMessage(); 3145 if (currentMessage == NULL 3146 || currentMessage->FindInt32("modifiers", &modifiers) != B_OK) { 3147 modifiers = 0; 3148 } 3149 } 3150 3151 bool shiftKeyDown = (modifiers & B_SHIFT_KEY) != 0; 3152 bool controlKeyDown = (modifiers & B_CONTROL_KEY) != 0; 3153 bool optionKeyDown = (modifiers & B_OPTION_KEY) != 0; 3154 bool commandKeyDown = (modifiers & B_COMMAND_KEY) != 0; 3155 3156 int32 lastClickOffset = fCaretOffset; 3157 3158 switch (arrowKey) { 3159 case B_LEFT_ARROW: 3160 if (!fEditable) 3161 _ScrollBy(-1 * kHorizontalScrollBarStep, 0); 3162 else if (fSelStart != fSelEnd && !shiftKeyDown) 3163 fCaretOffset = fSelStart; 3164 else { 3165 if ((commandKeyDown || optionKeyDown) && !controlKeyDown) 3166 fCaretOffset = _PreviousWordStart(fCaretOffset - 1); 3167 else 3168 fCaretOffset = _PreviousInitialByte(fCaretOffset); 3169 3170 if (shiftKeyDown && fCaretOffset != lastClickOffset) { 3171 if (fCaretOffset < fSelStart) { 3172 // extend selection to the left 3173 selStart = fCaretOffset; 3174 if (lastClickOffset > fSelStart) { 3175 // caret has jumped across "anchor" 3176 selEnd = fSelStart; 3177 } 3178 } else { 3179 // shrink selection from the right 3180 selEnd = fCaretOffset; 3181 } 3182 } 3183 } 3184 break; 3185 3186 case B_RIGHT_ARROW: 3187 if (!fEditable) 3188 _ScrollBy(kHorizontalScrollBarStep, 0); 3189 else if (fSelStart != fSelEnd && !shiftKeyDown) 3190 fCaretOffset = fSelEnd; 3191 else { 3192 if ((commandKeyDown || optionKeyDown) && !controlKeyDown) 3193 fCaretOffset = _NextWordEnd(fCaretOffset); 3194 else 3195 fCaretOffset = _NextInitialByte(fCaretOffset); 3196 3197 if (shiftKeyDown && fCaretOffset != lastClickOffset) { 3198 if (fCaretOffset > fSelEnd) { 3199 // extend selection to the right 3200 selEnd = fCaretOffset; 3201 if (lastClickOffset < fSelEnd) { 3202 // caret has jumped across "anchor" 3203 selStart = fSelEnd; 3204 } 3205 } else { 3206 // shrink selection from the left 3207 selStart = fCaretOffset; 3208 } 3209 } 3210 } 3211 break; 3212 3213 case B_UP_ARROW: 3214 { 3215 if (!fEditable) 3216 _ScrollBy(0, -1 * kVerticalScrollBarStep); 3217 else if (fSelStart != fSelEnd && !shiftKeyDown) 3218 fCaretOffset = fSelStart; 3219 else { 3220 if (optionKeyDown && !commandKeyDown && !controlKeyDown) 3221 fCaretOffset = _PreviousLineStart(fCaretOffset); 3222 else if (commandKeyDown && !optionKeyDown && !controlKeyDown) { 3223 _ScrollTo(0, 0); 3224 fCaretOffset = 0; 3225 } else { 3226 float height; 3227 BPoint point = PointAt(fCaretOffset, &height); 3228 // find the caret position on the previous 3229 // line by gently stepping onto this line 3230 for (int i = 1; i <= height; i++) { 3231 point.y--; 3232 int32 offset = OffsetAt(point); 3233 if (offset < fCaretOffset || i == height) { 3234 fCaretOffset = offset; 3235 break; 3236 } 3237 } 3238 } 3239 3240 if (shiftKeyDown && fCaretOffset != lastClickOffset) { 3241 if (fCaretOffset < fSelStart) { 3242 // extend selection to the top 3243 selStart = fCaretOffset; 3244 if (lastClickOffset > fSelStart) { 3245 // caret has jumped across "anchor" 3246 selEnd = fSelStart; 3247 } 3248 } else { 3249 // shrink selection from the bottom 3250 selEnd = fCaretOffset; 3251 } 3252 } 3253 } 3254 break; 3255 } 3256 3257 case B_DOWN_ARROW: 3258 { 3259 if (!fEditable) 3260 _ScrollBy(0, kVerticalScrollBarStep); 3261 else if (fSelStart != fSelEnd && !shiftKeyDown) 3262 fCaretOffset = fSelEnd; 3263 else { 3264 if (optionKeyDown && !commandKeyDown && !controlKeyDown) 3265 fCaretOffset = _NextLineEnd(fCaretOffset); 3266 else if (commandKeyDown && !optionKeyDown && !controlKeyDown) { 3267 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset); 3268 fCaretOffset = fText->Length(); 3269 } else { 3270 float height; 3271 BPoint point = PointAt(fCaretOffset, &height); 3272 point.y += height; 3273 fCaretOffset = OffsetAt(point); 3274 } 3275 3276 if (shiftKeyDown && fCaretOffset != lastClickOffset) { 3277 if (fCaretOffset > fSelEnd) { 3278 // extend selection to the bottom 3279 selEnd = fCaretOffset; 3280 if (lastClickOffset < fSelEnd) { 3281 // caret has jumped across "anchor" 3282 selStart = fSelEnd; 3283 } 3284 } else { 3285 // shrink selection from the top 3286 selStart = fCaretOffset; 3287 } 3288 } 3289 } 3290 break; 3291 } 3292 } 3293 3294 fStyles->InvalidateNullStyle(); 3295 3296 if (fEditable) { 3297 if (shiftKeyDown) 3298 Select(selStart, selEnd); 3299 else 3300 Select(fCaretOffset, fCaretOffset); 3301 3302 // scroll if needed 3303 ScrollToOffset(fCaretOffset); 3304 } 3305 } 3306 3307 3308 //! Handles when the Delete key is pressed. 3309 void 3310 BTextView::_HandleDelete() 3311 { 3312 if (fUndo) { 3313 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>( 3314 fUndo); 3315 if (!undoBuffer) { 3316 delete fUndo; 3317 fUndo = undoBuffer = new TypingUndoBuffer(this); 3318 } 3319 undoBuffer->ForwardErase(); 3320 } 3321 3322 if (fSelStart == fSelEnd) { 3323 if (fSelEnd == fText->Length()) 3324 return; 3325 else 3326 fSelEnd = _NextInitialByte(fSelEnd); 3327 } else 3328 Highlight(fSelStart, fSelEnd); 3329 3330 DeleteText(fSelStart, fSelEnd); 3331 fCaretOffset = fSelEnd = fSelStart; 3332 3333 _Refresh(fSelStart, fSelEnd, true); 3334 } 3335 3336 3337 //! Handles when the Page Up, Page Down, Home, or End key is pressed. 3338 void 3339 BTextView::_HandlePageKey(uint32 pageKey, int32 modifiers) 3340 { 3341 if (modifiers < 0) { 3342 BMessage* currentMessage = Window()->CurrentMessage(); 3343 if (currentMessage == NULL 3344 || currentMessage->FindInt32("modifiers", &modifiers) != B_OK) { 3345 modifiers = 0; 3346 } 3347 } 3348 3349 bool shiftKeyDown = (modifiers & B_SHIFT_KEY) != 0; 3350 bool controlKeyDown = (modifiers & B_CONTROL_KEY) != 0; 3351 bool optionKeyDown = (modifiers & B_OPTION_KEY) != 0; 3352 bool commandKeyDown = (modifiers & B_COMMAND_KEY) != 0; 3353 3354 STELine* line = NULL; 3355 int32 selStart = fSelStart; 3356 int32 selEnd = fSelEnd; 3357 3358 int32 lastClickOffset = fCaretOffset; 3359 switch (pageKey) { 3360 case B_HOME: 3361 if (!fEditable) { 3362 fCaretOffset = 0; 3363 _ScrollTo(0, 0); 3364 break; 3365 } else { 3366 if (commandKeyDown && !optionKeyDown && !controlKeyDown) { 3367 _ScrollTo(0, 0); 3368 fCaretOffset = 0; 3369 } else { 3370 // get the start of the last line if caret is on it 3371 line = (*fLines)[_LineAt(lastClickOffset)]; 3372 fCaretOffset = line->offset; 3373 } 3374 3375 if (!shiftKeyDown) 3376 selStart = selEnd = fCaretOffset; 3377 else if (fCaretOffset != lastClickOffset) { 3378 if (fCaretOffset < fSelStart) { 3379 // extend selection to the left 3380 selStart = fCaretOffset; 3381 if (lastClickOffset > fSelStart) { 3382 // caret has jumped across "anchor" 3383 selEnd = fSelStart; 3384 } 3385 } else { 3386 // shrink selection from the right 3387 selEnd = fCaretOffset; 3388 } 3389 } 3390 } 3391 break; 3392 3393 case B_END: 3394 if (!fEditable) { 3395 fCaretOffset = fText->Length(); 3396 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset); 3397 break; 3398 } else { 3399 if (commandKeyDown && !optionKeyDown && !controlKeyDown) { 3400 _ScrollTo(0, fTextRect.bottom + fLayoutData->bottomInset); 3401 fCaretOffset = fText->Length(); 3402 } else { 3403 // If we are on the last line, just go to the last 3404 // character in the buffer, otherwise get the starting 3405 // offset of the next line, and go to the previous character 3406 int32 currentLine = _LineAt(lastClickOffset); 3407 if (currentLine + 1 < fLines->NumLines()) { 3408 line = (*fLines)[currentLine + 1]; 3409 fCaretOffset = _PreviousInitialByte(line->offset); 3410 } else { 3411 // This check is needed to avoid moving the cursor 3412 // when the cursor is on the last line, and that line 3413 // is empty 3414 if (fCaretOffset != fText->Length()) { 3415 fCaretOffset = fText->Length(); 3416 if (ByteAt(fCaretOffset - 1) == B_ENTER) 3417 fCaretOffset--; 3418 } 3419 } 3420 } 3421 3422 if (!shiftKeyDown) 3423 selStart = selEnd = fCaretOffset; 3424 else if (fCaretOffset != lastClickOffset) { 3425 if (fCaretOffset > fSelEnd) { 3426 // extend selection to the right 3427 selEnd = fCaretOffset; 3428 if (lastClickOffset < fSelEnd) { 3429 // caret has jumped across "anchor" 3430 selStart = fSelEnd; 3431 } 3432 } else { 3433 // shrink selection from the left 3434 selStart = fCaretOffset; 3435 } 3436 } 3437 } 3438 break; 3439 3440 case B_PAGE_UP: 3441 { 3442 float lineHeight; 3443 BPoint currentPos = PointAt(fCaretOffset, &lineHeight); 3444 BPoint nextPos(currentPos.x, 3445 currentPos.y + lineHeight - Bounds().Height()); 3446 fCaretOffset = OffsetAt(nextPos); 3447 nextPos = PointAt(fCaretOffset); 3448 _ScrollBy(0, nextPos.y - currentPos.y); 3449 3450 if (!fEditable) 3451 break; 3452 3453 if (!shiftKeyDown) 3454 selStart = selEnd = fCaretOffset; 3455 else if (fCaretOffset != lastClickOffset) { 3456 if (fCaretOffset < fSelStart) { 3457 // extend selection to the top 3458 selStart = fCaretOffset; 3459 if (lastClickOffset > fSelStart) { 3460 // caret has jumped across "anchor" 3461 selEnd = fSelStart; 3462 } 3463 } else { 3464 // shrink selection from the bottom 3465 selEnd = fCaretOffset; 3466 } 3467 } 3468 3469 break; 3470 } 3471 3472 case B_PAGE_DOWN: 3473 { 3474 BPoint currentPos = PointAt(fCaretOffset); 3475 BPoint nextPos(currentPos.x, currentPos.y + Bounds().Height()); 3476 fCaretOffset = OffsetAt(nextPos); 3477 nextPos = PointAt(fCaretOffset); 3478 _ScrollBy(0, nextPos.y - currentPos.y); 3479 3480 if (!fEditable) 3481 break; 3482 3483 if (!shiftKeyDown) 3484 selStart = selEnd = fCaretOffset; 3485 else if (fCaretOffset != lastClickOffset) { 3486 if (fCaretOffset > fSelEnd) { 3487 // extend selection to the bottom 3488 selEnd = fCaretOffset; 3489 if (lastClickOffset < fSelEnd) { 3490 // caret has jumped across "anchor" 3491 selStart = fSelEnd; 3492 } 3493 } else { 3494 // shrink selection from the top 3495 selStart = fCaretOffset; 3496 } 3497 } 3498 3499 break; 3500 } 3501 } 3502 3503 if (fEditable) { 3504 if (shiftKeyDown) 3505 Select(selStart, selEnd); 3506 else 3507 Select(fCaretOffset, fCaretOffset); 3508 3509 ScrollToOffset(fCaretOffset); 3510 } 3511 } 3512 3513 3514 /*! Handles when an alpha-numeric key is pressed. 3515 3516 \param bytes The string or character associated with the key. 3517 \param numBytes The amount of bytes containes in "bytes". 3518 */ 3519 void 3520 BTextView::_HandleAlphaKey(const char* bytes, int32 numBytes) 3521 { 3522 // TODO: block input if not editable (Andrew) 3523 if (fUndo) { 3524 TypingUndoBuffer* undoBuffer = dynamic_cast<TypingUndoBuffer*>(fUndo); 3525 if (!undoBuffer) { 3526 delete fUndo; 3527 fUndo = undoBuffer = new TypingUndoBuffer(this); 3528 } 3529 undoBuffer->InputCharacter(numBytes); 3530 } 3531 3532 if (fSelStart != fSelEnd) { 3533 Highlight(fSelStart, fSelEnd); 3534 DeleteText(fSelStart, fSelEnd); 3535 } 3536 3537 if (fAutoindent && numBytes == 1 && *bytes == B_ENTER) { 3538 int32 start, offset; 3539 start = offset = OffsetAt(_LineAt(fSelStart)); 3540 3541 while (ByteAt(offset) != '\0' && 3542 (ByteAt(offset) == B_TAB || ByteAt(offset) == B_SPACE) 3543 && offset < fSelStart) 3544 offset++; 3545 3546 _DoInsertText(bytes, numBytes, fSelStart, NULL); 3547 if (start != offset) 3548 _DoInsertText(Text() + start, offset - start, fSelStart, NULL); 3549 } else 3550 _DoInsertText(bytes, numBytes, fSelStart, NULL); 3551 3552 fCaretOffset = fSelEnd; 3553 3554 ScrollToOffset(fCaretOffset); 3555 } 3556 3557 3558 /*! Redraw the text between the two given offsets, recalculating line-breaks 3559 if needed. 3560 3561 \param fromOffset The offset from where to refresh. 3562 \param toOffset The offset where to refresh to. 3563 \param scroll If \c true, scroll the view to the end offset. 3564 */ 3565 void 3566 BTextView::_Refresh(int32 fromOffset, int32 toOffset, bool scroll) 3567 { 3568 // TODO: Cleanup 3569 float saveHeight = fTextRect.Height(); 3570 float saveWidth = fTextRect.Width(); 3571 int32 fromLine = _LineAt(fromOffset); 3572 int32 toLine = _LineAt(toOffset); 3573 int32 saveFromLine = fromLine; 3574 int32 saveToLine = toLine; 3575 3576 _RecalculateLineBreaks(&fromLine, &toLine); 3577 3578 // TODO: Maybe there is still something we can do without a window... 3579 if (!Window()) 3580 return; 3581 3582 BRect bounds = Bounds(); 3583 float newHeight = fTextRect.Height(); 3584 3585 // if the line breaks have changed, force an erase 3586 if (fromLine != saveFromLine || toLine != saveToLine 3587 || newHeight != saveHeight) { 3588 fromOffset = -1; 3589 } 3590 3591 if (newHeight != saveHeight) { 3592 // the text area has changed 3593 if (newHeight < saveHeight) 3594 toLine = _LineAt(BPoint(0.0f, saveHeight + fTextRect.top)); 3595 else 3596 toLine = _LineAt(BPoint(0.0f, newHeight + fTextRect.top)); 3597 } 3598 3599 // draw only those lines that are visible 3600 int32 fromVisible = _LineAt(BPoint(0.0f, bounds.top)); 3601 int32 toVisible = _LineAt(BPoint(0.0f, bounds.bottom)); 3602 fromLine = max_c(fromVisible, fromLine); 3603 toLine = min_c(toLine, toVisible); 3604 3605 _AutoResize(false); 3606 3607 _RequestDrawLines(fromLine, toLine); 3608 3609 // erase the area below the text 3610 BRect eraseRect = bounds; 3611 eraseRect.top = fTextRect.top + (*fLines)[fLines->NumLines()]->origin; 3612 eraseRect.bottom = fTextRect.top + saveHeight; 3613 if (eraseRect.bottom > eraseRect.top && eraseRect.Intersects(bounds)) { 3614 SetLowColor(ViewColor()); 3615 FillRect(eraseRect, B_SOLID_LOW); 3616 } 3617 3618 // update the scroll bars if the text area has changed 3619 if (newHeight != saveHeight || fMinTextRectWidth != saveWidth) 3620 _UpdateScrollbars(); 3621 3622 if (scroll) 3623 ScrollToOffset(fSelEnd); 3624 3625 Flush(); 3626 } 3627 3628 3629 /*! Recalculate line breaks between two lines. 3630 3631 \param startLine The line number to start recalculating line breaks. 3632 \param endLine The line number to stop recalculating line breaks. 3633 */ 3634 void 3635 BTextView::_RecalculateLineBreaks(int32* startLine, int32* endLine) 3636 { 3637 CALLED(); 3638 3639 // are we insane? 3640 *startLine = (*startLine < 0) ? 0 : *startLine; 3641 *endLine = (*endLine > fLines->NumLines() - 1) ? fLines->NumLines() - 1 3642 : *endLine; 3643 3644 int32 textLength = fText->Length(); 3645 int32 lineIndex = (*startLine > 0) ? *startLine - 1 : 0; 3646 int32 recalThreshold = (*fLines)[*endLine + 1]->offset; 3647 float width = max_c(fTextRect.Width(), 10); 3648 // TODO: The minimum width of 10 is a work around for the following 3649 // problem: If the text rect is too small, we are not calculating any 3650 // line heights, not even for the first line. Maybe this is a bug 3651 // in the algorithm, but other places in the class rely on at least 3652 // the first line to return a valid height. Maybe "10" should really 3653 // be the width of the very first glyph instead. 3654 STELine* curLine = (*fLines)[lineIndex]; 3655 STELine* nextLine = curLine + 1; 3656 3657 do { 3658 float ascent, descent; 3659 int32 fromOffset = curLine->offset; 3660 int32 toOffset = _FindLineBreak(fromOffset, &ascent, &descent, &width); 3661 3662 curLine->ascent = ascent; 3663 curLine->width = width; 3664 3665 // we want to advance at least by one character 3666 int32 nextOffset = _NextInitialByte(fromOffset); 3667 if (toOffset < nextOffset && fromOffset < textLength) 3668 toOffset = nextOffset; 3669 3670 lineIndex++; 3671 STELine saveLine = *nextLine; 3672 if (lineIndex > fLines->NumLines() || toOffset < nextLine->offset) { 3673 // the new line comes before the old line start, add a line 3674 STELine newLine; 3675 newLine.offset = toOffset; 3676 newLine.origin = ceilf(curLine->origin + ascent + descent) + 1; 3677 newLine.ascent = 0; 3678 fLines->InsertLine(&newLine, lineIndex); 3679 } else { 3680 // update the existing line 3681 nextLine->offset = toOffset; 3682 nextLine->origin = ceilf(curLine->origin + ascent + descent) + 1; 3683 3684 // remove any lines that start before the current line 3685 while (lineIndex < fLines->NumLines() 3686 && toOffset >= ((*fLines)[lineIndex] + 1)->offset) { 3687 fLines->RemoveLines(lineIndex + 1); 3688 } 3689 3690 nextLine = (*fLines)[lineIndex]; 3691 if (nextLine->offset == saveLine.offset) { 3692 if (nextLine->offset >= recalThreshold) { 3693 if (nextLine->origin != saveLine.origin) 3694 fLines->BumpOrigin(nextLine->origin - saveLine.origin, 3695 lineIndex + 1); 3696 break; 3697 } 3698 } else { 3699 if (lineIndex > 0 && lineIndex == *startLine) 3700 *startLine = lineIndex - 1; 3701 } 3702 } 3703 3704 curLine = (*fLines)[lineIndex]; 3705 nextLine = curLine + 1; 3706 } while (curLine->offset < textLength); 3707 3708 // make sure that the sentinel line (which starts at the end of the buffer) 3709 // has always a width of 0 3710 (*fLines)[fLines->NumLines()]->width = 0; 3711 3712 // update the text rect 3713 float newHeight = TextHeight(0, fLines->NumLines() - 1); 3714 fTextRect.bottom = fTextRect.top + newHeight; 3715 if (!fWrap) { 3716 fMinTextRectWidth = fLines->MaxWidth(); 3717 fTextRect.right = ceilf(fTextRect.left + fMinTextRectWidth); 3718 } 3719 3720 *endLine = lineIndex - 1; 3721 *startLine = min_c(*startLine, *endLine); 3722 } 3723 3724 3725 int32 3726 BTextView::_FindLineBreak(int32 fromOffset, float* _ascent, float* _descent, 3727 float* inOutWidth) 3728 { 3729 *_ascent = 0.0; 3730 *_descent = 0.0; 3731 3732 const int32 limit = fText->Length(); 3733 3734 // is fromOffset at the end? 3735 if (fromOffset >= limit) { 3736 // try to return valid height info anyway 3737 if (fStyles->NumRuns() > 0) { 3738 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, _ascent, 3739 _descent); 3740 } else { 3741 if (fStyles->IsValidNullStyle()) { 3742 const BFont* font = NULL; 3743 fStyles->GetNullStyle(&font, NULL); 3744 3745 font_height fh; 3746 font->GetHeight(&fh); 3747 *_ascent = fh.ascent; 3748 *_descent = fh.descent + fh.leading; 3749 } 3750 } 3751 *inOutWidth = 0; 3752 3753 return limit; 3754 } 3755 3756 int32 offset = fromOffset; 3757 3758 if (!fWrap) { 3759 // Text wrapping is turned off. 3760 // Just find the offset of the first \n character 3761 offset = limit - fromOffset; 3762 fText->FindChar(B_ENTER, fromOffset, &offset); 3763 offset += fromOffset; 3764 int32 toOffset = (offset < limit) ? offset : limit; 3765 3766 *inOutWidth = _TabExpandedStyledWidth(fromOffset, toOffset - fromOffset, 3767 _ascent, _descent); 3768 3769 return offset < limit ? offset + 1 : limit; 3770 } 3771 3772 bool done = false; 3773 float ascent = 0.0; 3774 float descent = 0.0; 3775 int32 delta = 0; 3776 float deltaWidth = 0.0; 3777 float strWidth = 0.0; 3778 uchar theChar; 3779 3780 // wrap the text 3781 while (offset < limit && !done) { 3782 // find the next line break candidate 3783 for (; (offset + delta) < limit; delta++) { 3784 if (CanEndLine(offset + delta)) { 3785 theChar = fText->RealCharAt(offset + delta); 3786 if (theChar != B_SPACE && theChar != B_TAB 3787 && theChar != B_ENTER) { 3788 // we are scanning for trailing whitespace below, so we 3789 // have to skip non-whitespace characters, that can end 3790 // the line, here 3791 delta++; 3792 } 3793 break; 3794 } 3795 } 3796 3797 int32 deltaBeforeWhitespace = delta; 3798 // now skip over trailing whitespace, if any 3799 for (; (offset + delta) < limit; delta++) { 3800 theChar = fText->RealCharAt(offset + delta); 3801 if (theChar == B_ENTER) { 3802 // found a newline, we're done! 3803 done = true; 3804 delta++; 3805 break; 3806 } else if (theChar != B_SPACE && theChar != B_TAB) { 3807 // stop at anything else than trailing whitespace 3808 break; 3809 } 3810 } 3811 3812 delta = max_c(delta, 1); 3813 3814 // do not include B_ENTER-terminator into width & height calculations 3815 deltaWidth = _TabExpandedStyledWidth(offset, 3816 done ? delta - 1 : delta, &ascent, &descent); 3817 strWidth += deltaWidth; 3818 3819 if (strWidth >= *inOutWidth) { 3820 // we've found where the line will wrap 3821 done = true; 3822 3823 // we have included trailing whitespace in the width computation 3824 // above, but that is not being shown anyway, so we try again 3825 // without the trailing whitespace 3826 if (delta == deltaBeforeWhitespace) { 3827 // there is no trailing whitespace, no point in trying 3828 break; 3829 } 3830 3831 // reset string width to start of current run ... 3832 strWidth -= deltaWidth; 3833 3834 // ... and compute the resulting width (of visible characters) 3835 strWidth += _StyledWidth(offset, deltaBeforeWhitespace, NULL, NULL); 3836 if (strWidth >= *inOutWidth) { 3837 // width of visible characters exceeds line, we need to wrap 3838 // before the current "word" 3839 break; 3840 } 3841 } 3842 3843 *_ascent = max_c(ascent, *_ascent); 3844 *_descent = max_c(descent, *_descent); 3845 3846 offset += delta; 3847 delta = 0; 3848 } 3849 3850 if (offset - fromOffset < 1) { 3851 // there weren't any words that fit entirely in this line 3852 // force a break in the middle of a word 3853 *_ascent = 0.0; 3854 *_descent = 0.0; 3855 strWidth = 0.0; 3856 3857 int32 current = fromOffset; 3858 for (offset = _NextInitialByte(current); current < limit; 3859 current = offset, offset = _NextInitialByte(offset)) { 3860 strWidth += _StyledWidth(current, offset - current, &ascent, 3861 &descent); 3862 if (strWidth >= *inOutWidth) { 3863 offset = _PreviousInitialByte(offset); 3864 break; 3865 } 3866 3867 *_ascent = max_c(ascent, *_ascent); 3868 *_descent = max_c(descent, *_descent); 3869 } 3870 } 3871 3872 return min_c(offset, limit); 3873 } 3874 3875 3876 int32 3877 BTextView::_PreviousLineStart(int32 offset) 3878 { 3879 if (offset <= 0) 3880 return 0; 3881 3882 while (offset > 0) { 3883 offset = _PreviousInitialByte(offset); 3884 if (_CharClassification(offset) == CHAR_CLASS_WHITESPACE 3885 && ByteAt(offset) == B_ENTER) { 3886 return offset + 1; 3887 } 3888 } 3889 3890 return offset; 3891 } 3892 3893 3894 int32 3895 BTextView::_NextLineEnd(int32 offset) 3896 { 3897 int32 textLen = fText->Length(); 3898 if (offset >= textLen) 3899 return textLen; 3900 3901 while (offset < textLen) { 3902 if (_CharClassification(offset) == CHAR_CLASS_WHITESPACE 3903 && ByteAt(offset) == B_ENTER) { 3904 break; 3905 } 3906 offset = _NextInitialByte(offset); 3907 } 3908 3909 return offset; 3910 } 3911 3912 3913 int32 3914 BTextView::_PreviousWordBoundary(int32 offset) 3915 { 3916 uint32 charType = _CharClassification(offset); 3917 int32 previous; 3918 while (offset > 0) { 3919 previous = _PreviousInitialByte(offset); 3920 if (_CharClassification(previous) != charType) 3921 break; 3922 offset = previous; 3923 } 3924 3925 return offset; 3926 } 3927 3928 3929 int32 3930 BTextView::_NextWordBoundary(int32 offset) 3931 { 3932 int32 textLen = fText->Length(); 3933 uint32 charType = _CharClassification(offset); 3934 while (offset < textLen) { 3935 offset = _NextInitialByte(offset); 3936 if (_CharClassification(offset) != charType) 3937 break; 3938 } 3939 3940 return offset; 3941 } 3942 3943 3944 int32 3945 BTextView::_PreviousWordStart(int32 offset) 3946 { 3947 if (offset <= 1) 3948 return 0; 3949 3950 --offset; 3951 // need to look at previous char 3952 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) { 3953 // skip non-word characters 3954 while (offset > 0) { 3955 offset = _PreviousInitialByte(offset); 3956 if (_CharClassification(offset) == CHAR_CLASS_DEFAULT) 3957 break; 3958 } 3959 } 3960 while (offset > 0) { 3961 // skip to start of word 3962 int32 previous = _PreviousInitialByte(offset); 3963 if (_CharClassification(previous) != CHAR_CLASS_DEFAULT) 3964 break; 3965 offset = previous; 3966 } 3967 3968 return offset; 3969 } 3970 3971 3972 int32 3973 BTextView::_NextWordEnd(int32 offset) 3974 { 3975 int32 textLen = fText->Length(); 3976 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) { 3977 // skip non-word characters 3978 while (offset < textLen) { 3979 offset = _NextInitialByte(offset); 3980 if (_CharClassification(offset) == CHAR_CLASS_DEFAULT) 3981 break; 3982 } 3983 } 3984 while (offset < textLen) { 3985 // skip to end of word 3986 offset = _NextInitialByte(offset); 3987 if (_CharClassification(offset) != CHAR_CLASS_DEFAULT) 3988 break; 3989 } 3990 3991 return offset; 3992 } 3993 3994 3995 /*! Returns the width used by the characters starting at the given 3996 offset with the given length, expanding all tab characters as needed. 3997 */ 3998 float 3999 BTextView::_TabExpandedStyledWidth(int32 offset, int32 length, float* _ascent, 4000 float* _descent) const 4001 { 4002 float ascent = 0.0; 4003 float descent = 0.0; 4004 float maxAscent = 0.0; 4005 float maxDescent = 0.0; 4006 4007 float width = 0.0; 4008 int32 numBytes = length; 4009 bool foundTab = false; 4010 do { 4011 foundTab = fText->FindChar(B_TAB, offset, &numBytes); 4012 width += _StyledWidth(offset, numBytes, &ascent, &descent); 4013 4014 if (maxAscent < ascent) 4015 maxAscent = ascent; 4016 if (maxDescent < descent) 4017 maxDescent = descent; 4018 4019 if (foundTab) { 4020 width += _ActualTabWidth(width); 4021 numBytes++; 4022 } 4023 4024 offset += numBytes; 4025 length -= numBytes; 4026 numBytes = length; 4027 } while (foundTab && length > 0); 4028 4029 if (_ascent != NULL) 4030 *_ascent = maxAscent; 4031 if (_descent != NULL) 4032 *_descent = maxDescent; 4033 4034 return width; 4035 } 4036 4037 4038 /*! Calculate the width of the text within the given limits. 4039 4040 \param fromOffset The offset where to start. 4041 \param length The length of the text to examine. 4042 \param _ascent A pointer to a float which will contain the maximum ascent. 4043 \param _descent A pointer to a float which will contain the maximum descent. 4044 4045 \return The width for the text within the given limits. 4046 */ 4047 float 4048 BTextView::_StyledWidth(int32 fromOffset, int32 length, float* _ascent, 4049 float* _descent) const 4050 { 4051 if (length == 0) { 4052 // determine height of char at given offset, but return empty width 4053 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, _ascent, 4054 _descent); 4055 return 0.0; 4056 } 4057 4058 float result = 0.0; 4059 float ascent = 0.0; 4060 float descent = 0.0; 4061 float maxAscent = 0.0; 4062 float maxDescent = 0.0; 4063 4064 // iterate through the style runs 4065 const BFont* font = NULL; 4066 int32 numBytes; 4067 while ((numBytes = fStyles->Iterate(fromOffset, length, fInline, &font, 4068 NULL, &ascent, &descent)) != 0) { 4069 maxAscent = max_c(ascent, maxAscent); 4070 maxDescent = max_c(descent, maxDescent); 4071 4072 #if USE_WIDTHBUFFER 4073 // Use _BWidthBuffer_ if possible 4074 if (BPrivate::gWidthBuffer != NULL) { 4075 result += BPrivate::gWidthBuffer->StringWidth(*fText, fromOffset, 4076 numBytes, font); 4077 } else { 4078 #endif 4079 const char* text = fText->GetString(fromOffset, &numBytes); 4080 result += font->StringWidth(text, numBytes); 4081 4082 #if USE_WIDTHBUFFER 4083 } 4084 #endif 4085 4086 fromOffset += numBytes; 4087 length -= numBytes; 4088 } 4089 4090 if (_ascent != NULL) 4091 *_ascent = maxAscent; 4092 if (_descent != NULL) 4093 *_descent = maxDescent; 4094 4095 return result; 4096 } 4097 4098 4099 //! Calculate the actual tab width for the given location. 4100 float 4101 BTextView::_ActualTabWidth(float location) const 4102 { 4103 float tabWidth = fTabWidth - fmod(location, fTabWidth); 4104 if (round(tabWidth) == 0) 4105 tabWidth = fTabWidth; 4106 4107 return tabWidth; 4108 } 4109 4110 4111 void 4112 BTextView::_DoInsertText(const char* text, int32 length, int32 offset, 4113 const text_run_array* runs) 4114 { 4115 _CancelInputMethod(); 4116 4117 if (TextLength() + length > MaxBytes()) 4118 return; 4119 4120 if (fSelStart != fSelEnd) 4121 Select(fSelStart, fSelStart); 4122 4123 const int32 textLength = TextLength(); 4124 if (offset > textLength) 4125 offset = textLength; 4126 4127 // copy data into buffer 4128 InsertText(text, length, offset, runs); 4129 4130 // recalc line breaks and draw the text 4131 _Refresh(offset, offset + length, false); 4132 } 4133 4134 4135 void 4136 BTextView::_DoDeleteText(int32 fromOffset, int32 toOffset) 4137 { 4138 CALLED(); 4139 } 4140 4141 4142 void 4143 BTextView::_DrawLine(BView* view, const int32 &lineNum, 4144 const int32 &startOffset, const bool &erase, BRect &eraseRect, 4145 BRegion &inputRegion) 4146 { 4147 STELine* line = (*fLines)[lineNum]; 4148 float startLeft = fTextRect.left; 4149 if (startOffset != -1) { 4150 if (ByteAt(startOffset) == B_ENTER) { 4151 // StartOffset is a newline 4152 startLeft = PointAt(line->offset).x; 4153 } else 4154 startLeft = PointAt(startOffset).x; 4155 } 4156 else if (fAlignment != B_ALIGN_LEFT) { 4157 float alignmentOffset = fTextRect.Width() - LineWidth(lineNum); 4158 if (fAlignment == B_ALIGN_CENTER) 4159 alignmentOffset /= 2; 4160 startLeft = fTextRect.left + alignmentOffset; 4161 } 4162 4163 int32 length = (line + 1)->offset; 4164 if (startOffset != -1) 4165 length -= startOffset; 4166 else 4167 length -= line->offset; 4168 4169 // DrawString() chokes if you draw a newline 4170 if (ByteAt((line + 1)->offset - 1) == B_ENTER) 4171 length--; 4172 4173 view->MovePenTo(startLeft, line->origin + line->ascent + fTextRect.top + 1); 4174 4175 if (erase) { 4176 eraseRect.top = line->origin + fTextRect.top; 4177 eraseRect.bottom = (line + 1)->origin + fTextRect.top; 4178 view->FillRect(eraseRect, B_SOLID_LOW); 4179 } 4180 4181 // do we have any text to draw? 4182 if (length <= 0) 4183 return; 4184 4185 bool foundTab = false; 4186 int32 tabChars = 0; 4187 int32 numTabs = 0; 4188 int32 offset = startOffset != -1 ? startOffset : line->offset; 4189 const BFont* font = NULL; 4190 const rgb_color* color = NULL; 4191 int32 numBytes; 4192 drawing_mode defaultTextRenderingMode = DrawingMode(); 4193 // iterate through each style on this line 4194 while ((numBytes = fStyles->Iterate(offset, length, fInline, &font, 4195 &color)) != 0) { 4196 view->SetFont(font); 4197 view->SetHighColor(*color); 4198 4199 tabChars = min_c(numBytes, length); 4200 do { 4201 foundTab = fText->FindChar(B_TAB, offset, &tabChars); 4202 if (foundTab) { 4203 do { 4204 numTabs++; 4205 if (ByteAt(offset + tabChars + numTabs) != B_TAB) 4206 break; 4207 } while ((tabChars + numTabs) < numBytes); 4208 } 4209 4210 drawing_mode textRenderingMode = defaultTextRenderingMode; 4211 4212 if (inputRegion.CountRects() > 0 4213 && ((offset <= fInline->Offset() 4214 && fInline->Offset() < offset + tabChars) 4215 || (fInline->Offset() <= offset 4216 && offset < fInline->Offset() + fInline->Length()))) { 4217 4218 textRenderingMode = B_OP_OVER; 4219 4220 BRegion textRegion; 4221 GetTextRegion(offset, offset + length, &textRegion); 4222 4223 textRegion.IntersectWith(&inputRegion); 4224 view->PushState(); 4225 4226 // Highlight in blue the inputted text 4227 view->SetHighColor(kBlueInputColor); 4228 view->FillRect(textRegion.Frame()); 4229 4230 // Highlight in red the selected part 4231 if (fInline->SelectionLength() > 0) { 4232 BRegion selectedRegion; 4233 GetTextRegion(fInline->Offset() 4234 + fInline->SelectionOffset(), fInline->Offset() 4235 + fInline->SelectionOffset() 4236 + fInline->SelectionLength(), &selectedRegion); 4237 4238 textRegion.IntersectWith(&selectedRegion); 4239 4240 view->SetHighColor(kRedInputColor); 4241 view->FillRect(textRegion.Frame()); 4242 } 4243 4244 view->PopState(); 4245 } 4246 4247 int32 returnedBytes = tabChars; 4248 const char* stringToDraw = fText->GetString(offset, &returnedBytes); 4249 view->SetDrawingMode(textRenderingMode); 4250 view->DrawString(stringToDraw, returnedBytes); 4251 if (foundTab) { 4252 float penPos = PenLocation().x - fTextRect.left; 4253 float tabWidth = _ActualTabWidth(penPos); 4254 if (numTabs > 1) 4255 tabWidth += ((numTabs - 1) * fTabWidth); 4256 4257 view->MovePenBy(tabWidth, 0.0); 4258 tabChars += numTabs; 4259 } 4260 4261 offset += tabChars; 4262 length -= tabChars; 4263 numBytes -= tabChars; 4264 tabChars = min_c(numBytes, length); 4265 numTabs = 0; 4266 } while (foundTab && tabChars > 0); 4267 } 4268 } 4269 4270 4271 void 4272 BTextView::_DrawLines(int32 startLine, int32 endLine, int32 startOffset, 4273 bool erase) 4274 { 4275 if (!Window()) 4276 return; 4277 4278 // clip the text 4279 BRect textRect(fTextRect); 4280 float minWidth 4281 = Bounds().Width() - fLayoutData->leftInset - fLayoutData->rightInset; 4282 if (textRect.Width() < minWidth) 4283 textRect.right = textRect.left + minWidth; 4284 BRect clipRect = Bounds() & textRect; 4285 clipRect.InsetBy(-1, -1); 4286 4287 BRegion newClip; 4288 newClip.Set(clipRect); 4289 ConstrainClippingRegion(&newClip); 4290 4291 // set the low color to the view color so that 4292 // drawing to a non-white background will work 4293 SetLowColor(ViewColor()); 4294 4295 BView* view = NULL; 4296 if (fOffscreen == NULL) 4297 view = this; 4298 else { 4299 fOffscreen->Lock(); 4300 view = fOffscreen->ChildAt(0); 4301 view->SetLowColor(ViewColor()); 4302 view->FillRect(view->Bounds(), B_SOLID_LOW); 4303 } 4304 4305 long maxLine = fLines->NumLines() - 1; 4306 if (startLine < 0) 4307 startLine = 0; 4308 if (endLine > maxLine) 4309 endLine = maxLine; 4310 4311 // TODO: See if we can avoid this 4312 if (fAlignment != B_ALIGN_LEFT) 4313 erase = true; 4314 4315 BRect eraseRect = clipRect; 4316 int32 startEraseLine = startLine; 4317 STELine* line = (*fLines)[startLine]; 4318 4319 if (erase && startOffset != -1 && fAlignment == B_ALIGN_LEFT) { 4320 // erase only to the right of startOffset 4321 startEraseLine++; 4322 int32 startErase = startOffset; 4323 4324 BPoint erasePoint = PointAt(startErase); 4325 eraseRect.left = erasePoint.x; 4326 eraseRect.top = erasePoint.y; 4327 eraseRect.bottom = (line + 1)->origin + fTextRect.top; 4328 4329 view->FillRect(eraseRect, B_SOLID_LOW); 4330 4331 eraseRect = clipRect; 4332 } 4333 4334 BRegion inputRegion; 4335 if (fInline != NULL && fInline->IsActive()) { 4336 GetTextRegion(fInline->Offset(), fInline->Offset() + fInline->Length(), 4337 &inputRegion); 4338 } 4339 4340 //BPoint leftTop(startLeft, line->origin); 4341 for (int32 lineNum = startLine; lineNum <= endLine; lineNum++) { 4342 const bool eraseThisLine = erase && lineNum >= startEraseLine; 4343 _DrawLine(view, lineNum, startOffset, eraseThisLine, eraseRect, 4344 inputRegion); 4345 startOffset = -1; 4346 // Set this to -1 so the next iteration will use the line offset 4347 } 4348 4349 // draw the caret/hilite the selection 4350 if (fActive) { 4351 if (fSelStart != fSelEnd) { 4352 if (fSelectable) 4353 Highlight(fSelStart, fSelEnd); 4354 } else { 4355 if (fCaretVisible) 4356 _DrawCaret(fSelStart, true); 4357 } 4358 } 4359 4360 if (fOffscreen != NULL) { 4361 view->Sync(); 4362 /*BPoint penLocation = view->PenLocation(); 4363 BRect drawRect(leftTop.x, leftTop.y, penLocation.x, penLocation.y); 4364 DrawBitmap(fOffscreen, drawRect, drawRect);*/ 4365 fOffscreen->Unlock(); 4366 } 4367 4368 ConstrainClippingRegion(NULL); 4369 } 4370 4371 4372 void 4373 BTextView::_RequestDrawLines(int32 startLine, int32 endLine) 4374 { 4375 if (!Window()) 4376 return; 4377 4378 long maxLine = fLines->NumLines() - 1; 4379 4380 STELine* from = (*fLines)[startLine]; 4381 STELine* to = endLine == maxLine ? NULL : (*fLines)[endLine + 1]; 4382 BRect invalidRect(Bounds().left, from->origin + fTextRect.top, 4383 Bounds().right, 4384 to != NULL ? to->origin + fTextRect.top : fTextRect.bottom); 4385 Invalidate(invalidRect); 4386 Window()->UpdateIfNeeded(); 4387 } 4388 4389 4390 void 4391 BTextView::_DrawCaret(int32 offset, bool visible) 4392 { 4393 float lineHeight; 4394 BPoint caretPoint = PointAt(offset, &lineHeight); 4395 caretPoint.x = min_c(caretPoint.x, fTextRect.right); 4396 4397 BRect caretRect; 4398 caretRect.left = caretRect.right = caretPoint.x; 4399 caretRect.top = caretPoint.y; 4400 caretRect.bottom = caretPoint.y + lineHeight - 1; 4401 4402 if (visible) 4403 InvertRect(caretRect); 4404 else 4405 Invalidate(caretRect); 4406 } 4407 4408 4409 inline void 4410 BTextView::_ShowCaret() 4411 { 4412 if (fActive && !fCaretVisible && fEditable && fSelStart == fSelEnd) 4413 _InvertCaret(); 4414 } 4415 4416 4417 inline void 4418 BTextView::_HideCaret() 4419 { 4420 if (fCaretVisible && fSelStart == fSelEnd) 4421 _InvertCaret(); 4422 } 4423 4424 4425 //! Hides the caret if it is being shown, and if it's hidden, shows it. 4426 void 4427 BTextView::_InvertCaret() 4428 { 4429 fCaretVisible = !fCaretVisible; 4430 _DrawCaret(fSelStart, fCaretVisible); 4431 fCaretTime = system_time(); 4432 } 4433 4434 4435 /*! Place the dragging caret at the given offset. 4436 4437 \param offset The offset (zero based within the object's text) where to 4438 place the dragging caret. If it's -1, hide the caret. 4439 */ 4440 void 4441 BTextView::_DragCaret(int32 offset) 4442 { 4443 // does the caret need to move? 4444 if (offset == fDragOffset) 4445 return; 4446 4447 // hide the previous drag caret 4448 if (fDragOffset != -1) 4449 _DrawCaret(fDragOffset, false); 4450 4451 // do we have a new location? 4452 if (offset != -1) { 4453 if (fActive) { 4454 // ignore if offset is within active selection 4455 if (offset >= fSelStart && offset <= fSelEnd) { 4456 fDragOffset = -1; 4457 return; 4458 } 4459 } 4460 4461 _DrawCaret(offset, true); 4462 } 4463 4464 fDragOffset = offset; 4465 } 4466 4467 4468 void 4469 BTextView::_StopMouseTracking() 4470 { 4471 delete fTrackingMouse; 4472 fTrackingMouse = NULL; 4473 } 4474 4475 4476 bool 4477 BTextView::_PerformMouseUp(BPoint where) 4478 { 4479 if (fTrackingMouse == NULL) 4480 return false; 4481 4482 if (fTrackingMouse->selectionRect.IsValid()) 4483 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset); 4484 4485 _StopMouseTracking(); 4486 // adjust cursor if necessary 4487 _TrackMouse(where, NULL, true); 4488 4489 return true; 4490 } 4491 4492 4493 bool 4494 BTextView::_PerformMouseMoved(BPoint where, uint32 code) 4495 { 4496 fWhere = where; 4497 4498 if (fTrackingMouse == NULL) 4499 return false; 4500 4501 int32 currentOffset = OffsetAt(where); 4502 if (fTrackingMouse->selectionRect.IsValid()) { 4503 // we are tracking the mouse for drag action, if the mouse has moved 4504 // to another index or more than three pixels from where it was clicked, 4505 // we initiate a drag now: 4506 if (currentOffset != fTrackingMouse->clickOffset 4507 || fabs(fTrackingMouse->where.x - where.x) > 3 4508 || fabs(fTrackingMouse->where.y - where.y) > 3) { 4509 _StopMouseTracking(); 4510 _InitiateDrag(); 4511 return true; 4512 } 4513 return false; 4514 } 4515 4516 switch (fClickCount) { 4517 case 3: 4518 // triple click, extend selection linewise 4519 if (currentOffset <= fTrackingMouse->anchor) { 4520 fTrackingMouse->selStart 4521 = (*fLines)[_LineAt(currentOffset)]->offset; 4522 fTrackingMouse->selEnd = fTrackingMouse->shiftDown 4523 ? fSelEnd 4524 : (*fLines)[_LineAt(fTrackingMouse->anchor) + 1]->offset; 4525 } else { 4526 fTrackingMouse->selStart 4527 = fTrackingMouse->shiftDown 4528 ? fSelStart 4529 : (*fLines)[_LineAt(fTrackingMouse->anchor)]->offset; 4530 fTrackingMouse->selEnd 4531 = (*fLines)[_LineAt(currentOffset) + 1]->offset; 4532 } 4533 break; 4534 4535 case 2: 4536 // double click, extend selection wordwise 4537 if (currentOffset <= fTrackingMouse->anchor) { 4538 fTrackingMouse->selStart = _PreviousWordBoundary(currentOffset); 4539 fTrackingMouse->selEnd 4540 = fTrackingMouse->shiftDown 4541 ? fSelEnd 4542 : _NextWordBoundary(fTrackingMouse->anchor); 4543 } else { 4544 fTrackingMouse->selStart 4545 = fTrackingMouse->shiftDown 4546 ? fSelStart 4547 : _PreviousWordBoundary(fTrackingMouse->anchor); 4548 fTrackingMouse->selEnd = _NextWordBoundary(currentOffset); 4549 } 4550 break; 4551 4552 default: 4553 // new click, extend selection char by char 4554 if (currentOffset <= fTrackingMouse->anchor) { 4555 fTrackingMouse->selStart = currentOffset; 4556 fTrackingMouse->selEnd 4557 = fTrackingMouse->shiftDown 4558 ? fSelEnd : fTrackingMouse->anchor; 4559 } else { 4560 fTrackingMouse->selStart 4561 = fTrackingMouse->shiftDown 4562 ? fSelStart : fTrackingMouse->anchor; 4563 fTrackingMouse->selEnd = currentOffset; 4564 } 4565 break; 4566 } 4567 4568 // position caret to follow the direction of the selection 4569 if (fTrackingMouse->selEnd != fSelEnd) 4570 fCaretOffset = fTrackingMouse->selEnd; 4571 else if (fTrackingMouse->selStart != fSelStart) 4572 fCaretOffset = fTrackingMouse->selStart; 4573 4574 Select(fTrackingMouse->selStart, fTrackingMouse->selEnd); 4575 _TrackMouse(where, NULL); 4576 4577 return true; 4578 } 4579 4580 4581 /*! Tracks the mouse position, doing special actions like changing the 4582 view cursor. 4583 4584 \param where The point where the mouse has moved. 4585 \param message The dragging message, if there is any. 4586 \param force Passed as second parameter of SetViewCursor() 4587 */ 4588 void 4589 BTextView::_TrackMouse(BPoint where, const BMessage* message, bool force) 4590 { 4591 BRegion textRegion; 4592 GetTextRegion(fSelStart, fSelEnd, &textRegion); 4593 4594 if (message && AcceptsDrop(message)) 4595 _TrackDrag(where); 4596 else if ((fSelectable || fEditable) 4597 && (fTrackingMouse != NULL || !textRegion.Contains(where))) { 4598 SetViewCursor(B_CURSOR_I_BEAM, force); 4599 } else 4600 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT, force); 4601 } 4602 4603 4604 //! Tracks the mouse position when the user is dragging some data. 4605 void 4606 BTextView::_TrackDrag(BPoint where) 4607 { 4608 CALLED(); 4609 if (Bounds().Contains(where)) 4610 _DragCaret(OffsetAt(where)); 4611 } 4612 4613 4614 //! Initiates a drag operation. 4615 void 4616 BTextView::_InitiateDrag() 4617 { 4618 BMessage dragMessage(B_MIME_DATA); 4619 BBitmap* dragBitmap = NULL; 4620 BPoint bitmapPoint; 4621 BHandler* dragHandler = NULL; 4622 4623 GetDragParameters(&dragMessage, &dragBitmap, &bitmapPoint, &dragHandler); 4624 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); 4625 4626 if (dragBitmap != NULL) 4627 DragMessage(&dragMessage, dragBitmap, bitmapPoint, dragHandler); 4628 else { 4629 BRegion region; 4630 GetTextRegion(fSelStart, fSelEnd, ®ion); 4631 BRect bounds = Bounds(); 4632 BRect dragRect = region.Frame(); 4633 if (!bounds.Contains(dragRect)) 4634 dragRect = bounds & dragRect; 4635 4636 DragMessage(&dragMessage, dragRect, dragHandler); 4637 } 4638 4639 BMessenger messenger(this); 4640 BMessage message(_DISPOSE_DRAG_); 4641 fDragRunner = new (nothrow) BMessageRunner(messenger, &message, 100000); 4642 } 4643 4644 4645 //! Handles when some data is dropped on the view. 4646 bool 4647 BTextView::_MessageDropped(BMessage* message, BPoint where, BPoint offset) 4648 { 4649 ASSERT(message); 4650 4651 void* from = NULL; 4652 bool internalDrop = false; 4653 if (message->FindPointer("be:originator", &from) == B_OK 4654 && from == this && fSelEnd != fSelStart) 4655 internalDrop = true; 4656 4657 _DragCaret(-1); 4658 4659 delete fDragRunner; 4660 fDragRunner = NULL; 4661 4662 _TrackMouse(where, NULL); 4663 4664 // are we sure we like this message? 4665 if (!AcceptsDrop(message)) 4666 return false; 4667 4668 int32 dropOffset = OffsetAt(where); 4669 if (dropOffset > TextLength()) 4670 dropOffset = TextLength(); 4671 4672 // if this view initiated the drag, move instead of copy 4673 if (internalDrop) { 4674 // dropping onto itself? 4675 if (dropOffset >= fSelStart && dropOffset <= fSelEnd) 4676 return true; 4677 } 4678 4679 ssize_t dataLength = 0; 4680 const char* text = NULL; 4681 entry_ref ref; 4682 if (message->FindData("text/plain", B_MIME_TYPE, (const void**)&text, 4683 &dataLength) == B_OK) { 4684 text_run_array* runArray = NULL; 4685 ssize_t runLength = 0; 4686 if (fStylable) { 4687 message->FindData("application/x-vnd.Be-text_run_array", 4688 B_MIME_TYPE, (const void**)&runArray, &runLength); 4689 } 4690 4691 _FilterDisallowedChars((char*)text, dataLength, runArray); 4692 4693 if (dataLength < 1) { 4694 beep(); 4695 return true; 4696 } 4697 4698 if (fUndo) { 4699 delete fUndo; 4700 fUndo = new DropUndoBuffer(this, text, dataLength, runArray, 4701 runLength, dropOffset, internalDrop); 4702 } 4703 4704 if (internalDrop) { 4705 if (dropOffset > fSelEnd) 4706 dropOffset -= dataLength; 4707 Delete(); 4708 } 4709 4710 Insert(dropOffset, text, dataLength, runArray); 4711 } 4712 4713 return true; 4714 } 4715 4716 4717 void 4718 BTextView::_PerformAutoScrolling() 4719 { 4720 // Scroll the view a bit if mouse is outside the view bounds 4721 BRect bounds = Bounds(); 4722 BPoint scrollBy(B_ORIGIN); 4723 4724 // R5 does a pretty soft auto-scroll, we try to do the same by 4725 // simply scrolling the distance between cursor and border 4726 if (fWhere.x > bounds.right) { 4727 scrollBy.x = fWhere.x - bounds.right; 4728 } else if (fWhere.x < bounds.left) { 4729 scrollBy.x = fWhere.x - bounds.left; // negative value 4730 } 4731 4732 // prevent from scrolling out of view 4733 if (scrollBy.x != 0.0) { 4734 float rightMax = floorf(fTextRect.right + fLayoutData->rightInset); 4735 if (bounds.right + scrollBy.x > rightMax) 4736 scrollBy.x = rightMax - bounds.right; 4737 if (bounds.left + scrollBy.x < 0) 4738 scrollBy.x = -bounds.left; 4739 } 4740 4741 if (CountLines() > 1) { 4742 // scroll in Y only if multiple lines! 4743 if (fWhere.y > bounds.bottom) { 4744 scrollBy.y = fWhere.y - bounds.bottom; 4745 } else if (fWhere.y < bounds.top) { 4746 scrollBy.y = fWhere.y - bounds.top; // negative value 4747 } 4748 4749 // prevent from scrolling out of view 4750 if (scrollBy.y != 0.0) { 4751 float bottomMax = floorf(fTextRect.bottom 4752 + fLayoutData->bottomInset); 4753 if (bounds.bottom + scrollBy.y > bottomMax) 4754 scrollBy.y = bottomMax - bounds.bottom; 4755 if (bounds.top + scrollBy.y < 0) 4756 scrollBy.y = -bounds.top; 4757 } 4758 } 4759 4760 if (scrollBy != B_ORIGIN) 4761 ScrollBy(scrollBy.x, scrollBy.y); 4762 } 4763 4764 4765 //! Updates the scrollbars associated with the object (if any). 4766 void 4767 BTextView::_UpdateScrollbars() 4768 { 4769 BRect bounds(Bounds()); 4770 BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL); 4771 BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL); 4772 4773 // do we have a horizontal scroll bar? 4774 if (horizontalScrollBar != NULL) { 4775 long viewWidth = bounds.IntegerWidth(); 4776 long dataWidth = (long)ceilf(fTextRect.IntegerWidth() 4777 + fLayoutData->leftInset + fLayoutData->rightInset); 4778 4779 long maxRange = dataWidth - viewWidth; 4780 maxRange = max_c(maxRange, 0); 4781 4782 horizontalScrollBar->SetRange(0, (float)maxRange); 4783 horizontalScrollBar->SetProportion((float)viewWidth / (float)dataWidth); 4784 horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10); 4785 } 4786 4787 // how about a vertical scroll bar? 4788 if (verticalScrollBar != NULL) { 4789 long viewHeight = bounds.IntegerHeight(); 4790 long dataHeight = (long)ceilf(fTextRect.IntegerHeight() 4791 + fLayoutData->topInset + fLayoutData->bottomInset); 4792 4793 long maxRange = dataHeight - viewHeight; 4794 maxRange = max_c(maxRange, 0); 4795 4796 verticalScrollBar->SetRange(0, maxRange); 4797 verticalScrollBar->SetProportion((float)viewHeight / (float)dataHeight); 4798 verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight); 4799 } 4800 } 4801 4802 4803 //! Scrolls by the given offsets 4804 void 4805 BTextView::_ScrollBy(float horizontal, float vertical) 4806 { 4807 BRect bounds = Bounds(); 4808 _ScrollTo(bounds.left + horizontal, bounds.top + vertical); 4809 } 4810 4811 4812 //! Scrolls to the given position, making sure not to scroll out of bounds. 4813 void 4814 BTextView::_ScrollTo(float x, float y) 4815 { 4816 BRect bounds = Bounds(); 4817 long viewWidth = bounds.IntegerWidth(); 4818 long viewHeight = bounds.IntegerHeight(); 4819 4820 if (x > fTextRect.right - viewWidth) 4821 x = fTextRect.right - viewWidth; 4822 if (x < 0.0) 4823 x = 0.0; 4824 4825 if (y > fTextRect.bottom + fLayoutData->bottomInset - viewHeight) 4826 y = fTextRect.bottom + fLayoutData->bottomInset - viewHeight; 4827 if (y < 0.0) 4828 y = 0.0; 4829 4830 ScrollTo(x, y); 4831 } 4832 4833 4834 //! Autoresizes the view to fit the contained text. 4835 void 4836 BTextView::_AutoResize(bool redraw) 4837 { 4838 if (!fResizable) 4839 return; 4840 4841 BRect bounds = Bounds(); 4842 float oldWidth = bounds.Width(); 4843 float newWidth = ceilf(fLayoutData->leftInset + fTextRect.Width() 4844 + fLayoutData->rightInset); 4845 4846 if (fContainerView != NULL) { 4847 // NOTE: This container view thing is only used by Tracker. 4848 // move container view if not left aligned 4849 if (fAlignment == B_ALIGN_CENTER) { 4850 if (fmod(ceilf(newWidth - oldWidth), 2.0) != 0.0) 4851 newWidth += 1; 4852 fContainerView->MoveBy(ceilf(oldWidth - newWidth) / 2, 0); 4853 } else if (fAlignment == B_ALIGN_RIGHT) { 4854 fContainerView->MoveBy(ceilf(oldWidth - newWidth), 0); 4855 } 4856 // resize container view 4857 fContainerView->ResizeBy(ceilf(newWidth - oldWidth), 0); 4858 } 4859 4860 4861 if (redraw) 4862 _RequestDrawLines(0, 0); 4863 4864 // erase any potential left over outside the text rect 4865 // (can only be on right hand side) 4866 BRect dirty(fTextRect.right + 1, fTextRect.top, bounds.right, 4867 fTextRect.bottom); 4868 if (dirty.IsValid()) { 4869 SetLowColor(ViewColor()); 4870 FillRect(dirty, B_SOLID_LOW); 4871 } 4872 } 4873 4874 4875 //! Creates a new offscreen BBitmap with an associated BView. 4876 void 4877 BTextView::_NewOffscreen(float padding) 4878 { 4879 if (fOffscreen != NULL) 4880 _DeleteOffscreen(); 4881 4882 #if USE_DOUBLEBUFFERING 4883 BRect bitmapRect(0, 0, fTextRect.Width() + padding, fTextRect.Height()); 4884 fOffscreen = new BBitmap(bitmapRect, fColorSpace, true, false); 4885 if (fOffscreen != NULL && fOffscreen->Lock()) { 4886 BView* bufferView = new BView(bitmapRect, "drawing view", 0, 0); 4887 fOffscreen->AddChild(bufferView); 4888 fOffscreen->Unlock(); 4889 } 4890 #endif 4891 } 4892 4893 4894 //! Deletes the textview's offscreen bitmap, if any. 4895 void 4896 BTextView::_DeleteOffscreen() 4897 { 4898 if (fOffscreen != NULL && fOffscreen->Lock()) { 4899 delete fOffscreen; 4900 fOffscreen = NULL; 4901 } 4902 } 4903 4904 4905 /*! Creates a new offscreen bitmap, highlight the selection, and set the 4906 cursor to \c B_CURSOR_I_BEAM. 4907 */ 4908 void 4909 BTextView::_Activate() 4910 { 4911 fActive = true; 4912 4913 // Create a new offscreen BBitmap 4914 _NewOffscreen(); 4915 4916 if (fSelStart != fSelEnd) { 4917 if (fSelectable) 4918 Highlight(fSelStart, fSelEnd); 4919 } else 4920 _ShowCaret(); 4921 4922 BPoint where; 4923 uint32 buttons; 4924 GetMouse(&where, &buttons, false); 4925 if (Bounds().Contains(where)) 4926 _TrackMouse(where, NULL); 4927 4928 if (Window() != NULL) { 4929 BMessage* message; 4930 4931 if (!Window()->HasShortcut(B_LEFT_ARROW, B_COMMAND_KEY) 4932 && !Window()->HasShortcut(B_RIGHT_ARROW, B_COMMAND_KEY)) { 4933 message = new BMessage(kMsgNavigateArrow); 4934 message->AddInt32("key", B_LEFT_ARROW); 4935 message->AddInt32("modifiers", B_COMMAND_KEY); 4936 Window()->AddShortcut(B_LEFT_ARROW, B_COMMAND_KEY, message, this); 4937 4938 message = new BMessage(kMsgNavigateArrow); 4939 message->AddInt32("key", B_RIGHT_ARROW); 4940 message->AddInt32("modifiers", B_COMMAND_KEY); 4941 Window()->AddShortcut(B_RIGHT_ARROW, B_COMMAND_KEY, message, this); 4942 4943 fInstalledNavigateCommandWordwiseShortcuts = true; 4944 } 4945 if (!Window()->HasShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY) 4946 && !Window()->HasShortcut(B_RIGHT_ARROW, 4947 B_COMMAND_KEY | B_SHIFT_KEY)) { 4948 message = new BMessage(kMsgNavigateArrow); 4949 message->AddInt32("key", B_LEFT_ARROW); 4950 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY); 4951 Window()->AddShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY, 4952 message, this); 4953 4954 message = new BMessage(kMsgNavigateArrow); 4955 message->AddInt32("key", B_RIGHT_ARROW); 4956 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY); 4957 Window()->AddShortcut(B_RIGHT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY, 4958 message, this); 4959 4960 fInstalledSelectCommandWordwiseShortcuts = true; 4961 } 4962 4963 if (!Window()->HasShortcut(B_LEFT_ARROW, B_OPTION_KEY) 4964 && !Window()->HasShortcut(B_RIGHT_ARROW, B_OPTION_KEY)) { 4965 message = new BMessage(kMsgNavigateArrow); 4966 message->AddInt32("key", B_LEFT_ARROW); 4967 message->AddInt32("modifiers", B_OPTION_KEY); 4968 Window()->AddShortcut(B_LEFT_ARROW, B_OPTION_KEY, message, this); 4969 4970 message = new BMessage(kMsgNavigateArrow); 4971 message->AddInt32("key", B_RIGHT_ARROW); 4972 message->AddInt32("modifiers", B_OPTION_KEY); 4973 Window()->AddShortcut(B_RIGHT_ARROW, B_OPTION_KEY, message, this); 4974 4975 fInstalledNavigateOptionWordwiseShortcuts = true; 4976 } 4977 if (!Window()->HasShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY) 4978 && !Window()->HasShortcut(B_RIGHT_ARROW, 4979 B_OPTION_KEY | B_SHIFT_KEY)) { 4980 message = new BMessage(kMsgNavigateArrow); 4981 message->AddInt32("key", B_LEFT_ARROW); 4982 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY); 4983 Window()->AddShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY, 4984 message, this); 4985 4986 message = new BMessage(kMsgNavigateArrow); 4987 message->AddInt32("key", B_RIGHT_ARROW); 4988 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY); 4989 Window()->AddShortcut(B_RIGHT_ARROW, B_OPTION_KEY | B_SHIFT_KEY, 4990 message, this); 4991 4992 fInstalledSelectOptionWordwiseShortcuts = true; 4993 } 4994 4995 if (!Window()->HasShortcut(B_UP_ARROW, B_OPTION_KEY) 4996 && !Window()->HasShortcut(B_DOWN_ARROW, B_OPTION_KEY)) { 4997 message = new BMessage(kMsgNavigateArrow); 4998 message->AddInt32("key", B_UP_ARROW); 4999 message->AddInt32("modifiers", B_OPTION_KEY); 5000 Window()->AddShortcut(B_UP_ARROW, B_OPTION_KEY, message, this); 5001 5002 message = new BMessage(kMsgNavigateArrow); 5003 message->AddInt32("key", B_DOWN_ARROW); 5004 message->AddInt32("modifiers", B_OPTION_KEY); 5005 Window()->AddShortcut(B_DOWN_ARROW, B_OPTION_KEY, message, this); 5006 5007 fInstalledNavigateOptionLinewiseShortcuts = true; 5008 } 5009 if (!Window()->HasShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY) 5010 && !Window()->HasShortcut(B_DOWN_ARROW, 5011 B_OPTION_KEY | B_SHIFT_KEY)) { 5012 message = new BMessage(kMsgNavigateArrow); 5013 message->AddInt32("key", B_UP_ARROW); 5014 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY); 5015 Window()->AddShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY, 5016 message, this); 5017 5018 message = new BMessage(kMsgNavigateArrow); 5019 message->AddInt32("key", B_DOWN_ARROW); 5020 message->AddInt32("modifiers", B_OPTION_KEY | B_SHIFT_KEY); 5021 Window()->AddShortcut(B_DOWN_ARROW, B_OPTION_KEY | B_SHIFT_KEY, 5022 message, this); 5023 5024 fInstalledSelectOptionLinewiseShortcuts = true; 5025 } 5026 5027 if (!Window()->HasShortcut(B_HOME, B_COMMAND_KEY) 5028 && !Window()->HasShortcut(B_END, B_COMMAND_KEY)) { 5029 message = new BMessage(kMsgNavigatePage); 5030 message->AddInt32("key", B_HOME); 5031 message->AddInt32("modifiers", B_COMMAND_KEY); 5032 Window()->AddShortcut(B_HOME, B_COMMAND_KEY, message, this); 5033 5034 message = new BMessage(kMsgNavigatePage); 5035 message->AddInt32("key", B_END); 5036 message->AddInt32("modifiers", B_COMMAND_KEY); 5037 Window()->AddShortcut(B_END, B_COMMAND_KEY, message, this); 5038 5039 fInstalledNavigateHomeEndDocwiseShortcuts = true; 5040 } 5041 if (!Window()->HasShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY) 5042 && !Window()->HasShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY)) { 5043 message = new BMessage(kMsgNavigatePage); 5044 message->AddInt32("key", B_HOME); 5045 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY); 5046 Window()->AddShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY, 5047 message, this); 5048 5049 message = new BMessage(kMsgNavigatePage); 5050 message->AddInt32("key", B_END); 5051 message->AddInt32("modifiers", B_COMMAND_KEY | B_SHIFT_KEY); 5052 Window()->AddShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY, 5053 message, this); 5054 5055 fInstalledSelectHomeEndDocwiseShortcuts = true; 5056 } 5057 } 5058 } 5059 5060 5061 //! Unhilights the selection, set the cursor to \c B_CURSOR_SYSTEM_DEFAULT. 5062 void 5063 BTextView::_Deactivate() 5064 { 5065 fActive = false; 5066 5067 _CancelInputMethod(); 5068 _DeleteOffscreen(); 5069 5070 if (fSelStart != fSelEnd) { 5071 if (fSelectable) 5072 Highlight(fSelStart, fSelEnd); 5073 } else 5074 _HideCaret(); 5075 5076 if (Window() != NULL) { 5077 if (fInstalledNavigateCommandWordwiseShortcuts) { 5078 Window()->RemoveShortcut(B_LEFT_ARROW, B_COMMAND_KEY); 5079 Window()->RemoveShortcut(B_RIGHT_ARROW, B_COMMAND_KEY); 5080 fInstalledNavigateCommandWordwiseShortcuts = false; 5081 } 5082 if (fInstalledSelectCommandWordwiseShortcuts) { 5083 Window()->RemoveShortcut(B_LEFT_ARROW, B_COMMAND_KEY | B_SHIFT_KEY); 5084 Window()->RemoveShortcut(B_RIGHT_ARROW, 5085 B_COMMAND_KEY | B_SHIFT_KEY); 5086 fInstalledSelectCommandWordwiseShortcuts = false; 5087 } 5088 5089 if (fInstalledNavigateOptionWordwiseShortcuts) { 5090 Window()->RemoveShortcut(B_LEFT_ARROW, B_OPTION_KEY); 5091 Window()->RemoveShortcut(B_RIGHT_ARROW, B_OPTION_KEY); 5092 fInstalledNavigateOptionWordwiseShortcuts = false; 5093 } 5094 if (fInstalledSelectOptionWordwiseShortcuts) { 5095 Window()->RemoveShortcut(B_LEFT_ARROW, B_OPTION_KEY | B_SHIFT_KEY); 5096 Window()->RemoveShortcut(B_RIGHT_ARROW, B_OPTION_KEY | B_SHIFT_KEY); 5097 fInstalledSelectOptionWordwiseShortcuts = false; 5098 } 5099 5100 if (fInstalledNavigateOptionLinewiseShortcuts) { 5101 Window()->RemoveShortcut(B_UP_ARROW, B_OPTION_KEY); 5102 Window()->RemoveShortcut(B_DOWN_ARROW, B_OPTION_KEY); 5103 fInstalledNavigateOptionLinewiseShortcuts = false; 5104 } 5105 if (fInstalledSelectOptionLinewiseShortcuts) { 5106 Window()->RemoveShortcut(B_UP_ARROW, B_OPTION_KEY | B_SHIFT_KEY); 5107 Window()->RemoveShortcut(B_DOWN_ARROW, B_OPTION_KEY | B_SHIFT_KEY); 5108 fInstalledSelectOptionLinewiseShortcuts = false; 5109 } 5110 5111 if (fInstalledNavigateHomeEndDocwiseShortcuts) { 5112 Window()->RemoveShortcut(B_HOME, B_COMMAND_KEY); 5113 Window()->RemoveShortcut(B_END, B_COMMAND_KEY); 5114 fInstalledNavigateHomeEndDocwiseShortcuts = false; 5115 } 5116 if (fInstalledSelectHomeEndDocwiseShortcuts) { 5117 Window()->RemoveShortcut(B_HOME, B_COMMAND_KEY | B_SHIFT_KEY); 5118 Window()->RemoveShortcut(B_END, B_COMMAND_KEY | B_SHIFT_KEY); 5119 fInstalledSelectHomeEndDocwiseShortcuts = false; 5120 } 5121 } 5122 } 5123 5124 5125 /*! Changes the passed in font to be displayable by the object. 5126 5127 Set font rotation to 0, removes any font flag, set font spacing 5128 to \c B_BITMAP_SPACING and font encoding to \c B_UNICODE_UTF8. 5129 */ 5130 void 5131 BTextView::_NormalizeFont(BFont* font) 5132 { 5133 if (font) { 5134 font->SetRotation(0.0f); 5135 font->SetFlags(0); 5136 font->SetSpacing(B_BITMAP_SPACING); 5137 font->SetEncoding(B_UNICODE_UTF8); 5138 } 5139 } 5140 5141 5142 void 5143 BTextView::_SetRunArray(int32 startOffset, int32 endOffset, 5144 const text_run_array* runs) 5145 { 5146 const int32 numStyles = runs->count; 5147 if (numStyles > 0) { 5148 const text_run* theRun = &runs->runs[0]; 5149 for (int32 index = 0; index < numStyles; index++) { 5150 int32 fromOffset = theRun->offset + startOffset; 5151 int32 toOffset = endOffset; 5152 if (index + 1 < numStyles) { 5153 toOffset = (theRun + 1)->offset + startOffset; 5154 toOffset = (toOffset > endOffset) ? endOffset : toOffset; 5155 } 5156 5157 _ApplyStyleRange(fromOffset, toOffset, B_FONT_ALL, &theRun->font, 5158 &theRun->color, false); 5159 5160 theRun++; 5161 } 5162 fStyles->InvalidateNullStyle(); 5163 } 5164 } 5165 5166 5167 /*! Returns the character class of the character at the given offset. 5168 5169 \param offset The offset where the wanted character can be found. 5170 5171 \return A value which represents the character's classification. 5172 */ 5173 uint32 5174 BTextView::_CharClassification(int32 offset) const 5175 { 5176 // TODO: Should check against a list of characters containing also 5177 // japanese word breakers. 5178 // And what about other languages ? Isn't there a better way to check 5179 // for separator characters ? 5180 // Andrew suggested to have a look at UnicodeBlockObject.h 5181 switch (fText->RealCharAt(offset)) { 5182 case '\0': 5183 return CHAR_CLASS_END_OF_TEXT; 5184 5185 case B_SPACE: 5186 case B_TAB: 5187 case B_ENTER: 5188 return CHAR_CLASS_WHITESPACE; 5189 5190 case '=': 5191 case '+': 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 return CHAR_CLASS_GRAPHICAL; 5206 5207 case '\'': 5208 case '"': 5209 return CHAR_CLASS_QUOTE; 5210 5211 case ',': 5212 case '.': 5213 case '?': 5214 case '!': 5215 case ';': 5216 case ':': 5217 case '-': 5218 return CHAR_CLASS_PUNCTUATION; 5219 5220 case '(': 5221 case '[': 5222 case '{': 5223 return CHAR_CLASS_PARENS_OPEN; 5224 5225 case ')': 5226 case ']': 5227 case '}': 5228 return CHAR_CLASS_PARENS_CLOSE; 5229 5230 default: 5231 return CHAR_CLASS_DEFAULT; 5232 } 5233 } 5234 5235 5236 /*! Returns the offset of the next UTF-8 character. 5237 5238 \param offset The offset where to start looking. 5239 5240 \return The offset of the next UTF-8 character. 5241 */ 5242 int32 5243 BTextView::_NextInitialByte(int32 offset) const 5244 { 5245 if (offset >= fText->Length()) 5246 return offset; 5247 5248 for (++offset; (ByteAt(offset) & 0xC0) == 0x80; ++offset) 5249 ; 5250 5251 return offset; 5252 } 5253 5254 5255 /*! Returns the offset of the previous UTF-8 character. 5256 5257 \param offset The offset where to start looking. 5258 5259 \return The offset of the previous UTF-8 character. 5260 */ 5261 int32 5262 BTextView::_PreviousInitialByte(int32 offset) const 5263 { 5264 if (offset <= 0) 5265 return 0; 5266 5267 int32 count = 6; 5268 5269 for (--offset; offset > 0 && count; --offset, --count) { 5270 if ((ByteAt(offset) & 0xC0) != 0x80) 5271 break; 5272 } 5273 5274 return count ? offset : 0; 5275 } 5276 5277 5278 bool 5279 BTextView::_GetProperty(BMessage* specifier, int32 form, const char* property, 5280 BMessage* reply) 5281 { 5282 CALLED(); 5283 if (strcmp(property, "selection") == 0) { 5284 reply->what = B_REPLY; 5285 reply->AddInt32("result", fSelStart); 5286 reply->AddInt32("result", fSelEnd); 5287 reply->AddInt32("error", B_OK); 5288 5289 return true; 5290 } else if (strcmp(property, "Text") == 0) { 5291 if (IsTypingHidden()) { 5292 // Do not allow stealing passwords via scripting 5293 beep(); 5294 return false; 5295 } 5296 5297 int32 index, range; 5298 specifier->FindInt32("index", &index); 5299 specifier->FindInt32("range", &range); 5300 5301 char* buffer = new char[range + 1]; 5302 GetText(index, range, buffer); 5303 5304 reply->what = B_REPLY; 5305 reply->AddString("result", buffer); 5306 reply->AddInt32("error", B_OK); 5307 5308 delete[] buffer; 5309 5310 return true; 5311 } else if (strcmp(property, "text_run_array") == 0) 5312 return false; 5313 5314 return false; 5315 } 5316 5317 5318 bool 5319 BTextView::_SetProperty(BMessage* specifier, int32 form, const char* property, 5320 BMessage* reply) 5321 { 5322 CALLED(); 5323 if (strcmp(property, "selection") == 0) { 5324 int32 index, range; 5325 5326 specifier->FindInt32("index", &index); 5327 specifier->FindInt32("range", &range); 5328 5329 Select(index, index + range); 5330 5331 reply->what = B_REPLY; 5332 reply->AddInt32("error", B_OK); 5333 5334 return true; 5335 } else if (strcmp(property, "Text") == 0) { 5336 int32 index, range; 5337 specifier->FindInt32("index", &index); 5338 specifier->FindInt32("range", &range); 5339 5340 const char* buffer = NULL; 5341 if (specifier->FindString("data", &buffer) == B_OK) 5342 InsertText(buffer, range, index, NULL); 5343 else 5344 DeleteText(index, range); 5345 5346 reply->what = B_REPLY; 5347 reply->AddInt32("error", B_OK); 5348 5349 return true; 5350 } else if (strcmp(property, "text_run_array") == 0) 5351 return false; 5352 5353 return false; 5354 } 5355 5356 5357 bool 5358 BTextView::_CountProperties(BMessage* specifier, int32 form, 5359 const char* property, BMessage* reply) 5360 { 5361 CALLED(); 5362 if (strcmp(property, "Text") == 0) { 5363 reply->what = B_REPLY; 5364 reply->AddInt32("result", TextLength()); 5365 reply->AddInt32("error", B_OK); 5366 return true; 5367 } 5368 5369 return false; 5370 } 5371 5372 5373 //! Called when the object receives a \c B_INPUT_METHOD_CHANGED message. 5374 void 5375 BTextView::_HandleInputMethodChanged(BMessage* message) 5376 { 5377 // TODO: block input if not editable (Andrew) 5378 ASSERT(fInline != NULL); 5379 5380 const char* string = NULL; 5381 if (message->FindString("be:string", &string) < B_OK || string == NULL) 5382 return; 5383 5384 _HideCaret(); 5385 5386 if (IsFocus()) 5387 be_app->ObscureCursor(); 5388 5389 // If we find the "be:confirmed" boolean (and the boolean is true), 5390 // it means it's over for now, so the current InlineInput object 5391 // should become inactive. We will probably receive a 5392 // B_INPUT_METHOD_STOPPED message after this one. 5393 bool confirmed; 5394 if (message->FindBool("be:confirmed", &confirmed) != B_OK) 5395 confirmed = false; 5396 5397 // Delete the previously inserted text (if any) 5398 if (fInline->IsActive()) { 5399 const int32 oldOffset = fInline->Offset(); 5400 DeleteText(oldOffset, oldOffset + fInline->Length()); 5401 if (confirmed) 5402 fInline->SetActive(false); 5403 fCaretOffset = fSelStart = fSelEnd = oldOffset; 5404 } 5405 5406 const int32 stringLen = strlen(string); 5407 5408 fInline->SetOffset(fSelStart); 5409 fInline->SetLength(stringLen); 5410 fInline->ResetClauses(); 5411 5412 if (!confirmed && !fInline->IsActive()) 5413 fInline->SetActive(true); 5414 5415 // Get the clauses, and pass them to the InlineInput object 5416 // TODO: Find out if what we did it's ok, currently we don't consider 5417 // clauses at all, while the bebook says we should; though the visual 5418 // effect we obtained seems correct. Weird. 5419 int32 clauseCount = 0; 5420 int32 clauseStart; 5421 int32 clauseEnd; 5422 while (message->FindInt32("be:clause_start", clauseCount, &clauseStart) 5423 == B_OK 5424 && message->FindInt32("be:clause_end", clauseCount, &clauseEnd) 5425 == B_OK) { 5426 if (!fInline->AddClause(clauseStart, clauseEnd)) 5427 break; 5428 clauseCount++; 5429 } 5430 5431 if (confirmed) { 5432 _Refresh(fSelStart, fSelEnd, true); 5433 _ShowCaret(); 5434 5435 // now we need to feed ourselves the individual characters as if the 5436 // user would have pressed them now - this lets KeyDown() pick out all 5437 // the special characters like B_BACKSPACE, cursor keys and the like: 5438 const char* currPos = string; 5439 const char* prevPos = currPos; 5440 while (*currPos != '\0') { 5441 if ((*currPos & 0xC0) == 0xC0) { 5442 // found the start of an UTF-8 char, we collect while it lasts 5443 ++currPos; 5444 while ((*currPos & 0xC0) == 0x80) 5445 ++currPos; 5446 } else if ((*currPos & 0xC0) == 0x80) { 5447 // illegal: character starts with utf-8 intermediate byte, 5448 // skip it 5449 prevPos = ++currPos; 5450 } else { 5451 // single byte character/code, just feed that 5452 ++currPos; 5453 } 5454 KeyDown(prevPos, currPos - prevPos); 5455 prevPos = currPos; 5456 } 5457 5458 _Refresh(fSelStart, fSelEnd, true); 5459 } else { 5460 // temporarily show transient state of inline input 5461 int32 selectionStart = 0; 5462 int32 selectionEnd = 0; 5463 message->FindInt32("be:selection", 0, &selectionStart); 5464 message->FindInt32("be:selection", 1, &selectionEnd); 5465 5466 fInline->SetSelectionOffset(selectionStart); 5467 fInline->SetSelectionLength(selectionEnd - selectionStart); 5468 5469 const int32 inlineOffset = fInline->Offset(); 5470 InsertText(string, stringLen, fSelStart, NULL); 5471 5472 _Refresh(inlineOffset, fSelEnd, true); 5473 _ShowCaret(); 5474 } 5475 5476 } 5477 5478 5479 /*! Called when the object receives a \c B_INPUT_METHOD_LOCATION_REQUEST 5480 message. 5481 */ 5482 void 5483 BTextView::_HandleInputMethodLocationRequest() 5484 { 5485 ASSERT(fInline != NULL); 5486 5487 int32 offset = fInline->Offset(); 5488 const int32 limit = offset + fInline->Length(); 5489 5490 BMessage message(B_INPUT_METHOD_EVENT); 5491 message.AddInt32("be:opcode", B_INPUT_METHOD_LOCATION_REQUEST); 5492 5493 // Add the location of the UTF8 characters 5494 while (offset < limit) { 5495 float height; 5496 BPoint where = PointAt(offset, &height); 5497 ConvertToScreen(&where); 5498 5499 message.AddPoint("be:location_reply", where); 5500 message.AddFloat("be:height_reply", height); 5501 5502 offset = _NextInitialByte(offset); 5503 } 5504 5505 fInline->Method()->SendMessage(&message); 5506 } 5507 5508 5509 //! Tells the Input Server method add-on to stop the current transaction. 5510 void 5511 BTextView::_CancelInputMethod() 5512 { 5513 if (!fInline) 5514 return; 5515 5516 InlineInput* inlineInput = fInline; 5517 fInline = NULL; 5518 5519 if (inlineInput->IsActive() && Window()) { 5520 _Refresh(inlineInput->Offset(), fText->Length() - inlineInput->Offset(), 5521 false); 5522 5523 BMessage message(B_INPUT_METHOD_EVENT); 5524 message.AddInt32("be:opcode", B_INPUT_METHOD_STOPPED); 5525 inlineInput->Method()->SendMessage(&message); 5526 } 5527 5528 delete inlineInput; 5529 } 5530 5531 5532 /*! Returns the line number of the character at the given \a offset. 5533 5534 \note This will never return the last line (use LineAt() if you 5535 need to be correct about that.) N.B. 5536 5537 \param offset The offset of the wanted character. 5538 5539 \return The line number of the character at the given \a offset as an int32. 5540 */ 5541 int32 5542 BTextView::_LineAt(int32 offset) const 5543 { 5544 return fLines->OffsetToLine(offset); 5545 } 5546 5547 5548 /*! Returns the line number that the given \a point is on. 5549 5550 \note This will never return the last line (use LineAt() if you 5551 need to be correct about that.) N.B. 5552 5553 \param point The \a point the get the line number of. 5554 5555 \return The line number of the given \a point as an int32. 5556 */ 5557 int32 5558 BTextView::_LineAt(const BPoint& point) const 5559 { 5560 return fLines->PixelToLine(point.y - fTextRect.top); 5561 } 5562 5563 5564 /*! Returns whether or not the given \a offset is on the empty line at the end 5565 of the buffer. 5566 */ 5567 bool 5568 BTextView::_IsOnEmptyLastLine(int32 offset) const 5569 { 5570 return (offset == TextLength() && offset > 0 5571 && fText->RealCharAt(offset - 1) == B_ENTER); 5572 } 5573 5574 5575 void 5576 BTextView::_ApplyStyleRange(int32 fromOffset, int32 toOffset, uint32 mode, 5577 const BFont* font, const rgb_color* color, bool syncNullStyle) 5578 { 5579 if (font != NULL) { 5580 // if a font has been given, normalize it 5581 BFont normalized = *font; 5582 _NormalizeFont(&normalized); 5583 font = &normalized; 5584 } 5585 5586 if (!fStylable) { 5587 // always apply font and color to full range for non-stylable textviews 5588 fromOffset = 0; 5589 toOffset = fText->Length(); 5590 } 5591 5592 if (syncNullStyle) 5593 fStyles->SyncNullStyle(fromOffset); 5594 5595 fStyles->SetStyleRange(fromOffset, toOffset, fText->Length(), mode, 5596 font, color); 5597 } 5598 5599 5600 float 5601 BTextView::_NullStyleHeight() const 5602 { 5603 const BFont* font = NULL; 5604 fStyles->GetNullStyle(&font, NULL); 5605 5606 font_height fontHeight; 5607 font->GetHeight(&fontHeight); 5608 return ceilf(fontHeight.ascent + fontHeight.descent + 1); 5609 } 5610 5611 5612 void 5613 BTextView::_ShowContextMenu(BPoint where) 5614 { 5615 bool isRedo; 5616 undo_state state = UndoState(&isRedo); 5617 bool isUndo = state != B_UNDO_UNAVAILABLE && !isRedo; 5618 5619 int32 start; 5620 int32 finish; 5621 GetSelection(&start, &finish); 5622 5623 bool canEdit = IsEditable(); 5624 int32 length = TextLength(); 5625 5626 BPopUpMenu* menu = new BPopUpMenu(B_EMPTY_STRING, false, false); 5627 5628 BLayoutBuilder::Menu<>(menu) 5629 .AddItem(TRANSLATE("Undo"), B_UNDO/*, 'Z'*/) 5630 .SetEnabled(canEdit && isUndo) 5631 .AddItem(TRANSLATE("Redo"), B_UNDO/*, 'Z', B_SHIFT_KEY*/) 5632 .SetEnabled(canEdit && isRedo) 5633 .AddSeparator() 5634 .AddItem(TRANSLATE("Cut"), B_CUT, 'X') 5635 .SetEnabled(canEdit && start != finish) 5636 .AddItem(TRANSLATE("Copy"), B_COPY, 'C') 5637 .SetEnabled(start != finish) 5638 .AddItem(TRANSLATE("Paste"), B_PASTE, 'V') 5639 .SetEnabled(canEdit && be_clipboard->SystemCount() > 0) 5640 .AddSeparator() 5641 .AddItem(TRANSLATE("Select All"), B_SELECT_ALL, 'A') 5642 .SetEnabled(!(start == 0 && finish == length)) 5643 ; 5644 5645 menu->SetTargetForItems(this); 5646 ConvertToScreen(&where); 5647 menu->Go(where, true, true, true); 5648 } 5649 5650 5651 void 5652 BTextView::_FilterDisallowedChars(char* text, ssize_t& length, 5653 text_run_array* runArray) 5654 { 5655 if (!fDisallowedChars) 5656 return; 5657 5658 if (fDisallowedChars->IsEmpty() || !text) 5659 return; 5660 5661 ssize_t stringIndex = 0; 5662 if (runArray) { 5663 ssize_t remNext = 0; 5664 5665 for (int i = 0; i < runArray->count; i++) { 5666 runArray->runs[i].offset -= remNext; 5667 while (stringIndex < runArray->runs[i].offset 5668 && stringIndex < length) { 5669 if (fDisallowedChars->HasItem( 5670 reinterpret_cast<void*>(text[stringIndex]))) { 5671 memmove(text + stringIndex, text + stringIndex + 1, 5672 length - stringIndex - 1); 5673 length--; 5674 runArray->runs[i].offset--; 5675 remNext++; 5676 } else 5677 stringIndex++; 5678 } 5679 } 5680 } 5681 5682 while (stringIndex < length) { 5683 if (fDisallowedChars->HasItem( 5684 reinterpret_cast<void*>(text[stringIndex]))) { 5685 memmove(text + stringIndex, text + stringIndex + 1, 5686 length - stringIndex - 1); 5687 length--; 5688 } else 5689 stringIndex++; 5690 } 5691 } 5692 5693 5694 // #pragma mark - BTextView::TextTrackState 5695 5696 5697 BTextView::TextTrackState::TextTrackState(BMessenger messenger) 5698 : 5699 clickOffset(0), 5700 shiftDown(false), 5701 anchor(0), 5702 selStart(0), 5703 selEnd(0), 5704 fRunner(NULL) 5705 { 5706 BMessage message(_PING_); 5707 const bigtime_t scrollSpeed = 25 * 1000; // 40 scroll steps per second 5708 fRunner = new (nothrow) BMessageRunner(messenger, &message, scrollSpeed); 5709 } 5710 5711 5712 BTextView::TextTrackState::~TextTrackState() 5713 { 5714 delete fRunner; 5715 } 5716 5717 5718 void 5719 BTextView::TextTrackState::SimulateMouseMovement(BTextView* textView) 5720 { 5721 BPoint where; 5722 uint32 buttons; 5723 // When the mouse cursor is still and outside the textview, 5724 // no B_MOUSE_MOVED message are sent, obviously. But scrolling 5725 // has to work neverthless, so we "fake" a MouseMoved() call here. 5726 textView->GetMouse(&where, &buttons); 5727 textView->_PerformMouseMoved(where, B_INSIDE_VIEW); 5728 } 5729 5730 5731 // #pragma mark - Binary ABI compat 5732 5733 5734 extern "C" void 5735 B_IF_GCC_2(InvalidateLayout__9BTextViewb, _ZN9BTextView16InvalidateLayoutEb)( 5736 BTextView* view, bool descendants) 5737 { 5738 perform_data_layout_invalidated data; 5739 data.descendants = descendants; 5740 5741 view->Perform(PERFORM_CODE_LAYOUT_INVALIDATED, &data); 5742 } 5743