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