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