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