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