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