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