1 /* 2 * Copyright 2001-2007, Haiku Inc. 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 (burton666@libero.it) 9 */ 10 11 /** BTextView displays and manages styled text. */ 12 13 // TODOs: 14 // - Finish documenting this class 15 // - Consider using BObjectList instead of BList 16 // for disallowed characters (it would remove a lot of reinterpret_casts) 17 // - Check for correctness and possible optimizations the calls to Refresh(), 18 // to refresh only changed parts of text (currently we often redraw the whole text) 19 20 // Known Bugs: 21 // - Double buffering doesn't work well (disabled by default) 22 23 #include <stdio.h> 24 #include <stdlib.h> 25 #include <new> 26 using namespace std; 27 28 #include <Application.h> 29 #include <Beep.h> 30 #include <Bitmap.h> 31 #include <Clipboard.h> 32 #include <Debug.h> 33 #include <Input.h> 34 #include <MessageRunner.h> 35 #include <PropertyInfo.h> 36 #include <Region.h> 37 #include <ScrollBar.h> 38 #include <TextView.h> 39 #include <Window.h> 40 41 #include "InlineInput.h" 42 #include "LineBuffer.h" 43 #include "StyleBuffer.h" 44 #include "TextGapBuffer.h" 45 #include "UndoBuffer.h" 46 #include "WidthBuffer.h" 47 48 49 //#define TRACE_TEXTVIEW 50 #ifdef TRACE_TEXTVIEW 51 #define CALLED() printf("%s\n", __PRETTY_FUNCTION__) 52 #else 53 #define CALLED() 54 #endif 55 56 57 #define USE_WIDTHBUFFER 0 58 #define USE_DOUBLEBUFFERING 0 59 60 61 struct flattened_text_run { 62 int32 offset; 63 font_family family; 64 font_style style; 65 float size; 66 float shear; /* typically 90.0 */ 67 uint16 face; /* typically 0 */ 68 uint8 red; 69 uint8 green; 70 uint8 blue; 71 uint8 alpha; /* 255 == opaque */ 72 uint16 _reserved_; /* 0 */ 73 }; 74 75 struct flattened_text_run_array { 76 uint32 magic; 77 uint32 version; 78 int32 count; 79 flattened_text_run styles[1]; 80 }; 81 82 static const uint32 kFlattenedTextRunArrayMagic = 'Ali!'; 83 static const uint32 kFlattenedTextRunArrayVersion = 0; 84 85 enum { 86 B_SEPARATOR_CHARACTER, 87 B_OTHER_CHARACTER 88 }; 89 90 91 class _BTextTrackState_ { 92 public: 93 _BTextTrackState_(BMessenger messenger) 94 : 95 clickOffset(0), 96 shiftDown(false), 97 anchor(0), 98 selStart(0), 99 selEnd(0), 100 fRunner(NULL) 101 { 102 BMessage message(_PING_); 103 fRunner = new (nothrow) BMessageRunner(messenger, &message, 300000); 104 } 105 106 ~_BTextTrackState_() 107 { 108 delete fRunner; 109 } 110 111 int32 clickOffset; 112 bool shiftDown; 113 BRect selectionRect; 114 115 int32 anchor; 116 int32 selStart; 117 int32 selEnd; 118 119 private: 120 BMessageRunner *fRunner; 121 }; 122 123 124 // Initialized/finalized by init/fini_interface_kit 125 _BWidthBuffer_* BTextView::sWidths = NULL; 126 sem_id BTextView::sWidthSem = B_BAD_SEM_ID; 127 int32 BTextView::sWidthAtom = 0; 128 129 130 const static rgb_color kBlackColor = { 0, 0, 0, 255 }; 131 const static rgb_color kBlueInputColor = { 152, 203, 255, 255 }; 132 const static rgb_color kRedInputColor = { 255, 152, 152, 255 }; 133 134 135 static property_info 136 sPropertyList[] = { 137 { 138 "selection", 139 { B_GET_PROPERTY, 0 }, 140 { B_DIRECT_SPECIFIER, 0 }, 141 "Returns the current selection.", 0, 142 { B_INT32_TYPE, 0 } 143 }, 144 { 145 "selection", 146 { B_SET_PROPERTY, 0 }, 147 { B_DIRECT_SPECIFIER, 0 }, 148 "Sets the current selection.", 0, 149 { B_INT32_TYPE, 0 } 150 }, 151 { 152 "Text", 153 { B_COUNT_PROPERTIES, 0 }, 154 { B_DIRECT_SPECIFIER, 0 }, 155 "Returns the length of the text in bytes.", 0, 156 { B_INT32_TYPE, 0 } 157 }, 158 { 159 "Text", 160 { B_GET_PROPERTY, 0 }, 161 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 }, 162 "Returns the text in the specified range in the BTextView.", 0, 163 { B_STRING_TYPE, 0 } 164 }, 165 { 166 "Text", 167 { B_SET_PROPERTY, 0 }, 168 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 }, 169 "Removes or inserts text into the specified range in the BTextView.", 0, 170 { B_STRING_TYPE, 0 } 171 }, 172 { 173 "text_run_array", 174 { B_GET_PROPERTY, 0 }, 175 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 }, 176 "Returns the style information for the text in the specified range in the BTextView.", 0, 177 { B_RAW_TYPE, 0 }, 178 }, 179 { 180 "text_run_array", 181 { B_SET_PROPERTY, 0 }, 182 { B_RANGE_SPECIFIER, B_REVERSE_RANGE_SPECIFIER, 0 }, 183 "Sets the style information for the text in the specified range in the BTextView.", 0, 184 { B_RAW_TYPE, 0 }, 185 }, 186 { 0 } 187 }; 188 189 190 /*! \brief Creates a BTextView object with the given attributes. 191 \param frame The rect which will enclose the BTextView object. 192 \param name The name of the object. 193 \param textRect Determines the area of the text within the BTextView object. 194 \param resizeMask The resizing mask for the BTextView, passed to the BView constructor. 195 \param flags The flags for the BTextView, passed to the BView constructor. 196 */ 197 BTextView::BTextView(BRect frame, const char *name, BRect textRect, 198 uint32 resizeMask, uint32 flags) 199 : BView(frame, name, resizeMask, 200 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE) 201 { 202 InitObject(textRect, NULL, NULL); 203 } 204 205 206 /*! \brief Creates a BTextView object with the given attributes. 207 \param frame The rect which will enclose the BTextView object. 208 \param name The name of the object. 209 \param textRect Determines the area of the text within the BTextView object. 210 \param initialFont The BTextView will display its text using this font, unless otherwise specified. 211 \param initialColor The BTextView will display its text using this color, unless otherwise specified. 212 \param resizeMask The resizing mask for the BTextView, passed to the BView constructor. 213 \param flags The flags for the BTextView, passed to the BView constructor. 214 */ 215 BTextView::BTextView(BRect frame, const char *name, BRect textRect, 216 const BFont *initialFont, const rgb_color *initialColor, 217 uint32 resizeMask, uint32 flags) 218 : BView(frame, name, resizeMask, 219 flags | B_FRAME_EVENTS | B_PULSE_NEEDED | B_INPUT_METHOD_AWARE) 220 { 221 InitObject(textRect, initialFont, initialColor); 222 } 223 224 225 /*! \brief Creates a BTextView object from the passed BMessage. 226 \param archive The BMessage from which the object shall be created. 227 */ 228 BTextView::BTextView(BMessage *archive) 229 : BView(archive) 230 { 231 CALLED(); 232 BRect rect; 233 234 if (archive->FindRect("_trect", &rect) != B_OK) 235 rect.Set(0, 0, 0, 0); 236 237 InitObject(rect, NULL, NULL); 238 239 const char *text = NULL; 240 if (archive->FindString("_text", &text) == B_OK) 241 SetText(text); 242 243 int32 flag, flag2; 244 if (archive->FindInt32("_align", &flag) == B_OK) 245 SetAlignment((alignment)flag); 246 247 float value; 248 249 if (archive->FindFloat("_tab", &value) == B_OK) 250 SetTabWidth(value); 251 252 if (archive->FindInt32("_col_sp", &flag) == B_OK) 253 SetColorSpace((color_space)flag); 254 255 if (archive->FindInt32("_max", &flag) == B_OK) 256 SetMaxBytes(flag); 257 258 if (archive->FindInt32("_sel", &flag) == B_OK && 259 archive->FindInt32("_sel", &flag2) == B_OK) 260 Select(flag, flag2); 261 262 bool toggle; 263 264 if (archive->FindBool("_stylable", &toggle) == B_OK) 265 SetStylable(toggle); 266 267 if (archive->FindBool("_auto_in", &toggle) == B_OK) 268 SetAutoindent(toggle); 269 270 if (archive->FindBool("_wrap", &toggle) == B_OK) 271 SetWordWrap(toggle); 272 273 if (archive->FindBool("_nsel", &toggle) == B_OK) 274 MakeSelectable(!toggle); 275 276 if (archive->FindBool("_nedit", &toggle) == B_OK) 277 MakeEditable(!toggle); 278 279 ssize_t disallowedCount = 0; 280 const int32 *disallowedChars = NULL; 281 if (archive->FindData("_dis_ch", B_RAW_TYPE, 282 (const void **)&disallowedChars, &disallowedCount) == B_OK) { 283 284 fDisallowedChars = new BList; 285 disallowedCount /= sizeof(int32); 286 for (int32 x = 0; x < disallowedCount; x++) 287 fDisallowedChars->AddItem(reinterpret_cast<void *>(disallowedChars[x])); 288 } 289 290 ssize_t runSize = 0; 291 const void *flattenedRun = NULL; 292 293 if (archive->FindData("_runs", B_RAW_TYPE, &flattenedRun, &runSize) == B_OK) { 294 text_run_array *runArray = UnflattenRunArray(flattenedRun, (int32 *)&runSize); 295 if (runArray) { 296 SetRunArray(0, TextLength(), runArray); 297 FreeRunArray(runArray); 298 } 299 } 300 301 } 302 303 304 /*! \brief Frees the memory allocated and destroy the object created on construction. 305 */ 306 BTextView::~BTextView() 307 { 308 CancelInputMethod(); 309 StopMouseTracking(); 310 DeleteOffscreen(); 311 312 delete fText; 313 delete fLines; 314 delete fStyles; 315 delete fDisallowedChars; 316 delete fUndo; 317 delete fClickRunner; 318 delete fDragRunner; 319 } 320 321 322 /*! \brief Static function used to istantiate a BTextView object from the given BMessage. 323 \param archive The BMessage from which the object shall be created. 324 \return A constructed BTextView as a BArchivable object. 325 */ 326 BArchivable * 327 BTextView::Instantiate(BMessage *archive) 328 { 329 CALLED(); 330 if (validate_instantiation(archive, "BTextView")) 331 return new BTextView(archive); 332 return NULL; 333 } 334 335 336 /*! \brief Archives the object into the passed message. 337 \param data A pointer to the message where to archive the object. 338 \param deep ? 339 \return \c B_OK if everything went well, an error code if not. 340 */ 341 status_t 342 BTextView::Archive(BMessage *data, bool deep) const 343 { 344 CALLED(); 345 status_t err = BView::Archive(data, deep); 346 if (err == B_OK) 347 err = data->AddString("_text", Text()); 348 if (err == B_OK) 349 err = data->AddInt32("_align", fAlignment); 350 if (err == B_OK) 351 err = data->AddFloat("_tab", fTabWidth); 352 if (err == B_OK) 353 err = data->AddInt32("_col_sp", fColorSpace); 354 if (err == B_OK) 355 err = data->AddRect("_trect", fTextRect); 356 if (err == B_OK) 357 err = data->AddInt32("_max", fMaxBytes); 358 if (err == B_OK) 359 err = data->AddInt32("_sel", fSelStart); 360 if (err == B_OK) 361 err = data->AddInt32("_sel", fSelEnd); 362 if (err == B_OK) 363 err = data->AddBool("_stylable", fStylable); 364 if (err == B_OK) 365 err = data->AddBool("_auto_in", fAutoindent); 366 if (err == B_OK) 367 err = data->AddBool("_wrap", fWrap); 368 if (err == B_OK) 369 err = data->AddBool("_nsel", !fSelectable); 370 if (err == B_OK) 371 err = data->AddBool("_nedit", !fEditable); 372 373 if (err == B_OK && fDisallowedChars != NULL) 374 err = data->AddData("_dis_ch", B_RAW_TYPE, fDisallowedChars->Items(), 375 fDisallowedChars->CountItems() * sizeof(int32)); 376 377 if (err == B_OK) { 378 int32 runSize = 0; 379 text_run_array *runArray = RunArray(0, TextLength()); 380 381 void *flattened = FlattenRunArray(runArray, &runSize); 382 if (flattened != NULL) { 383 data->AddData("_runs", B_RAW_TYPE, flattened, runSize); 384 free(flattened); 385 } else 386 err = B_NO_MEMORY; 387 388 FreeRunArray(runArray); 389 } 390 391 return err; 392 } 393 394 395 /*! \brief Hook function called when the BTextView is added to the 396 window's view hierarchy. 397 398 Set the window's pulse rate to 2 per second and adjust scrollbars if needed 399 */ 400 void 401 BTextView::AttachedToWindow() 402 { 403 BView::AttachedToWindow(); 404 405 SetDrawingMode(B_OP_COPY); 406 407 Window()->SetPulseRate(500000); 408 409 fCaretVisible = false; 410 fCaretTime = 0; 411 fClickCount = 0; 412 fClickTime = 0; 413 fDragOffset = -1; 414 fActive = false; 415 416 if (fResizable) 417 AutoResize(true); 418 419 UpdateScrollbars(); 420 421 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); 422 } 423 424 425 /*! \brief Hook function called when the BTextView is removed from the 426 window's view hierarchy. 427 */ 428 void 429 BTextView::DetachedFromWindow() 430 { 431 BView::DetachedFromWindow(); 432 } 433 434 435 /*! \brief Hook function called whenever 436 the contents of the BTextView need to be (re)drawn. 437 \param updateRect The rect which needs to be redrawn 438 */ 439 void 440 BTextView::Draw(BRect updateRect) 441 { 442 // what lines need to be drawn? 443 int32 startLine = LineAt(BPoint(0.0f, updateRect.top)); 444 int32 endLine = LineAt(BPoint(0.0f, updateRect.bottom)); 445 446 // TODO: DrawLines draw the text over and over, causing the text to 447 // antialias against itself. In theory we should use an offscreen bitmap 448 // to draw the text which would eliminate the problem. 449 //DrawLines(startLine, endLine); 450 Refresh(OffsetAt(startLine), OffsetAt(endLine + 1), false, false); 451 } 452 453 454 /*! \brief Hook function called when a mouse button is clicked while 455 the cursor is in the view. 456 \param where The location where the mouse button has been clicked. 457 */ 458 void 459 BTextView::MouseDown(BPoint where) 460 { 461 // should we even bother? 462 if (!fEditable && !fSelectable) 463 return; 464 465 CancelInputMethod(); 466 467 if (!IsFocus()) 468 MakeFocus(); 469 470 // hide the caret if it's visible 471 if (fCaretVisible) 472 InvertCaret(); 473 474 StopMouseTracking(); 475 476 BMessenger messenger(this); 477 fTrackingMouse = new (nothrow) _BTextTrackState_(messenger); 478 if (fTrackingMouse == NULL) 479 return; 480 481 int32 modifiers = 0; 482 //uint32 buttons; 483 BMessage *currentMessage = Window()->CurrentMessage(); 484 if (currentMessage != NULL) { 485 currentMessage->FindInt32("modifiers", &modifiers); 486 //currentMessage->FindInt32("buttons", (int32 *)&buttons); 487 } 488 489 fTrackingMouse->clickOffset = OffsetAt(where); 490 fTrackingMouse->shiftDown = modifiers & B_SHIFT_KEY; 491 492 bigtime_t clickTime = system_time(); 493 bigtime_t clickSpeed = 0; 494 get_click_speed(&clickSpeed); 495 bool multipleClick = false; 496 if (clickTime - fClickTime < clickSpeed && fClickOffset == fTrackingMouse->clickOffset) 497 multipleClick = true; 498 499 fWhere = where; 500 501 SetMouseEventMask(B_POINTER_EVENTS | B_KEYBOARD_EVENTS, B_LOCK_WINDOW_FOCUS | B_NO_POINTER_HISTORY); 502 503 if (fSelStart != fSelEnd && !fTrackingMouse->shiftDown && !multipleClick) { 504 BRegion region; 505 GetTextRegion(fSelStart, fSelEnd, ®ion); 506 if (region.Contains(where)) { 507 // Setup things for dragging 508 fTrackingMouse->selectionRect = region.Frame(); 509 return; 510 } 511 } 512 513 if (multipleClick) { 514 if (fClickCount > 1) { 515 fClickCount = 0; 516 fClickTime = 0; 517 } else { 518 fClickCount = 2; 519 fClickTime = clickTime; 520 } 521 } else { 522 fClickOffset = fTrackingMouse->clickOffset; 523 fClickCount = 1; 524 fClickTime = clickTime; 525 526 // Deselect any previously selected text 527 if (!fTrackingMouse->shiftDown) 528 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset); 529 } 530 531 if (fClickTime == clickTime) { 532 BMessage message(_PING_); 533 message.AddInt64("clickTime", clickTime); 534 delete fClickRunner; 535 536 BMessenger messenger(this); 537 fClickRunner = new (nothrow) BMessageRunner(messenger, &message, clickSpeed, 1); 538 } 539 540 541 if (!fSelectable) { 542 StopMouseTracking(); 543 return; 544 } 545 546 int32 offset = fSelStart; 547 if (fTrackingMouse->clickOffset > fSelStart) 548 offset = fSelEnd; 549 550 fTrackingMouse->anchor = offset; 551 552 MouseMoved(where, B_INSIDE_VIEW, NULL); 553 } 554 555 556 /*! \brief Hook function called when a mouse button is released while 557 the cursor is in the view. 558 \param where The point where the mouse button has been released. 559 560 Stops asynchronous mouse tracking 561 */ 562 void 563 BTextView::MouseUp(BPoint where) 564 { 565 BView::MouseUp(where); 566 PerformMouseUp(where); 567 568 delete fDragRunner; 569 fDragRunner = NULL; 570 } 571 572 573 /*! \brief Hook function called whenever the mouse cursor enters, exits 574 or moves inside the view. 575 \param where The point where the mouse cursor has moved to. 576 \param code A code which tells if the mouse entered or exited the view 577 \param message The message containing dragged information, if any. 578 */ 579 void 580 BTextView::MouseMoved(BPoint where, uint32 code, const BMessage *message) 581 { 582 // Check if it's a "click'n'move 583 if (PerformMouseMoved(where, code)) 584 return; 585 586 bool sync = false; 587 switch (code) { 588 // We force a sync when the mouse enters the view 589 case B_ENTERED_VIEW: 590 sync = true; 591 // supposed to fall through 592 case B_INSIDE_VIEW: 593 TrackMouse(where, message, sync); 594 break; 595 596 case B_EXITED_VIEW: 597 DragCaret(-1); 598 if (Window()->IsActive() && message == NULL) 599 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); 600 break; 601 602 default: 603 BView::MouseMoved(where, code, message); 604 break; 605 } 606 } 607 608 609 /*! \brief Hook function called when the window becomes the active window 610 or gives up that status. 611 \param state If true, window has just become active, if false, window has 612 just become inactive. 613 */ 614 void 615 BTextView::WindowActivated(bool state) 616 { 617 BView::WindowActivated(state); 618 619 if (state && IsFocus()) { 620 if (!fActive) 621 Activate(); 622 } else { 623 if (fActive) 624 Deactivate(); 625 } 626 627 BPoint where; 628 ulong buttons; 629 GetMouse(&where, &buttons, false); 630 631 if (Bounds().Contains(where)) 632 TrackMouse(where, NULL); 633 } 634 635 636 /*! \brief Hook function called whenever a key is pressed while the view is 637 the focus view of the active window. 638 */ 639 void 640 BTextView::KeyDown(const char *bytes, int32 numBytes) 641 { 642 const char keyPressed = bytes[0]; 643 644 if (!fEditable) { 645 // only arrow and page keys are allowed 646 // (no need to hide the cursor) 647 switch (keyPressed) { 648 case B_LEFT_ARROW: 649 case B_RIGHT_ARROW: 650 case B_UP_ARROW: 651 case B_DOWN_ARROW: 652 HandleArrowKey(keyPressed); 653 break; 654 655 case B_HOME: 656 case B_END: 657 case B_PAGE_UP: 658 case B_PAGE_DOWN: 659 HandlePageKey(keyPressed); 660 break; 661 662 default: 663 BView::KeyDown(bytes, numBytes); 664 break; 665 } 666 667 return; 668 } 669 670 // hide the cursor and caret 671 be_app->ObscureCursor(); 672 if (fCaretVisible) 673 InvertCaret(); 674 675 switch (keyPressed) { 676 case B_BACKSPACE: 677 HandleBackspace(); 678 break; 679 680 case B_LEFT_ARROW: 681 case B_RIGHT_ARROW: 682 case B_UP_ARROW: 683 case B_DOWN_ARROW: 684 HandleArrowKey(keyPressed); 685 break; 686 687 case B_DELETE: 688 HandleDelete(); 689 break; 690 691 case B_HOME: 692 case B_END: 693 case B_PAGE_UP: 694 case B_PAGE_DOWN: 695 HandlePageKey(keyPressed); 696 break; 697 698 case B_ESCAPE: 699 case B_INSERT: 700 case B_FUNCTION_KEY: 701 // ignore, pass it up to superclass 702 BView::KeyDown(bytes, numBytes); 703 break; 704 705 default: 706 // if the character is not allowed, bail out. 707 if (fDisallowedChars 708 && fDisallowedChars->HasItem(reinterpret_cast<void *>((uint32)keyPressed))) { 709 beep(); 710 return; 711 } 712 713 HandleAlphaKey(bytes, numBytes); 714 break; 715 } 716 717 // draw the caret 718 if (fSelStart == fSelEnd) { 719 if (!fCaretVisible) 720 InvertCaret(); 721 } 722 } 723 724 725 /*! \brief Hook function called every x microseconds. 726 It's the function which makes the caret blink. 727 */ 728 void 729 BTextView::Pulse() 730 { 731 if (fActive && fEditable && fSelStart == fSelEnd) { 732 if (system_time() > (fCaretTime + 500000.0)) 733 InvertCaret(); 734 } 735 } 736 737 738 /*! \brief Hook function called when the view's frame is resized. 739 \param width The new view's width. 740 \param height The new view's height. 741 742 Updates the associated scrollbars. 743 */ 744 void 745 BTextView::FrameResized(float width, float height) 746 { 747 BView::FrameResized(width, height); 748 UpdateScrollbars(); 749 } 750 751 752 /*! \brief Highlight/unhighlight the selection when the view gets 753 or looses the focus. 754 \param focusState The focus state: true, if the view is getting the focus, 755 false otherwise. 756 */ 757 void 758 BTextView::MakeFocus(bool focusState) 759 { 760 BView::MakeFocus(focusState); 761 762 if (focusState && Window() && Window()->IsActive()) { 763 if (!fActive) 764 Activate(); 765 } else { 766 if (fActive) 767 Deactivate(); 768 } 769 } 770 771 772 /*! \brief Hook function executed every time the BTextView gets a message. 773 \param message The received message 774 */ 775 void 776 BTextView::MessageReceived(BMessage *message) 777 { 778 // TODO: block input if not editable (Andrew) 779 780 // was this message dropped? 781 if (message->WasDropped()) { 782 BPoint dropOffset; 783 BPoint dropPoint = message->DropPoint(&dropOffset); 784 ConvertFromScreen(&dropPoint); 785 ConvertFromScreen(&dropOffset); 786 if (!MessageDropped(message, dropPoint, dropOffset)) 787 BView::MessageReceived(message); 788 789 return; 790 } 791 792 switch (message->what) { 793 case B_CUT: 794 Cut(be_clipboard); 795 break; 796 797 case B_COPY: 798 Copy(be_clipboard); 799 break; 800 801 case B_PASTE: 802 Paste(be_clipboard); 803 break; 804 805 case B_UNDO: 806 Undo(be_clipboard); 807 break; 808 809 case B_SELECT_ALL: 810 SelectAll(); 811 break; 812 813 814 case B_INPUT_METHOD_EVENT: 815 { 816 int32 opcode; 817 if (message->FindInt32("be:opcode", &opcode) == B_OK) { 818 switch (opcode) { 819 case B_INPUT_METHOD_STARTED: 820 { 821 BMessenger messenger; 822 if (message->FindMessenger("be:reply_to", &messenger) == B_OK) { 823 ASSERT(fInline == NULL); 824 fInline = new _BInlineInput_(messenger); 825 } 826 break; 827 } 828 829 case B_INPUT_METHOD_STOPPED: 830 delete fInline; 831 fInline = NULL; 832 break; 833 834 case B_INPUT_METHOD_CHANGED: 835 HandleInputMethodChanged(message); 836 break; 837 838 case B_INPUT_METHOD_LOCATION_REQUEST: 839 HandleInputMethodLocationRequest(); 840 break; 841 842 default: 843 break; 844 } 845 } 846 break; 847 } 848 849 case B_SET_PROPERTY: 850 case B_GET_PROPERTY: 851 case B_COUNT_PROPERTIES: 852 { 853 BPropertyInfo propInfo(sPropertyList); 854 BMessage specifier; 855 const char *property; 856 857 if (message->GetCurrentSpecifier(NULL, &specifier) < B_OK || 858 specifier.FindString("property", &property) < B_OK) 859 return; 860 861 if (propInfo.FindMatch(message, 0, &specifier, specifier.what, 862 property) < B_OK) { 863 BView::MessageReceived(message); 864 break; 865 } 866 867 switch(message->what) { 868 case B_GET_PROPERTY: 869 { 870 BMessage reply; 871 GetProperty(&specifier, specifier.what, property, &reply); 872 message->SendReply(&reply); 873 break; 874 } 875 876 case B_SET_PROPERTY: 877 { 878 BMessage reply; 879 SetProperty(&specifier, specifier.what, property, &reply); 880 message->SendReply(&reply); 881 break; 882 } 883 884 case B_COUNT_PROPERTIES: 885 { 886 BMessage reply; 887 CountProperties(&specifier, specifier.what, property, &reply); 888 message->SendReply(&reply); 889 break; 890 } 891 892 default: 893 break; 894 } 895 896 break; 897 } 898 899 case _PING_: 900 { 901 if (message->HasInt64("clickTime")) { 902 bigtime_t clickTime; 903 message->FindInt64("clickTime", &clickTime); 904 if (clickTime == fClickTime) { 905 if (fSelStart != fSelEnd && fSelectable) { 906 BRegion region; 907 GetTextRegion(fSelStart, fSelEnd, ®ion); 908 if (region.Contains(fWhere)) 909 TrackMouse(fWhere, NULL); 910 } 911 delete fClickRunner; 912 fClickRunner = NULL; 913 } 914 } else 915 PerformAutoScrolling(); 916 break; 917 } 918 919 case _DISPOSE_DRAG_: 920 if (fEditable) 921 TrackDrag(fWhere); 922 break; 923 924 default: 925 BView::MessageReceived(message); 926 break; 927 } 928 } 929 930 931 /*! \brief Returns the proper handler for the given scripting message. 932 \param message The scripting message which needs to be examined. 933 \param index The index of the specifier 934 \param specifier The message which contains the specifier 935 \param what The 'what' field of the specifier message. 936 \param property The name of the targetted property 937 \return The proper BHandler for the given scripting message. 938 */ 939 BHandler * 940 BTextView::ResolveSpecifier(BMessage *message, int32 index, 941 BMessage *specifier, int32 what, 942 const char *property) 943 { 944 BPropertyInfo propInfo(sPropertyList); 945 BHandler *target = this; 946 947 if (propInfo.FindMatch(message, index, specifier, what, property) < B_OK) 948 target = BView::ResolveSpecifier(message, index, specifier, what, property); 949 950 return target; 951 } 952 953 954 status_t 955 BTextView::GetSupportedSuites(BMessage *data) 956 { 957 if (data == NULL) 958 return B_BAD_VALUE; 959 960 status_t err = data->AddString("suites", "suite/vnd.Be-text-view"); 961 if (err != B_OK) 962 return err; 963 964 BPropertyInfo prop_info(sPropertyList); 965 err = data->AddFlat("messages", &prop_info); 966 967 if (err != B_OK) 968 return err; 969 return BView::GetSupportedSuites(data); 970 } 971 972 973 status_t 974 BTextView::Perform(perform_code d, void *arg) 975 { 976 return BView::Perform(d, arg); 977 } 978 979 980 void 981 BTextView::SetText(const char *inText, const text_run_array *inRuns) 982 { 983 SetText(inText, inText ? strlen(inText) : 0, inRuns); 984 } 985 986 987 void 988 BTextView::SetText(const char *inText, int32 inLength, const text_run_array *inRuns) 989 { 990 CancelInputMethod(); 991 992 // hide the caret/unhilite the selection 993 if (fActive) { 994 if (fSelStart != fSelEnd) 995 Highlight(fSelStart, fSelEnd); 996 else { 997 if (fCaretVisible) 998 InvertCaret(); 999 } 1000 } 1001 1002 // remove data from buffer 1003 if (fText->Length() > 0) 1004 DeleteText(0, fText->Length()); // TODO: was fText->Length() - 1 1005 1006 if (inText != NULL && inLength > 0) 1007 InsertText(inText, inLength, 0, inRuns); 1008 1009 // recalc line breaks and draw the text 1010 Refresh(0, inLength, true, false); 1011 fClickOffset = fSelStart = fSelEnd = 0; 1012 ScrollToOffset(fSelStart); 1013 1014 // draw the caret 1015 if (fActive) { 1016 if (!fCaretVisible) 1017 InvertCaret(); 1018 } 1019 } 1020 1021 1022 void 1023 BTextView::SetText(BFile *inFile, int32 inOffset, int32 inLength, 1024 const text_run_array *inRuns) 1025 { 1026 CALLED(); 1027 1028 CancelInputMethod(); 1029 1030 if (!inFile) 1031 return; 1032 1033 if (fText->Length() > 0) 1034 DeleteText(0, fText->Length()); 1035 1036 fText->InsertText(inFile, inOffset, inLength, 0); 1037 1038 // update the start offsets of each line below offset 1039 fLines->BumpOffset(inLength, LineAt(inOffset) + 1); 1040 1041 // update the style runs 1042 fStyles->BumpOffset(inLength, fStyles->OffsetToRun(inOffset - 1) + 1); 1043 1044 if (inRuns != NULL) 1045 SetRunArray(inOffset, inOffset + inLength, inRuns); 1046 else { 1047 // apply nullStyle to inserted text 1048 fStyles->SyncNullStyle(inOffset); 1049 fStyles->SetStyleRange(inOffset, inOffset + inLength, 1050 fText->Length(), B_FONT_ALL, NULL, NULL); 1051 } 1052 1053 // recalc line breaks and draw the text 1054 Refresh(0, inLength, true, false); 1055 fClickOffset = fSelStart = fSelEnd = 0; 1056 ScrollToOffset(fSelStart); 1057 1058 // draw the caret 1059 if (fActive) { 1060 if (!fCaretVisible) 1061 InvertCaret(); 1062 } 1063 } 1064 1065 1066 void 1067 BTextView::Insert(const char *inText, const text_run_array *inRuns) 1068 { 1069 if (inText != NULL) 1070 DoInsertText(inText, strlen(inText), fSelStart, inRuns, NULL); 1071 } 1072 1073 1074 void 1075 BTextView::Insert(const char *inText, int32 inLength, const text_run_array *inRuns) 1076 { 1077 if (inText != NULL && inLength > 0) { 1078 int32 realLength = strlen(inText); 1079 DoInsertText(inText, min_c(inLength, realLength), fSelStart, inRuns, NULL); 1080 } 1081 } 1082 1083 1084 void 1085 BTextView::Insert(int32 startOffset, const char *inText, int32 inLength, 1086 const text_run_array *inRuns) 1087 { 1088 CALLED(); 1089 1090 // do we really need to do anything? 1091 if (inText != NULL && inLength > 0) { 1092 int32 realLength = strlen(inText); 1093 DoInsertText(inText, min_c(inLength, realLength), startOffset, inRuns, NULL); 1094 } 1095 } 1096 1097 1098 /*! \brief Deletes the text within the current selection. 1099 */ 1100 void 1101 BTextView::Delete() 1102 { 1103 Delete(fSelStart, fSelEnd); 1104 } 1105 1106 1107 /*! \brief Delets the text comprised within the given offsets. 1108 \param startOffset The offset of the text to delete. 1109 \param endOffset The offset where the text to delete ends. 1110 */ 1111 void 1112 BTextView::Delete(int32 startOffset, int32 endOffset) 1113 { 1114 CALLED(); 1115 // anything to delete? 1116 if (startOffset == endOffset) 1117 return; 1118 1119 // hide the caret/unhilite the selection 1120 if (fActive) { 1121 if (fSelStart != fSelEnd) 1122 Highlight(fSelStart, fSelEnd); 1123 else { 1124 if (fCaretVisible) 1125 InvertCaret(); 1126 } 1127 } 1128 // remove data from buffer 1129 DeleteText(startOffset, endOffset); 1130 1131 // Check if the caret needs to be moved 1132 if (fClickOffset >= endOffset) 1133 fClickOffset -= (endOffset - startOffset); 1134 else if (fClickOffset >= startOffset && fClickOffset < endOffset) 1135 fClickOffset = startOffset; 1136 1137 fSelEnd = fSelStart = fClickOffset; 1138 1139 // recalc line breaks and draw what's left 1140 Refresh(startOffset, endOffset, true, true); 1141 1142 // draw the caret 1143 if (fActive) { 1144 if (!fCaretVisible) 1145 InvertCaret(); 1146 } 1147 } 1148 1149 1150 /*! \brief Returns the BTextView text as a C string. 1151 \return A pointer to the text. 1152 1153 It is possible that the BTextView object had to do some operations 1154 on the text, to be able to return it as a C string. 1155 If you need to call Text() repeatedly, you'd better use GetText(). 1156 */ 1157 const char * 1158 BTextView::Text() const 1159 { 1160 return fText->Text(); 1161 } 1162 1163 1164 /*! \brief Returns the length of the BTextView's text. 1165 \return The length of the text. 1166 */ 1167 int32 1168 BTextView::TextLength() const 1169 { 1170 return fText->Length(); 1171 } 1172 1173 1174 void 1175 BTextView::GetText(int32 offset, int32 length, char *buffer) const 1176 { 1177 if (buffer != NULL) 1178 fText->GetString(offset, length, buffer); 1179 } 1180 1181 1182 /*! \brief Returns the character at the given offset. 1183 \param offset The offset of the wanted character. 1184 \return The character at the given offset. 1185 */ 1186 uchar 1187 BTextView::ByteAt(int32 offset) const 1188 { 1189 if (offset < 0 || offset >= fText->Length()) 1190 return '\0'; 1191 1192 return (*fText)[offset]; 1193 } 1194 1195 1196 /*! \brief Returns the number of lines that the object contains. 1197 \return The number of lines contained in the BTextView object. 1198 */ 1199 int32 1200 BTextView::CountLines() const 1201 { 1202 return fLines->NumLines(); 1203 } 1204 1205 1206 /*! \brief Returns the index of the current line. 1207 \return The index of the current line. 1208 */ 1209 int32 1210 BTextView::CurrentLine() const 1211 { 1212 return LineAt(fSelStart); 1213 } 1214 1215 1216 /*! \brief Move the caret to the specified line. 1217 \param index The index of the line. 1218 */ 1219 void 1220 BTextView::GoToLine(int32 index) 1221 { 1222 CancelInputMethod(); 1223 fSelStart = fSelEnd = fClickOffset = OffsetAt(index); 1224 } 1225 1226 1227 /*! \brief Cuts the current selection to the clipboard. 1228 \param clipboard The clipboard where to copy the cutted text. 1229 */ 1230 void 1231 BTextView::Cut(BClipboard *clipboard) 1232 { 1233 CancelInputMethod(); 1234 if (fUndo) { 1235 delete fUndo; 1236 fUndo = new _BCutUndoBuffer_(this); 1237 } 1238 Copy(clipboard); 1239 Delete(); 1240 } 1241 1242 1243 /*! \brief Copies the current selection to the clipboard. 1244 \param clipboard The clipboard where to copy the selected text. 1245 */ 1246 void 1247 BTextView::Copy(BClipboard *clipboard) 1248 { 1249 CancelInputMethod(); 1250 1251 if (clipboard->Lock()) { 1252 clipboard->Clear(); 1253 1254 BMessage *clip = clipboard->Data(); 1255 if (clip != NULL) { 1256 clip->AddData("text/plain", B_MIME_TYPE, Text() + fSelStart, 1257 fSelEnd - fSelStart); 1258 1259 int32 size; 1260 if (fStylable) { 1261 text_run_array *runArray = RunArray(fSelStart, fSelEnd, &size); 1262 clip->AddData("application/x-vnd.Be-text_run_array", B_MIME_TYPE, 1263 runArray, size); 1264 FreeRunArray(runArray); 1265 } 1266 clipboard->Commit(); 1267 } 1268 clipboard->Unlock(); 1269 } 1270 } 1271 1272 1273 /*! \brief Paste the text contained in the clipboard to the BTextView. 1274 \param clipboard A pointer to the clipboard. 1275 */ 1276 void 1277 BTextView::Paste(BClipboard *clipboard) 1278 { 1279 CALLED(); 1280 CancelInputMethod(); 1281 1282 if (clipboard->Lock()) { 1283 BMessage *clip = clipboard->Data(); 1284 if (clip != NULL) { 1285 const char *text = NULL; 1286 ssize_t len = 0; 1287 1288 if (clip->FindData("text/plain", B_MIME_TYPE, 1289 (const void **)&text, &len) == B_OK) { 1290 text_run_array *runArray = NULL; 1291 ssize_t runLen = 0; 1292 1293 if (fStylable) 1294 clip->FindData("application/x-vnd.Be-text_run_array", B_MIME_TYPE, 1295 (const void **)&runArray, &runLen); 1296 1297 if (fUndo) { 1298 delete fUndo; 1299 fUndo = new _BPasteUndoBuffer_(this, text, len, runArray, runLen); 1300 } 1301 1302 if (fSelStart != fSelEnd) 1303 Delete(); 1304 1305 Insert(text, len, runArray); 1306 } 1307 } 1308 1309 clipboard->Unlock(); 1310 } 1311 } 1312 1313 1314 /*! \brief Deletes the currently selected text. 1315 */ 1316 void 1317 BTextView::Clear() 1318 { 1319 // We always check for fUndo != NULL (not only here), 1320 // because when fUndo is NULL, undo is deactivated. 1321 if (fUndo) { 1322 delete fUndo; 1323 fUndo = new _BClearUndoBuffer_(this); 1324 } 1325 1326 Delete(); 1327 } 1328 1329 1330 bool 1331 BTextView::AcceptsPaste(BClipboard *clipboard) 1332 { 1333 bool result = false; 1334 1335 if (fEditable && clipboard && clipboard->Lock()) { 1336 BMessage *data = clipboard->Data(); 1337 result = data && data->HasData("text/plain", B_MIME_TYPE); 1338 clipboard->Unlock(); 1339 } 1340 1341 return result; 1342 } 1343 1344 1345 bool 1346 BTextView::AcceptsDrop(const BMessage *inMessage) 1347 { 1348 if (fEditable && inMessage && inMessage->HasData("text/plain", B_MIME_TYPE)) 1349 return true; 1350 1351 return false; 1352 } 1353 1354 1355 /*! \brief Selects the text within the given offsets. 1356 \param startOffset The offset of the text to select. 1357 \param endOffset The offset where the text ends. 1358 */ 1359 void 1360 BTextView::Select(int32 startOffset, int32 endOffset) 1361 { 1362 CALLED(); 1363 if (!fSelectable) 1364 return; 1365 1366 CancelInputMethod(); 1367 1368 // a negative selection? 1369 if (startOffset > endOffset) 1370 return; 1371 1372 // is the new selection any different from the current selection? 1373 if (startOffset == fSelStart && endOffset == fSelEnd) 1374 return; 1375 1376 fStyles->InvalidateNullStyle(); 1377 1378 // pin offsets at reasonable values 1379 startOffset = (startOffset < 0) ? 0 : startOffset; 1380 endOffset = (endOffset < 0) ? 0 : endOffset; 1381 endOffset = (endOffset > fText->Length()) ? fText->Length() : endOffset; 1382 1383 // hide the caret 1384 if (fCaretVisible) 1385 InvertCaret(); 1386 1387 if (startOffset == endOffset) { 1388 if (fSelStart != fSelEnd) { 1389 // unhilite the selection 1390 if (fActive) 1391 Highlight(fSelStart, fSelEnd); 1392 } 1393 fSelStart = fSelEnd = fClickOffset = startOffset; 1394 } else { 1395 if (fActive) { 1396 // draw only those ranges that are different 1397 long start, end; 1398 if (startOffset != fSelStart) { 1399 // start of selection has changed 1400 if (startOffset > fSelStart) { 1401 start = fSelStart; 1402 end = startOffset; 1403 } else { 1404 start = startOffset; 1405 end = fSelStart; 1406 } 1407 Highlight(start, end); 1408 } 1409 1410 if (endOffset != fSelEnd) { 1411 // end of selection has changed 1412 if (endOffset > fSelEnd) { 1413 start = fSelEnd; 1414 end = endOffset; 1415 } else { 1416 start = endOffset; 1417 end = fSelEnd; 1418 } 1419 Highlight(start, end); 1420 } 1421 } 1422 fSelStart = startOffset; 1423 fSelEnd = fClickOffset = endOffset; 1424 } 1425 } 1426 1427 1428 /*! \brief Selects all the text within the BTextView. 1429 */ 1430 void 1431 BTextView::SelectAll() 1432 { 1433 Select(0, fText->Length()); 1434 } 1435 1436 1437 /*! \brief Gets the current selection. 1438 \param outStart A pointer to an int32 which will contain the selection start's offset. 1439 \param outEnd A pointer to an int32 which will contain the selection end's offset. 1440 */ 1441 void 1442 BTextView::GetSelection(int32 *outStart, int32 *outEnd) const 1443 { 1444 int32 start = 0, end = 0; 1445 1446 if (fSelectable) { 1447 start = fSelStart; 1448 end = fSelEnd; 1449 } 1450 1451 if (outStart) 1452 *outStart = start; 1453 if (outEnd) 1454 *outEnd = end; 1455 } 1456 1457 1458 void 1459 BTextView::SetFontAndColor(const BFont *inFont, uint32 inMode, const rgb_color *inColor) 1460 { 1461 SetFontAndColor(fSelStart, fSelEnd, inFont, inMode, inColor); 1462 } 1463 1464 1465 void 1466 BTextView::SetFontAndColor(int32 startOffset, int32 endOffset, const BFont* font, 1467 uint32 fontMode, const rgb_color* color) 1468 { 1469 CALLED(); 1470 1471 if (startOffset > endOffset) 1472 return; 1473 1474 BFont newFont; 1475 if (font != NULL) { 1476 newFont = font; 1477 NormalizeFont(&newFont); 1478 } 1479 1480 const int32 textLength = fText->Length(); 1481 1482 if (!fStylable) { 1483 // When the text view is not stylable, we always set the whole text's 1484 // style and ignore the offsets 1485 startOffset = 0; 1486 endOffset = textLength; 1487 } 1488 1489 // pin offsets at reasonable values 1490 if (startOffset < 0) 1491 startOffset = 0; 1492 else if (startOffset > textLength) 1493 startOffset = textLength; 1494 1495 if (endOffset < 0) 1496 endOffset = 0; 1497 else if (endOffset > textLength) 1498 endOffset = textLength; 1499 1500 // add the style to the style buffer 1501 fStyles->SetStyleRange(startOffset, endOffset, fText->Length(), 1502 fontMode, &newFont, color); 1503 1504 if (fontMode & B_FONT_FAMILY_AND_STYLE || fontMode & B_FONT_SIZE) { 1505 // recalc the line breaks and redraw with new style 1506 Refresh(startOffset, endOffset, startOffset != endOffset, false); 1507 } else { 1508 // the line breaks wont change, simply redraw 1509 DrawLines(LineAt(startOffset), LineAt(endOffset), startOffset, true); 1510 } 1511 } 1512 1513 1514 void 1515 BTextView::GetFontAndColor(int32 inOffset, BFont *outFont, rgb_color *outColor) const 1516 { 1517 fStyles->GetStyle(inOffset, outFont, outColor); 1518 } 1519 1520 1521 void 1522 BTextView::GetFontAndColor(BFont *outFont, uint32 *outMode, rgb_color *outColor, bool *outEqColor) const 1523 { 1524 fStyles->ContinuousGetStyle(outFont, outMode, outColor, outEqColor, fSelStart, fSelEnd); 1525 } 1526 1527 1528 void 1529 BTextView::SetRunArray(int32 startOffset, int32 endOffset, const text_run_array *inRuns) 1530 { 1531 CALLED(); 1532 if (startOffset > endOffset) 1533 return; 1534 1535 CancelInputMethod(); 1536 1537 const int32 textLength = fText->Length(); 1538 1539 // pin offsets at reasonable values 1540 if (startOffset < 0) 1541 startOffset = 0; 1542 else if (startOffset > textLength) 1543 startOffset = textLength; 1544 1545 if (endOffset < 0) 1546 endOffset = 0; 1547 else if (endOffset > textLength) 1548 endOffset = textLength; 1549 1550 long numStyles = inRuns->count; 1551 if (numStyles > 0) { 1552 const text_run *theRun = &inRuns->runs[0]; 1553 for (long index = 0; index < numStyles; index++) { 1554 long fromOffset = theRun->offset + startOffset; 1555 long toOffset = endOffset; 1556 if (index + 1 < numStyles) { 1557 toOffset = (theRun + 1)->offset + startOffset; 1558 toOffset = (toOffset > endOffset) ? endOffset : toOffset; 1559 } 1560 1561 fStyles->SetStyleRange(fromOffset, toOffset, textLength, 1562 B_FONT_ALL, &theRun->font, &theRun->color); 1563 1564 theRun++; 1565 } 1566 fStyles->InvalidateNullStyle(); 1567 1568 Refresh(startOffset, endOffset, true, false); 1569 } 1570 } 1571 1572 1573 /*! \brief Returns a RunArray for the text within the given offsets. 1574 \param startOffset The offset where to start. 1575 \param endOffset The offset where the wanted text ends. 1576 \param outSize A pointer to an int32 which will contain the size 1577 of the run array. 1578 \return A text_run_array for the text in the given offsets. 1579 1580 The returned text_run_array belongs to the caller, so you better 1581 free it as soon as you don't need it. 1582 */ 1583 text_run_array * 1584 BTextView::RunArray(int32 startOffset, int32 endOffset, int32 *outSize) const 1585 { 1586 STEStyleRange* styleRange = fStyles->GetStyleRange(startOffset, endOffset - 1); 1587 if (styleRange == NULL) 1588 return NULL; 1589 1590 text_run_array *runArray = AllocRunArray(styleRange->count, outSize); 1591 if (runArray != NULL) { 1592 for (int32 i = 0; i < runArray->count; i++) { 1593 runArray->runs[i].offset = styleRange->runs[i].offset; 1594 runArray->runs[i].font = styleRange->runs[i].style.font; 1595 runArray->runs[i].color = styleRange->runs[i].style.color; 1596 } 1597 } 1598 1599 free(styleRange); 1600 1601 return runArray; 1602 } 1603 1604 1605 /*! \brief Returns the line number for the character at the given offset. 1606 \param offset The offset of the wanted character. 1607 \return A line number. 1608 */ 1609 int32 1610 BTextView::LineAt(int32 offset) const 1611 { 1612 return fLines->OffsetToLine(offset); 1613 } 1614 1615 1616 /*! \brief Returns the line number for the given point. 1617 \param point A point. 1618 \return A line number. 1619 */ 1620 int32 1621 BTextView::LineAt(BPoint point) const 1622 { 1623 return fLines->PixelToLine(point.y - fTextRect.top); 1624 } 1625 1626 1627 /*! \brief Returns the location of the character at the given offset. 1628 \param inOffset The offset of the character. 1629 \param outHeight Here the function will put the height of the character at the 1630 given offset. 1631 \return A BPoint which is the location of the character. 1632 */ 1633 BPoint 1634 BTextView::PointAt(int32 inOffset, float *outHeight) const 1635 { 1636 // TODO: Cleanup. 1637 const int32 textLength = fText->Length(); 1638 int32 lineNum = LineAt(inOffset); 1639 STELine* line = (*fLines)[lineNum]; 1640 float height = 0; 1641 1642 BPoint result; 1643 result.x = 0.0; 1644 result.y = line->origin + fTextRect.top; 1645 1646 // Handle the case where there is only one line 1647 // (no text inserted) 1648 // TODO: See if we can do this better 1649 if (fStyles->NumRuns() == 0) { 1650 const rgb_color *color = NULL; 1651 const BFont *font = NULL; 1652 fStyles->GetNullStyle(&font, &color); 1653 1654 font_height fontHeight; 1655 font->GetHeight(&fontHeight); 1656 height = fontHeight.ascent + fontHeight.descent; 1657 1658 } else { 1659 height = (line + 1)->origin - line->origin; 1660 1661 // special case: go down one line if inOffset is a newline 1662 if (inOffset == textLength && (*fText)[inOffset - 1] == B_ENTER) { 1663 result.y += height; 1664 height = LineHeight(CountLines() - 1); 1665 1666 } else { 1667 int32 offset = line->offset; 1668 int32 length = inOffset - line->offset; 1669 int32 numBytes = length; 1670 bool foundTab = false; 1671 do { 1672 foundTab = fText->FindChar(B_TAB, offset, &numBytes); 1673 1674 float width = StyledWidth(offset, numBytes); 1675 1676 result.x += width; 1677 1678 if (foundTab) { 1679 result.x += ActualTabWidth(result.x); 1680 numBytes++; 1681 } 1682 1683 offset += numBytes; 1684 length -= numBytes; 1685 numBytes = length; 1686 } while (foundTab && length > 0); 1687 } 1688 } 1689 1690 if (fAlignment != B_ALIGN_LEFT) { 1691 float modifier = fTextRect.right - LineWidth(lineNum); 1692 if (fAlignment == B_ALIGN_CENTER) 1693 modifier /= 2; 1694 result.x += modifier; 1695 } 1696 // convert from text rect coordinates 1697 // NOTE: I didn't understand why "- 1.0" 1698 // and it works only correct without it on Haiku app_server. 1699 // Feel free to enlighten me though! 1700 result.x += fTextRect.left;// - 1.0; 1701 1702 // round up 1703 result.x = ceilf(result.x); 1704 result.y = ceilf(result.y); 1705 if (outHeight != NULL) 1706 *outHeight = height; 1707 1708 return result; 1709 } 1710 1711 1712 /*! \brief Returns the offset for the given location. 1713 \param point A BPoint which specify the wanted location. 1714 \return The offset for the given point. 1715 */ 1716 int32 1717 BTextView::OffsetAt(BPoint point) const 1718 { 1719 const int32 textLength = fText->Length(); 1720 1721 // should we even bother? 1722 if (point.y >= fTextRect.bottom) 1723 return textLength; 1724 else if (point.y < fTextRect.top) 1725 return 0; 1726 1727 int32 lineNum = LineAt(point); 1728 STELine* line = (*fLines)[lineNum]; 1729 1730 // special case: if point is within the text rect and PixelToLine() 1731 // tells us that it's on the last line, but if point is actually 1732 // lower than the bottom of the last line, return the last offset 1733 // (can happen for newlines) 1734 if (lineNum == (fLines->NumLines() - 1)) { 1735 if (point.y >= ((line + 1)->origin + fTextRect.top)) 1736 return textLength; 1737 } 1738 1739 // convert to text rect coordinates 1740 if (fAlignment != B_ALIGN_LEFT) { 1741 float lineWidth = fTextRect.right - LineWidth(lineNum); 1742 if (fAlignment == B_ALIGN_CENTER) 1743 lineWidth /= 2; 1744 point.x -= lineWidth; 1745 } 1746 1747 point.x -= fTextRect.left; 1748 point.x = max_c(point.x, 0.0); 1749 1750 // TODO: The following code isn't very efficient because it always starts from the left end, 1751 // so when the point is near the right end it's very slow. 1752 int32 offset = line->offset; 1753 const int32 limit = (line + 1)->offset; 1754 float location = 0; 1755 do { 1756 const int32 nextInitial = NextInitialByte(offset); 1757 const int32 saveOffset = offset; 1758 float width = 0; 1759 if (ByteAt(offset) == B_TAB) 1760 width = ActualTabWidth(location); 1761 else 1762 width = StyledWidth(saveOffset, nextInitial - saveOffset); 1763 if (location + width > point.x) { 1764 if (fabs(location + width - point.x) < fabs(location - point.x)) 1765 offset = nextInitial; 1766 break; 1767 } 1768 1769 location += width; 1770 offset = nextInitial; 1771 } while (offset < limit); 1772 1773 if (offset == (line + 1)->offset) { 1774 // special case: newlines aren't visible 1775 // return the offset of the character preceding the newline 1776 if (ByteAt(offset - 1) == B_ENTER) 1777 return --offset; 1778 1779 // special case: return the offset preceding any spaces that 1780 // aren't at the end of the buffer 1781 if (offset != textLength && ByteAt(offset - 1) == B_SPACE) 1782 return --offset; 1783 } 1784 1785 return offset; 1786 } 1787 1788 1789 /*! \brief Returns the offset of the given line. 1790 \param line A line number. 1791 \return The offset of the passed line. 1792 */ 1793 int32 1794 BTextView::OffsetAt(int32 line) const 1795 { 1796 if (line > fLines->NumLines()) 1797 return fText->Length(); 1798 1799 return (*fLines)[line]->offset; 1800 } 1801 1802 1803 /*! \brief Looks for a sequence of character that qualifies as a word. 1804 \param inOffset The offset where to start looking. 1805 \param outFromOffset A pointer to an integer which will contain the starting offset of the word. 1806 \param outToOffset A pointer to an integer which will contain the ending offset of the word. 1807 */ 1808 void 1809 BTextView::FindWord(int32 inOffset, int32 *outFromOffset, int32 *outToOffset) 1810 { 1811 int32 offset; 1812 uint32 charType = CharClassification(inOffset); 1813 1814 // check to the left 1815 int32 previous; 1816 for (offset = inOffset, previous = offset; offset > 0; 1817 previous = PreviousInitialByte(offset)) { 1818 if (CharClassification(previous) != charType) 1819 break; 1820 offset = previous; 1821 } 1822 1823 if (outFromOffset) 1824 *outFromOffset = offset; 1825 1826 // check to the right 1827 int32 textLen = TextLength(); 1828 for (offset = inOffset; offset < textLen; offset = NextInitialByte(offset)) { 1829 if (CharClassification(offset) != charType) 1830 break; 1831 } 1832 1833 if (outToOffset) 1834 *outToOffset = offset; 1835 } 1836 1837 1838 /*! \brief Returns true if the character at the given offset can be the last character in a line. 1839 \param offset The offset of the character. 1840 \return true if the character can be the last of a line, false if not. 1841 */ 1842 bool 1843 BTextView::CanEndLine(int32 offset) 1844 { 1845 // TODO: Could be improved, the bebook says there are other checks to do 1846 return (CharClassification(offset) == B_SEPARATOR_CHARACTER); 1847 } 1848 1849 1850 /*! \brief Returns the width of the line at the given index. 1851 \param lineNum A line index. 1852 */ 1853 float 1854 BTextView::LineWidth(int32 lineNum) const 1855 { 1856 if (lineNum < 0 || lineNum >= fLines->NumLines()) 1857 return 0; 1858 1859 STELine* line = (*fLines)[lineNum]; 1860 return StyledWidth(line->offset, (line + 1)->offset - line->offset); 1861 } 1862 1863 1864 /*! \brief Returns the height of the line at the given index. 1865 \param lineNum A line index. 1866 */ 1867 float 1868 BTextView::LineHeight(int32 lineNum) const 1869 { 1870 return TextHeight(lineNum, lineNum); 1871 } 1872 1873 1874 /*! \brief Returns the height of the text comprised between the two given lines. 1875 \param startLine The index of the starting line. 1876 \param endLine The index of the ending line. 1877 */ 1878 float 1879 BTextView::TextHeight(int32 startLine, int32 endLine) const 1880 { 1881 const int32 numLines = fLines->NumLines(); 1882 if (startLine < 0) 1883 startLine = 0; 1884 if (endLine > numLines - 1) 1885 endLine = numLines - 1; 1886 1887 float height = (*fLines)[endLine + 1]->origin - (*fLines)[startLine]->origin; 1888 1889 if (startLine != endLine && endLine == numLines - 1 && (*fText)[fText->Length() - 1] == B_ENTER) 1890 height += (*fLines)[endLine + 1]->origin - (*fLines)[endLine]->origin; 1891 1892 return ceilf(height); 1893 } 1894 1895 1896 void 1897 BTextView::GetTextRegion(int32 startOffset, int32 endOffset, BRegion *outRegion) const 1898 { 1899 if (!outRegion) 1900 return; 1901 1902 outRegion->MakeEmpty(); 1903 1904 // return an empty region if the range is invalid 1905 if (startOffset >= endOffset) 1906 return; 1907 1908 float startLineHeight = 0.0; 1909 float endLineHeight = 0.0; 1910 BPoint startPt = PointAt(startOffset, &startLineHeight); 1911 BPoint endPt = PointAt(endOffset, &endLineHeight); 1912 1913 startLineHeight = ceilf(startLineHeight); 1914 endLineHeight = ceilf(endLineHeight); 1915 1916 BRect selRect; 1917 1918 if (startPt.y == endPt.y) { 1919 // this is a one-line region 1920 selRect.left = max_c(startPt.x, fTextRect.left); 1921 selRect.top = startPt.y; 1922 selRect.right = endPt.x - 1.0; 1923 selRect.bottom = endPt.y + endLineHeight - 1.0; 1924 outRegion->Include(selRect); 1925 } else { 1926 // more than one line in the specified offset range 1927 selRect.left = max_c(startPt.x, fTextRect.left); 1928 selRect.top = startPt.y; 1929 selRect.right = fTextRect.right; 1930 selRect.bottom = startPt.y + startLineHeight - 1.0; 1931 outRegion->Include(selRect); 1932 1933 if (startPt.y + startLineHeight < endPt.y) { 1934 // more than two lines in the range 1935 selRect.left = fTextRect.left; 1936 selRect.top = startPt.y + startLineHeight; 1937 selRect.right = fTextRect.right; 1938 selRect.bottom = endPt.y - 1.0; 1939 outRegion->Include(selRect); 1940 } 1941 1942 selRect.left = fTextRect.left; 1943 selRect.top = endPt.y; 1944 selRect.right = endPt.x - 1.0; 1945 selRect.bottom = endPt.y + endLineHeight - 1.0; 1946 outRegion->Include(selRect); 1947 } 1948 } 1949 1950 1951 /*! \brief Scrolls the text so that the character at "inOffset" is within the visible range. 1952 \param inOffset The offset of the character. 1953 */ 1954 void 1955 BTextView::ScrollToOffset(int32 inOffset) 1956 { 1957 BRect bounds = Bounds(); 1958 float lineHeight = 0.0; 1959 BPoint point = PointAt(inOffset, &lineHeight); 1960 1961 // TODO: We should do the following, since otherwise the textview 1962 // won't scroll unless it's attached to a scrollview. 1963 /*if (!bounds.Contains(point)) 1964 ScrollTo(point); */ 1965 1966 if (ScrollBar(B_HORIZONTAL) != NULL) { 1967 if (point.x < bounds.left || point.x >= bounds.right) 1968 ScrollBar(B_HORIZONTAL)->SetValue(point.x - (bounds.IntegerWidth() / 2)); 1969 } 1970 1971 if (ScrollBar(B_VERTICAL) != NULL) { 1972 if (point.y < bounds.top || (point.y + lineHeight) >= bounds.bottom) 1973 ScrollBar(B_VERTICAL)->SetValue(point.y - (bounds.IntegerHeight() / 2)); 1974 } 1975 } 1976 1977 1978 /*! \brief Scrolls the text so that the character which begins the current selection 1979 is within the visible range. 1980 \param inOffset The offset of the character. 1981 */ 1982 void 1983 BTextView::ScrollToSelection() 1984 { 1985 ScrollToOffset(fSelStart); 1986 } 1987 1988 1989 /*! \brief Highlight the text comprised between the given offset. 1990 \param startOffset The offset of the text to highlight. 1991 \param endOffset The offset where the text to highlight ends. 1992 */ 1993 void 1994 BTextView::Highlight(int32 startOffset, int32 endOffset) 1995 { 1996 // get real 1997 if (startOffset >= endOffset) 1998 return; 1999 2000 BRegion selRegion; 2001 GetTextRegion(startOffset, endOffset, &selRegion); 2002 2003 SetDrawingMode(B_OP_INVERT); 2004 FillRegion(&selRegion, B_SOLID_HIGH); 2005 SetDrawingMode(B_OP_COPY); 2006 } 2007 2008 2009 /*! \brief Sets the BTextView's text rectangle to be the same as the passed rect. 2010 \param rect A BRect. 2011 */ 2012 void 2013 BTextView::SetTextRect(BRect rect) 2014 { 2015 if (rect == fTextRect) 2016 return; 2017 2018 fTextRect = rect; 2019 2020 if (Window() != NULL) { 2021 Invalidate(); 2022 Window()->UpdateIfNeeded(); 2023 } 2024 } 2025 2026 2027 /*! \brief Returns the current BTextView's text rectangle. 2028 \return The current text rectangle. 2029 */ 2030 BRect 2031 BTextView::TextRect() const 2032 { 2033 return fTextRect; 2034 } 2035 2036 2037 /*! \brief Sets whether the BTextView accepts multiple character styles. 2038 */ 2039 void 2040 BTextView::SetStylable(bool stylable) 2041 { 2042 fStylable = stylable; 2043 } 2044 2045 2046 /*! \brief Tells if the object is stylable. 2047 \return true if the object is stylable, false otherwise. 2048 If the object is stylable, it can show multiple fonts at the same time. 2049 */ 2050 bool 2051 BTextView::IsStylable() const 2052 { 2053 return fStylable; 2054 } 2055 2056 2057 /*! \brief Sets the distance between tab stops (in pixel). 2058 \param width The distance (in pixel) between tab stops. 2059 */ 2060 void 2061 BTextView::SetTabWidth(float width) 2062 { 2063 if (width == fTabWidth) 2064 return; 2065 2066 fTabWidth = width; 2067 2068 if (Window() != NULL) 2069 Refresh(0, fText->Length(), true, false); 2070 } 2071 2072 2073 /*! \brief Returns the BTextView's tab width. 2074 \return The BTextView's tab width. 2075 */ 2076 float 2077 BTextView::TabWidth() const 2078 { 2079 return fTabWidth; 2080 } 2081 2082 2083 /*! \brief Makes the object selectable, or not selectable. 2084 \param selectable If true, the object will be selectable from now on. 2085 if false, it won't be selectable anymore. 2086 */ 2087 void 2088 BTextView::MakeSelectable(bool selectable) 2089 { 2090 if (selectable == fSelectable) 2091 return; 2092 2093 fSelectable = selectable; 2094 2095 if (Window() != NULL) { 2096 if (fActive) { 2097 // show/hide the caret, hilite/unhilite the selection 2098 if (fSelStart != fSelEnd) 2099 Highlight(fSelStart, fSelEnd); 2100 else 2101 InvertCaret(); 2102 } 2103 } 2104 } 2105 2106 2107 /*! \brief Tells if the object is selectable 2108 \return \c true if the object is selectable, 2109 \c false if not. 2110 */ 2111 bool 2112 BTextView::IsSelectable() const 2113 { 2114 return fSelectable; 2115 } 2116 2117 2118 /*! \brief Set (or remove) the editable property for the object. 2119 \param editable If true, will make the object editable, 2120 if false, will make it not editable. 2121 */ 2122 void 2123 BTextView::MakeEditable(bool editable) 2124 { 2125 if (editable == fEditable) 2126 return; 2127 2128 fEditable = editable; 2129 // TextControls change the color of the text when 2130 // they are made editable, so we need to invalidate 2131 // the NULL style here 2132 // TODO: it works well, but it could be caused by a bug somewhere else 2133 if (fEditable) 2134 fStyles->InvalidateNullStyle(); 2135 if (Window() != NULL && fActive) { 2136 if (!fEditable) { 2137 if (fCaretVisible) 2138 InvertCaret(); 2139 CancelInputMethod(); 2140 } 2141 } 2142 } 2143 2144 2145 /*! \brief Tells if the object is editable. 2146 \return \c true if the object is editable, 2147 \c false if not. 2148 */ 2149 bool 2150 BTextView::IsEditable() const 2151 { 2152 return fEditable; 2153 } 2154 2155 2156 /*! \brief Set (or unset) word wrapping mode. 2157 \param wrap Specifies if you want word wrapping active or not. 2158 */ 2159 void 2160 BTextView::SetWordWrap(bool wrap) 2161 { 2162 if (wrap == fWrap) 2163 return; 2164 2165 if (Window() != NULL) { 2166 if (fActive) { 2167 // hide the caret, unhilite the selection 2168 if (fSelStart != fSelEnd) 2169 Highlight(fSelStart, fSelEnd); 2170 else { 2171 if (fCaretVisible) 2172 InvertCaret(); 2173 } 2174 } 2175 2176 fWrap = wrap; 2177 Refresh(0, fText->Length(), true, true); 2178 2179 if (fActive) { 2180 // show the caret, hilite the selection 2181 if (fSelStart != fSelEnd && fSelectable) 2182 Highlight(fSelStart, fSelEnd); 2183 else { 2184 if (!fCaretVisible) 2185 InvertCaret(); 2186 } 2187 } 2188 } 2189 } 2190 2191 2192 /*! \brief Tells if word wrapping is activated. 2193 \return true if word wrapping is active, false otherwise. 2194 */ 2195 bool 2196 BTextView::DoesWordWrap() const 2197 { 2198 return fWrap; 2199 } 2200 2201 2202 /*! \brief Sets the maximun number of bytes that the BTextView can contain. 2203 \param max The new max number of bytes. 2204 */ 2205 void 2206 BTextView::SetMaxBytes(int32 max) 2207 { 2208 const int32 textLength = fText->Length(); 2209 fMaxBytes = max; 2210 2211 if (fMaxBytes < textLength) 2212 Delete(fMaxBytes, textLength); 2213 } 2214 2215 2216 /*! \brief Returns the maximum number of bytes that the BTextView can contain. 2217 \return the maximum number of bytes that the BTextView can contain. 2218 */ 2219 int32 2220 BTextView::MaxBytes() const 2221 { 2222 return fMaxBytes; 2223 } 2224 2225 2226 /*! \brief Adds the given char to the disallowed chars list. 2227 \param aChar The character to add to the list. 2228 2229 After this function returns, the given character won't be accepted 2230 by the textview anymore. 2231 */ 2232 void 2233 BTextView::DisallowChar(uint32 aChar) 2234 { 2235 if (fDisallowedChars == NULL) 2236 fDisallowedChars = new BList; 2237 if (!fDisallowedChars->HasItem(reinterpret_cast<void *>(aChar))) 2238 fDisallowedChars->AddItem(reinterpret_cast<void *>(aChar)); 2239 } 2240 2241 2242 /*! \brief Removes the given character from the disallowed list. 2243 \param aChar The character to remove from the list. 2244 */ 2245 void 2246 BTextView::AllowChar(uint32 aChar) 2247 { 2248 if (fDisallowedChars != NULL) 2249 fDisallowedChars->RemoveItem(reinterpret_cast<void *>(aChar)); 2250 } 2251 2252 2253 /*! \brief Sets the way text is aligned within the text rectangle. 2254 \param flag The new alignment. 2255 */ 2256 void 2257 BTextView::SetAlignment(alignment flag) 2258 { 2259 // Do a reality check 2260 if (fAlignment != flag && 2261 (flag == B_ALIGN_LEFT || 2262 flag == B_ALIGN_RIGHT || 2263 flag == B_ALIGN_CENTER)) { 2264 fAlignment = flag; 2265 2266 // After setting new alignment, update the view/window 2267 BWindow *window = Window(); 2268 if (window) { 2269 Invalidate(); 2270 window->UpdateIfNeeded(); 2271 } 2272 } 2273 } 2274 2275 2276 /*! \brief Returns the current alignment of the text. 2277 \return The current alignment. 2278 */ 2279 alignment 2280 BTextView::Alignment() const 2281 { 2282 return fAlignment; 2283 } 2284 2285 2286 /*! \brief Sets wheter a new line of text is automatically indented. 2287 \param state The new autoindent state 2288 */ 2289 void 2290 BTextView::SetAutoindent(bool state) 2291 { 2292 fAutoindent = state; 2293 } 2294 2295 2296 /*! \brief Returns the current autoindent state. 2297 \return The current autoindent state. 2298 */ 2299 bool 2300 BTextView::DoesAutoindent() const 2301 { 2302 return fAutoindent; 2303 } 2304 2305 2306 /*! \brief Set the color space for the offscreen BBitmap. 2307 \param colors The new colorspace for the offscreen BBitmap. 2308 */ 2309 void 2310 BTextView::SetColorSpace(color_space colors) 2311 { 2312 if (colors != fColorSpace && fOffscreen) { 2313 fColorSpace = colors; 2314 DeleteOffscreen(); 2315 NewOffscreen(); 2316 } 2317 } 2318 2319 2320 /*! \brief Returns the colorspace of the offscreen BBitmap, if any. 2321 \return The colorspace of the BTextView's offscreen BBitmap. 2322 */ 2323 color_space 2324 BTextView::ColorSpace() const 2325 { 2326 return fColorSpace; 2327 } 2328 2329 2330 /*! \brief Gives to the BTextView the ability to automatically resize itself when needed. 2331 \param resize If true, the BTextView will automatically resize itself. 2332 \param resizeView The BTextView's parent view, it's the view which resizes itself. 2333 The resizing mechanism is alternative to the BView resizing. The container view 2334 (the one passed to this function) should not automatically resize itself when the parent is 2335 resized. 2336 */ 2337 void 2338 BTextView::MakeResizable(bool resize, BView *resizeView) 2339 { 2340 if (resize) { 2341 fResizable = true; 2342 fContainerView = resizeView; 2343 2344 // Wrapping mode and resizable mode can't live together 2345 if (fWrap) { 2346 fWrap = false; 2347 2348 if (fActive && Window() != NULL) { 2349 if (fSelStart != fSelEnd && fSelectable) 2350 Highlight(fSelStart, fSelEnd); 2351 2352 else if (fCaretVisible) 2353 InvertCaret(); 2354 } 2355 } 2356 } else { 2357 fResizable = false; 2358 fContainerView = NULL; 2359 if (fOffscreen) 2360 DeleteOffscreen(); 2361 NewOffscreen(); 2362 } 2363 2364 Refresh(0, fText->Length(), true, false); 2365 } 2366 2367 2368 /*! \brief Returns whether the BTextView is currently resizable. 2369 \returns whether the BTextView is currently resizable. 2370 */ 2371 bool 2372 BTextView::IsResizable() const 2373 { 2374 return fResizable; 2375 } 2376 2377 2378 /*! \brief Enables or disables the undo mechanism. 2379 \param undo If true enables the undo mechanism, if false, disables it. 2380 */ 2381 void 2382 BTextView::SetDoesUndo(bool undo) 2383 { 2384 if (undo && fUndo == NULL) 2385 fUndo = new _BUndoBuffer_(this, B_UNDO_UNAVAILABLE); 2386 else if (!undo && fUndo != NULL) { 2387 delete fUndo; 2388 fUndo = NULL; 2389 } 2390 } 2391 2392 2393 /*! \brief Tells if the object is undoable. 2394 \return Whether the object is undoable. 2395 */ 2396 bool 2397 BTextView::DoesUndo() const 2398 { 2399 return fUndo != NULL; 2400 } 2401 2402 2403 void 2404 BTextView::HideTyping(bool enabled) 2405 { 2406 if (enabled) 2407 Delete(0, fText->Length()); 2408 2409 fText->SetPasswordMode(enabled); 2410 } 2411 2412 2413 bool 2414 BTextView::IsTypingHidden() const 2415 { 2416 return fText->PasswordMode(); 2417 } 2418 2419 2420 void 2421 BTextView::ResizeToPreferred() 2422 { 2423 float widht, height; 2424 GetPreferredSize(&widht, &height); 2425 BView::ResizeTo(widht, height); 2426 } 2427 2428 2429 void 2430 BTextView::GetPreferredSize(float *width, float *height) 2431 { 2432 BView::GetPreferredSize(width, height); 2433 } 2434 2435 2436 void 2437 BTextView::AllAttached() 2438 { 2439 BView::AllAttached(); 2440 } 2441 2442 2443 void 2444 BTextView::AllDetached() 2445 { 2446 BView::AllDetached(); 2447 } 2448 2449 2450 /* static */ 2451 text_run_array * 2452 BTextView::AllocRunArray(int32 entryCount, int32 *outSize) 2453 { 2454 int32 size = sizeof(text_run_array) + (entryCount - 1) * sizeof(text_run); 2455 2456 text_run_array *runArray = (text_run_array *)malloc(size); 2457 if (runArray == NULL) { 2458 if (outSize != NULL) 2459 *outSize = 0; 2460 return NULL; 2461 } 2462 2463 memset(runArray, 0, sizeof(size)); 2464 2465 runArray->count = entryCount; 2466 2467 // Call constructors explicitly as the text_run_array 2468 // was allocated with malloc (and has to, for backwards 2469 // compatibility) 2470 for (int32 i = 0; i < runArray->count; i++) { 2471 new (&runArray->runs[i].font) BFont; 2472 } 2473 2474 if (outSize != NULL) 2475 *outSize = size; 2476 2477 return runArray; 2478 } 2479 2480 2481 /* static */ 2482 text_run_array * 2483 BTextView::CopyRunArray(const text_run_array *orig, int32 countDelta) 2484 { 2485 text_run_array *copy = AllocRunArray(countDelta, NULL); 2486 if (copy != NULL) { 2487 for (int32 i = 0; i < countDelta; i++) { 2488 copy->runs[i].offset = orig->runs[i].offset; 2489 copy->runs[i].font = orig->runs[i].font; 2490 copy->runs[i].color = orig->runs[i].color; 2491 } 2492 } 2493 return copy; 2494 } 2495 2496 2497 /* static */ 2498 void 2499 BTextView::FreeRunArray(text_run_array *array) 2500 { 2501 if (array == NULL) 2502 return; 2503 2504 // Call destructors explicitly 2505 for (int32 i = 0; i < array->count; i++) 2506 array->runs[i].font.~BFont(); 2507 2508 free(array); 2509 } 2510 2511 2512 /* static */ 2513 void * 2514 BTextView::FlattenRunArray(const text_run_array* runArray, int32* _size) 2515 { 2516 CALLED(); 2517 int32 size = sizeof(flattened_text_run_array) + (runArray->count - 1) 2518 * sizeof(flattened_text_run); 2519 2520 flattened_text_run_array *array = (flattened_text_run_array *)malloc(size); 2521 if (array == NULL) { 2522 if (_size) 2523 *_size = 0; 2524 return NULL; 2525 } 2526 2527 array->magic = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayMagic); 2528 array->version = B_HOST_TO_BENDIAN_INT32(kFlattenedTextRunArrayVersion); 2529 array->count = B_HOST_TO_BENDIAN_INT32(runArray->count); 2530 2531 for (int32 i = 0; i < runArray->count; i++) { 2532 array->styles[i].offset = B_HOST_TO_BENDIAN_INT32(runArray->runs[i].offset); 2533 runArray->runs[i].font.GetFamilyAndStyle(&array->styles[i].family, 2534 &array->styles[i].style); 2535 array->styles[i].size = B_HOST_TO_BENDIAN_FLOAT(runArray->runs[i].font.Size()); 2536 array->styles[i].shear = B_HOST_TO_BENDIAN_FLOAT(runArray->runs[i].font.Shear()); 2537 array->styles[i].face = B_HOST_TO_BENDIAN_INT16(runArray->runs[i].font.Face()); 2538 array->styles[i].red = runArray->runs[i].color.red; 2539 array->styles[i].green = runArray->runs[i].color.green; 2540 array->styles[i].blue = runArray->runs[i].color.blue; 2541 array->styles[i].alpha = 255; 2542 array->styles[i]._reserved_ = 0; 2543 } 2544 2545 if (_size) 2546 *_size = size; 2547 2548 return array; 2549 } 2550 2551 2552 /* static */ 2553 text_run_array * 2554 BTextView::UnflattenRunArray(const void* data, int32* _size) 2555 { 2556 CALLED(); 2557 flattened_text_run_array *array = (flattened_text_run_array *)data; 2558 2559 if (B_BENDIAN_TO_HOST_INT32(array->magic) != kFlattenedTextRunArrayMagic 2560 || B_BENDIAN_TO_HOST_INT32(array->version) != kFlattenedTextRunArrayVersion) { 2561 if (_size) 2562 *_size = 0; 2563 2564 return NULL; 2565 } 2566 2567 int32 count = B_BENDIAN_TO_HOST_INT32(array->count); 2568 2569 text_run_array *runArray = AllocRunArray(count, _size); 2570 if (runArray == NULL) 2571 return NULL; 2572 2573 for (int32 i = 0; i < count; i++) { 2574 runArray->runs[i].offset = B_BENDIAN_TO_HOST_INT32(array->styles[i].offset); 2575 2576 // Set family and style independently from each other, so that 2577 // even if the family doesn't exist, we try to preserve the style 2578 runArray->runs[i].font.SetFamilyAndStyle(array->styles[i].family, NULL); 2579 runArray->runs[i].font.SetFamilyAndStyle(NULL, array->styles[i].style); 2580 2581 runArray->runs[i].font.SetSize(B_BENDIAN_TO_HOST_FLOAT(array->styles[i].size)); 2582 runArray->runs[i].font.SetShear(B_BENDIAN_TO_HOST_FLOAT(array->styles[i].shear)); 2583 2584 uint16 face = B_BENDIAN_TO_HOST_INT16(array->styles[i].face); 2585 if (face != B_REGULAR_FACE) { 2586 // Be's version doesn't seem to set this correctly 2587 runArray->runs[i].font.SetFace(face); 2588 } 2589 2590 runArray->runs[i].color.red = array->styles[i].red; 2591 runArray->runs[i].color.green = array->styles[i].green; 2592 runArray->runs[i].color.blue = array->styles[i].blue; 2593 runArray->runs[i].color.alpha = array->styles[i].alpha; 2594 } 2595 2596 return runArray; 2597 } 2598 2599 2600 void 2601 BTextView::InsertText(const char *inText, int32 inLength, int32 inOffset, 2602 const text_run_array *inRuns) 2603 { 2604 CALLED(); 2605 // why add nothing? 2606 if (inLength < 1) 2607 return; 2608 2609 // TODO: Pin offset/lenght 2610 // add the text to the buffer 2611 fText->InsertText(inText, inLength, inOffset); 2612 2613 // update the start offsets of each line below offset 2614 fLines->BumpOffset(inLength, LineAt(inOffset) + 1); 2615 2616 // update the style runs 2617 fStyles->BumpOffset(inLength, fStyles->OffsetToRun(inOffset - 1) + 1); 2618 2619 if (inRuns != NULL) 2620 SetRunArray(inOffset, inOffset + inLength, inRuns); 2621 else { 2622 // apply nullStyle to inserted text 2623 fStyles->SyncNullStyle(inOffset); 2624 fStyles->SetStyleRange(inOffset, inOffset + inLength, 2625 fText->Length(), B_FONT_ALL, NULL, NULL); 2626 } 2627 } 2628 2629 2630 void 2631 BTextView::DeleteText(int32 fromOffset, int32 toOffset) 2632 { 2633 CALLED(); 2634 // sanity checking 2635 if (fromOffset >= toOffset || fromOffset < 0 || toOffset > fText->Length()) 2636 return; 2637 2638 // set nullStyle to style at beginning of range 2639 fStyles->InvalidateNullStyle(); 2640 fStyles->SyncNullStyle(fromOffset); 2641 2642 // remove from the text buffer 2643 fText->RemoveRange(fromOffset, toOffset); 2644 2645 // remove any lines that have been obliterated 2646 fLines->RemoveLineRange(fromOffset, toOffset); 2647 2648 // remove any style runs that have been obliterated 2649 fStyles->RemoveStyleRange(fromOffset, toOffset); 2650 } 2651 2652 2653 /*! \brief Undoes the last changes. 2654 \param clipboard A clipboard to use for the undo operation. 2655 */ 2656 void 2657 BTextView::Undo(BClipboard *clipboard) 2658 { 2659 if (fUndo) 2660 fUndo->Undo(clipboard); 2661 } 2662 2663 2664 undo_state 2665 BTextView::UndoState(bool *isRedo) const 2666 { 2667 return fUndo == NULL ? B_UNDO_UNAVAILABLE : fUndo->State(isRedo); 2668 } 2669 2670 2671 void 2672 BTextView::GetDragParameters(BMessage *drag, BBitmap **bitmap, BPoint *point, BHandler **handler) 2673 { 2674 CALLED(); 2675 if (drag == NULL) 2676 return; 2677 2678 // Add originator and action 2679 drag->AddPointer("be:originator", this); 2680 drag->AddInt32("be_actions", B_TRASH_TARGET); 2681 2682 // add the text 2683 drag->AddData("text/plain", B_MIME_TYPE, fText->Text() + fSelStart, 2684 fSelEnd - fSelStart); 2685 2686 // add the corresponding styles 2687 int32 size = 0; 2688 text_run_array *styles = RunArray(fSelStart, fSelEnd, &size); 2689 2690 if (styles != NULL) { 2691 drag->AddData("application/x-vnd.Be-text_run_array", B_MIME_TYPE, 2692 styles, size); 2693 2694 FreeRunArray(styles); 2695 } 2696 2697 if (bitmap != NULL) 2698 *bitmap = NULL; 2699 if (handler != NULL) 2700 *handler = NULL; 2701 } 2702 2703 2704 void BTextView::_ReservedTextView3() {} 2705 void BTextView::_ReservedTextView4() {} 2706 void BTextView::_ReservedTextView5() {} 2707 void BTextView::_ReservedTextView6() {} 2708 void BTextView::_ReservedTextView7() {} 2709 void BTextView::_ReservedTextView8() {} 2710 void BTextView::_ReservedTextView9() {} 2711 void BTextView::_ReservedTextView10() {} 2712 void BTextView::_ReservedTextView11() {} 2713 void BTextView::_ReservedTextView12() {} 2714 2715 2716 /*! \brief Inits the BTextView object. 2717 \param textRect The BTextView's text rect. 2718 \param initialFont The font which the BTextView will use. 2719 \param initialColor The initial color of the text. 2720 */ 2721 void 2722 BTextView::InitObject(BRect textRect, const BFont *initialFont, 2723 const rgb_color *initialColor) 2724 { 2725 BFont font; 2726 if (initialFont == NULL) 2727 GetFont(&font); 2728 else 2729 font = *initialFont; 2730 2731 NormalizeFont(&font); 2732 2733 if (initialColor == NULL) 2734 initialColor = &kBlackColor; 2735 2736 fText = new _BTextGapBuffer_; 2737 fLines = new _BLineBuffer_; 2738 fStyles = new _BStyleBuffer_(&font, initialColor); 2739 2740 // We put these here instead of in the constructor initializer list 2741 // to have less code duplication, and a single place where to do changes 2742 // if needed., 2743 fTextRect = textRect; 2744 fSelStart = fSelEnd = 0; 2745 fCaretVisible = false; 2746 fCaretTime = 0; 2747 fClickOffset = 0; 2748 fClickCount = 0; 2749 fClickTime = 0; 2750 fDragOffset = -1; 2751 fCursor = 0; 2752 fActive = false; 2753 fStylable = false; 2754 fTabWidth = 28.0; 2755 fSelectable = true; 2756 fEditable = true; 2757 fWrap = true; 2758 fMaxBytes = LONG_MAX; 2759 fDisallowedChars = NULL; 2760 fAlignment = B_ALIGN_LEFT; 2761 fAutoindent = false; 2762 fOffscreen = NULL; 2763 fColorSpace = B_CMAP8; 2764 fResizable = false; 2765 fContainerView = NULL; 2766 fUndo = NULL; 2767 fInline = NULL; 2768 fDragRunner = NULL; 2769 fClickRunner = NULL; 2770 fTrackingMouse = NULL; 2771 fTextChange = NULL; 2772 } 2773 2774 2775 /*! \brief Called when Backspace key is pressed. 2776 */ 2777 void 2778 BTextView::HandleBackspace() 2779 { 2780 if (fUndo) { 2781 _BTypingUndoBuffer_ *undoBuffer = dynamic_cast<_BTypingUndoBuffer_ *>(fUndo); 2782 if (!undoBuffer) { 2783 delete fUndo; 2784 fUndo = undoBuffer = new _BTypingUndoBuffer_(this); 2785 } 2786 undoBuffer->BackwardErase(); 2787 } 2788 2789 if (fSelStart == fSelEnd) { 2790 if (fSelStart == 0) 2791 return; 2792 else 2793 fSelStart = PreviousInitialByte(fSelStart); 2794 } else 2795 Highlight(fSelStart, fSelEnd); 2796 2797 DeleteText(fSelStart, fSelEnd); 2798 fClickOffset = fSelEnd = fSelStart; 2799 2800 Refresh(fSelStart, fSelEnd, true, true); 2801 } 2802 2803 2804 /*! \brief Called when any arrow key is pressed. 2805 \param inArrowKey The code for the pressed key. 2806 */ 2807 void 2808 BTextView::HandleArrowKey(uint32 inArrowKey) 2809 { 2810 // return if there's nowhere to go 2811 if (fText->Length() == 0) 2812 return; 2813 2814 int32 selStart = fSelStart; 2815 int32 selEnd = fSelEnd; 2816 2817 int32 modifiers = 0; 2818 BMessage *message = Window()->CurrentMessage(); 2819 if (message != NULL) 2820 message->FindInt32("modifiers", &modifiers); 2821 2822 bool shiftDown = modifiers & B_SHIFT_KEY; 2823 2824 int32 currentOffset = fClickOffset; 2825 switch (inArrowKey) { 2826 case B_LEFT_ARROW: 2827 if (shiftDown) { 2828 fClickOffset = PreviousInitialByte(fClickOffset); 2829 if (fClickOffset != currentOffset) { 2830 if (fClickOffset >= fSelStart) 2831 selEnd = fClickOffset; 2832 else 2833 selStart = fClickOffset; 2834 } 2835 } else if (fSelStart != fSelEnd) 2836 fClickOffset = fSelStart; 2837 else 2838 fClickOffset = PreviousInitialByte(fSelStart); 2839 2840 break; 2841 2842 case B_RIGHT_ARROW: 2843 if (shiftDown) { 2844 fClickOffset = NextInitialByte(fClickOffset); 2845 if (fClickOffset != currentOffset) { 2846 if (fClickOffset <= fSelEnd) 2847 selStart = fClickOffset; 2848 else 2849 selEnd = fClickOffset; 2850 } 2851 } else if (fSelStart != fSelEnd) 2852 fClickOffset = fSelEnd; 2853 else 2854 fClickOffset = NextInitialByte(fSelEnd); 2855 break; 2856 2857 case B_UP_ARROW: 2858 { 2859 float height; 2860 BPoint point = PointAt(fClickOffset, &height); 2861 point.y -= height; 2862 fClickOffset = OffsetAt(point); 2863 if (shiftDown) { 2864 if (fClickOffset != currentOffset) { 2865 if (fClickOffset >= fSelStart) 2866 selEnd = fClickOffset; 2867 else 2868 selStart = fClickOffset; 2869 } 2870 } 2871 break; 2872 } 2873 2874 case B_DOWN_ARROW: 2875 { 2876 float height; 2877 BPoint point = PointAt(fClickOffset, &height); 2878 point.y += height; 2879 fClickOffset = OffsetAt(point); 2880 if (shiftDown) { 2881 if (fClickOffset != currentOffset) { 2882 if (fClickOffset <= fSelEnd) 2883 selStart = fClickOffset; 2884 else 2885 selEnd = fClickOffset; 2886 } 2887 } 2888 break; 2889 } 2890 } 2891 2892 // invalidate the null style 2893 fStyles->InvalidateNullStyle(); 2894 2895 currentOffset = fClickOffset; 2896 if (shiftDown) 2897 Select(selStart, selEnd); 2898 else 2899 Select(fClickOffset, fClickOffset); 2900 2901 fClickOffset = currentOffset; 2902 // Select sets fClickOffset = fSelEnd 2903 2904 // scroll if needed 2905 ScrollToOffset(fClickOffset); 2906 } 2907 2908 2909 /*! \brief Called when the Delete key is pressed. 2910 */ 2911 void 2912 BTextView::HandleDelete() 2913 { 2914 if (fUndo) { 2915 _BTypingUndoBuffer_ *undoBuffer = dynamic_cast<_BTypingUndoBuffer_ *>(fUndo); 2916 if (!undoBuffer) { 2917 delete fUndo; 2918 fUndo = undoBuffer = new _BTypingUndoBuffer_(this); 2919 } 2920 undoBuffer->ForwardErase(); 2921 } 2922 2923 if (fSelStart == fSelEnd) { 2924 if (fSelEnd == fText->Length()) 2925 return; 2926 else 2927 fSelEnd = NextInitialByte(fSelEnd); 2928 } else 2929 Highlight(fSelStart, fSelEnd); 2930 2931 DeleteText(fSelStart, fSelEnd); 2932 2933 fClickOffset = fSelEnd = fSelStart; 2934 2935 Refresh(fSelStart, fSelEnd, true, true); 2936 } 2937 2938 2939 /*! \brief Called when a "Page key" is pressed. 2940 \param inPageKey The page key which has been pressed. 2941 */ 2942 void 2943 BTextView::HandlePageKey(uint32 inPageKey) 2944 { 2945 int32 mods = 0; 2946 BMessage *currentMessage = Window()->CurrentMessage(); 2947 if (currentMessage) 2948 currentMessage->FindInt32("modifiers", &mods); 2949 2950 bool shiftDown = mods & B_SHIFT_KEY; 2951 2952 STELine* line = NULL; 2953 2954 int32 start = fSelStart, end = fSelEnd; 2955 2956 switch (inPageKey) { 2957 case B_HOME: 2958 line = (*fLines)[CurrentLine()]; 2959 fClickOffset = line->offset; 2960 if (shiftDown) { 2961 if (fClickOffset <= fSelStart) { 2962 start = fClickOffset; 2963 end = fSelEnd; 2964 } else { 2965 start = fSelStart; 2966 end = fClickOffset; 2967 } 2968 } else 2969 start = end = fClickOffset; 2970 2971 break; 2972 2973 case B_END: 2974 // If we are on the last line, just go to the last 2975 // character in the buffer, otherwise get the starting 2976 // offset of the next line, and go to the previous character 2977 if (CurrentLine() + 1 < fLines->NumLines()) { 2978 line = (*fLines)[CurrentLine() + 1]; 2979 fClickOffset = PreviousInitialByte(line->offset); 2980 } else { 2981 // This check if needed to avoid moving the cursor 2982 // when the cursor is on the last line, and that line 2983 // is empty 2984 if (fClickOffset != fText->Length()) { 2985 fClickOffset = fText->Length(); 2986 if (ByteAt(fClickOffset - 1) == B_ENTER) 2987 fClickOffset--; 2988 } 2989 } 2990 2991 if (shiftDown) { 2992 if (fClickOffset >= fSelEnd) { 2993 start = fSelStart; 2994 end = fClickOffset; 2995 } else { 2996 start = fClickOffset; 2997 end = fSelEnd; 2998 } 2999 } else 3000 start = end = fClickOffset; 3001 3002 break; 3003 3004 case B_PAGE_UP: 3005 { 3006 BPoint currentPos = PointAt(fClickOffset); 3007 3008 currentPos.y -= Bounds().Height(); 3009 fClickOffset = OffsetAt(LineAt(currentPos)); 3010 3011 if (shiftDown) { 3012 if (fClickOffset <= fSelStart) { 3013 start = fClickOffset; 3014 end = fSelEnd; 3015 } else { 3016 start = fSelStart; 3017 end = fClickOffset; 3018 } 3019 } else 3020 start = end = fClickOffset; 3021 3022 break; 3023 } 3024 3025 case B_PAGE_DOWN: 3026 { 3027 BPoint currentPos = PointAt(fClickOffset); 3028 3029 currentPos.y += Bounds().Height(); 3030 fClickOffset = OffsetAt(LineAt(currentPos) + 1); 3031 3032 if (shiftDown) { 3033 if (fClickOffset >= fSelEnd) { 3034 start = fSelStart; 3035 end = fClickOffset; 3036 } else { 3037 start = fClickOffset; 3038 end = fSelEnd; 3039 } 3040 } else 3041 start = end = fClickOffset; 3042 3043 break; 3044 } 3045 } 3046 3047 ScrollToOffset(fClickOffset); 3048 Select(start, end); 3049 } 3050 3051 3052 /*! \brief Called when an alphanumeric key is pressed. 3053 \param bytes The string or character associated with the key. 3054 \param numBytes The amount of bytes containes in "bytes". 3055 */ 3056 void 3057 BTextView::HandleAlphaKey(const char *bytes, int32 numBytes) 3058 { 3059 // TODO: block input if not editable (Andrew) 3060 if (fUndo) { 3061 _BTypingUndoBuffer_ *undoBuffer = dynamic_cast<_BTypingUndoBuffer_ *>(fUndo); 3062 if (!undoBuffer) { 3063 delete fUndo; 3064 fUndo = undoBuffer = new _BTypingUndoBuffer_(this); 3065 } 3066 undoBuffer->InputCharacter(numBytes); 3067 } 3068 3069 bool refresh = fSelStart != fText->Length(); 3070 3071 if (fSelStart != fSelEnd) { 3072 Highlight(fSelStart, fSelEnd); 3073 DeleteText(fSelStart, fSelEnd); 3074 refresh = true; 3075 } 3076 3077 if (fAutoindent && numBytes == 1 && *bytes == B_ENTER) { 3078 int32 start, offset; 3079 start = offset = OffsetAt(LineAt(fSelStart)); 3080 3081 while (ByteAt(offset) != '\0' && 3082 (ByteAt(offset) == B_TAB || ByteAt(offset) == B_SPACE)) 3083 offset++; 3084 3085 if (start != offset) 3086 InsertText(Text() + start, offset - start, fSelStart, NULL); 3087 3088 InsertText(bytes, numBytes, fSelStart, NULL); 3089 numBytes += offset - start; 3090 3091 } else 3092 InsertText(bytes, numBytes, fSelStart, NULL); 3093 3094 int32 saveStart = fSelStart; 3095 fClickOffset = fSelEnd = fSelStart = fSelStart + numBytes; 3096 3097 if (Window()) 3098 Refresh(saveStart, fSelEnd, refresh, true); 3099 } 3100 3101 3102 /*! \brief Redraw the text comprised between the two given offsets, 3103 recalculating linebreaks if needed. 3104 \param fromOffset The offset from where to refresh. 3105 \param toOffset The offset where to refresh to. 3106 \param erase If true, the function will also erase the textview content 3107 in the parts where text isn't present. 3108 \param scroll If true, function will scroll the view to the end offset. 3109 */ 3110 void 3111 BTextView::Refresh(int32 fromOffset, int32 toOffset, bool erase, bool scroll) 3112 { 3113 // TODO: Cleanup 3114 float saveHeight = fTextRect.Height(); 3115 int32 fromLine = LineAt(fromOffset); 3116 int32 toLine = LineAt(toOffset); 3117 int32 saveFromLine = fromLine; 3118 int32 saveToLine = toLine; 3119 float saveLineHeight = LineHeight(fromLine); 3120 3121 RecalculateLineBreaks(&fromLine, &toLine); 3122 3123 // TODO: Maybe there is still something we can do without a window... 3124 if (!Window()) 3125 return; 3126 3127 BRect bounds = Bounds(); 3128 float newHeight = fTextRect.Height(); 3129 3130 // if the line breaks have changed, force an erase 3131 if (fromLine != saveFromLine || toLine != saveToLine 3132 || newHeight != saveHeight ) 3133 erase = true; 3134 3135 if (newHeight != saveHeight) { 3136 // the text area has changed 3137 if (newHeight < saveHeight) 3138 toLine = LineAt(BPoint(0.0f, saveHeight + fTextRect.top)); 3139 else 3140 toLine = LineAt(BPoint(0.0f, newHeight + fTextRect.top)); 3141 } 3142 3143 // draw only those lines that are visible 3144 int32 fromVisible = LineAt(BPoint(0.0f, bounds.top)); 3145 int32 toVisible = LineAt(BPoint(0.0f, bounds.bottom)); 3146 fromLine = max_c(fromVisible, fromLine); 3147 toLine = min_c(toLine, toVisible); 3148 3149 int32 drawOffset = fromOffset; 3150 if (LineHeight(fromLine) != saveLineHeight || 3151 newHeight < saveHeight || fromLine < saveFromLine || fAlignment != B_ALIGN_LEFT) 3152 drawOffset = (*fLines)[fromLine]->offset; 3153 3154 if (fResizable) 3155 AutoResize(false); 3156 3157 DrawLines(fromLine, toLine, drawOffset, erase); 3158 3159 // erase the area below the text 3160 BRect eraseRect = bounds; 3161 eraseRect.top = fTextRect.top + (*fLines)[fLines->NumLines()]->origin; 3162 eraseRect.bottom = fTextRect.top + saveHeight; 3163 if (eraseRect.bottom > eraseRect.top && eraseRect.Intersects(bounds)) { 3164 SetLowColor(ViewColor()); 3165 FillRect(eraseRect, B_SOLID_LOW); 3166 } 3167 3168 // update the scroll bars if the text area has changed 3169 if (newHeight != saveHeight) 3170 UpdateScrollbars(); 3171 3172 if (scroll) 3173 ScrollToSelection(); 3174 3175 Flush(); 3176 } 3177 3178 3179 void 3180 BTextView::RecalculateLineBreaks(int32 *startLine, int32 *endLine) 3181 { 3182 // are we insane? 3183 *startLine = (*startLine < 0) ? 0 : *startLine; 3184 *endLine = (*endLine > fLines->NumLines() - 1) ? fLines->NumLines() - 1 : *endLine; 3185 3186 int32 textLength = fText->Length(); 3187 int32 lineIndex = (*startLine > 0) ? *startLine - 1 : 0; 3188 int32 recalThreshold = (*fLines)[*endLine + 1]->offset; 3189 float width = fTextRect.Width(); 3190 STELine* curLine = (*fLines)[lineIndex]; 3191 STELine* nextLine = curLine + 1; 3192 3193 do { 3194 float ascent, descent; 3195 int32 fromOffset = curLine->offset; 3196 int32 toOffset = FindLineBreak(fromOffset, &ascent, &descent, &width); 3197 3198 // we want to advance at least by one character 3199 int32 nextOffset = NextInitialByte(fromOffset); 3200 if (toOffset < nextOffset && fromOffset < textLength) 3201 toOffset = nextOffset; 3202 3203 // set the ascent of this line 3204 curLine->ascent = ascent; 3205 3206 lineIndex++; 3207 STELine saveLine = *nextLine; 3208 if ( lineIndex > fLines->NumLines() || 3209 toOffset < nextLine->offset ) { 3210 // the new line comes before the old line start, add a line 3211 STELine newLine; 3212 newLine.offset = toOffset; 3213 newLine.origin = ceilf(curLine->origin + ascent + descent) + 1; 3214 newLine.ascent = 0; 3215 fLines->InsertLine(&newLine, lineIndex); 3216 } else { 3217 // update the exising line 3218 nextLine->offset = toOffset; 3219 nextLine->origin = ceilf(curLine->origin + ascent + descent) + 1; 3220 3221 // remove any lines that start before the current line 3222 while ( lineIndex < fLines->NumLines() && 3223 toOffset >= ((*fLines)[lineIndex] + 1)->offset ) 3224 fLines->RemoveLines(lineIndex + 1); 3225 3226 nextLine = (*fLines)[lineIndex]; 3227 if (nextLine->offset == saveLine.offset) { 3228 if (nextLine->offset >= recalThreshold) { 3229 if (nextLine->origin != saveLine.origin) 3230 fLines->BumpOrigin(nextLine->origin - saveLine.origin, 3231 lineIndex + 1); 3232 break; 3233 } 3234 } else { 3235 if (lineIndex > 0 && lineIndex == *startLine) 3236 *startLine = lineIndex - 1; 3237 } 3238 } 3239 3240 curLine = (*fLines)[lineIndex]; 3241 nextLine = curLine + 1; 3242 } while (curLine->offset < textLength); 3243 3244 // update the text rect 3245 float newHeight = TextHeight(0, fLines->NumLines() - 1); 3246 fTextRect.bottom = fTextRect.top + newHeight; 3247 3248 *endLine = lineIndex - 1; 3249 *startLine = min_c(*startLine, *endLine); 3250 } 3251 3252 3253 int32 3254 BTextView::FindLineBreak(int32 fromOffset, float *outAscent, float *outDescent, float *ioWidth) 3255 { 3256 *outAscent = 0.0; 3257 *outDescent = 0.0; 3258 3259 const int32 limit = fText->Length(); 3260 3261 // is fromOffset at the end? 3262 if (fromOffset >= limit) { 3263 // try to return valid height info anyway 3264 if (fStyles->NumRuns() > 0) 3265 fStyles->Iterate(fromOffset, 1, fInline, NULL, NULL, outAscent, outDescent); 3266 else { 3267 if (fStyles->IsValidNullStyle()) { 3268 const BFont *font = NULL; 3269 fStyles->GetNullStyle(&font, NULL); 3270 3271 font_height fh; 3272 font->GetHeight(&fh); 3273 *outAscent = fh.ascent; 3274 *outDescent = fh.descent + fh.leading; 3275 } 3276 } 3277 3278 return limit; 3279 } 3280 3281 int32 offset = fromOffset; 3282 3283 // Text wrapping is turned off. 3284 // Just find the offset of the first \n character 3285 if (!fWrap) { 3286 offset = limit - fromOffset; 3287 fText->FindChar(B_ENTER, fromOffset, &offset); 3288 offset += fromOffset; 3289 offset = (offset < limit) ? offset + 1 : limit; 3290 3291 *ioWidth = StyledWidth(fromOffset, offset - fromOffset, outAscent, outDescent); 3292 3293 return offset; 3294 } 3295 3296 bool done = false; 3297 float ascent = 0.0; 3298 float descent = 0.0; 3299 int32 delta = 0; 3300 float deltaWidth = 0.0; 3301 float tabWidth = 0.0; 3302 float strWidth = 0.0; 3303 3304 // wrap the text 3305 do { 3306 bool foundTab = false; 3307 3308 // find the next line break candidate 3309 for ( ; (offset + delta) < limit ; delta++) { 3310 if (CanEndLine(offset + delta)) 3311 break; 3312 } 3313 for ( ; (offset + delta) < limit; delta++) { 3314 uchar theChar = (*fText)[offset + delta]; 3315 if (!CanEndLine(offset + delta)) 3316 break; 3317 3318 if (theChar == B_ENTER) { 3319 // found a newline, we're done! 3320 done = true; 3321 delta++; 3322 break; 3323 } else { 3324 // include all trailing spaces and tabs, 3325 // but not spaces after tabs 3326 if (theChar != B_SPACE && theChar != B_TAB) 3327 break; 3328 else { 3329 if (theChar == B_SPACE && foundTab) 3330 break; 3331 else { 3332 if (theChar == B_TAB) 3333 foundTab = true; 3334 } 3335 } 3336 } 3337 } 3338 delta = max_c(delta, 1); 3339 3340 deltaWidth = StyledWidth(offset, delta, &ascent, &descent); 3341 strWidth += deltaWidth; 3342 3343 if (!foundTab) 3344 tabWidth = 0.0; 3345 else { 3346 int32 tabCount = 0; 3347 for (int32 i = delta - 1; (*fText)[offset + i] == B_TAB; i--) 3348 tabCount++; 3349 3350 tabWidth = ActualTabWidth(strWidth); 3351 if (tabCount > 1) 3352 tabWidth += ((tabCount - 1) * fTabWidth); 3353 strWidth += tabWidth; 3354 } 3355 3356 if (strWidth >= *ioWidth) { 3357 // we've found where the line will wrap 3358 bool foundNewline = done; 3359 done = true; 3360 int32 pos = delta - 1; 3361 if (!CanEndLine(offset + pos)) 3362 break; 3363 3364 strWidth -= (deltaWidth + tabWidth); 3365 3366 for ( ; ((offset + pos) > offset); pos--) { 3367 if (!CanEndLine(offset + pos)) 3368 break; 3369 } 3370 3371 strWidth += StyledWidth(offset, pos + 1, &ascent, &descent); 3372 if (strWidth >= *ioWidth) 3373 break; 3374 3375 if (!foundNewline) { 3376 for ( ; (offset + delta) < limit; delta++) { 3377 if ((*fText)[offset + delta] != B_SPACE && 3378 (*fText)[offset + delta] != B_TAB) 3379 break; 3380 } 3381 if ( (offset + delta) < limit && 3382 (*fText)[offset + delta] == B_ENTER ) 3383 delta++; 3384 } 3385 // get the ascent and descent of the spaces/tabs 3386 StyledWidth(offset, delta, &ascent, &descent); 3387 } 3388 3389 *outAscent = max_c(ascent, *outAscent); 3390 *outDescent = max_c(descent, *outDescent); 3391 3392 offset += delta; 3393 delta = 0; 3394 } while (offset < limit && !done); 3395 3396 if (offset - fromOffset < 1) { 3397 // there weren't any words that fit entirely in this line 3398 // force a break in the middle of a word 3399 *outAscent = 0.0; 3400 *outDescent = 0.0; 3401 strWidth = 0.0; 3402 3403 int32 current = fromOffset; 3404 for (offset = fromOffset; offset <= limit; current = offset, offset = NextInitialByte(offset)) { 3405 strWidth += StyledWidth(current, offset - current, &ascent, &descent); 3406 3407 if (strWidth >= *ioWidth) { 3408 offset = PreviousInitialByte(offset); 3409 break; 3410 } 3411 3412 *outAscent = max_c(ascent, *outAscent); 3413 *outDescent = max_c(descent, *outDescent); 3414 } 3415 } 3416 3417 return min_c(offset, limit); 3418 } 3419 3420 3421 /*! \brief Calculate the width of the text within the given limits. 3422 \param fromOffset The offset where to start. 3423 \param length The length of the text to examine. 3424 \param outAscent A pointer to a float which will contain the maximum ascent. 3425 \param outDescent A pointer to a float which will contain the maximum descent. 3426 \return The width for the text within the given limits. 3427 */ 3428 float 3429 BTextView::StyledWidth(int32 fromOffset, int32 length, float *outAscent, 3430 float *outDescent) const 3431 { 3432 float result = 0.0; 3433 float ascent = 0.0; 3434 float descent = 0.0; 3435 float maxAscent = 0.0; 3436 float maxDescent = 0.0; 3437 3438 // iterate through the style runs 3439 const BFont *font = NULL; 3440 int32 numChars; 3441 while ((numChars = fStyles->Iterate(fromOffset, length, fInline, &font, NULL, &ascent, &descent)) != 0) { 3442 maxAscent = max_c(ascent, maxAscent); 3443 maxDescent = max_c(descent, maxDescent); 3444 3445 #if USE_WIDTHBUFFER 3446 // Use _BWidthBuffer_ if possible 3447 if (sWidths != NULL) { 3448 LockWidthBuffer(); 3449 result += sWidths->StringWidth(*fText, fromOffset, numChars, font); 3450 UnlockWidthBuffer(); 3451 } else 3452 #endif 3453 result += font->StringWidth(fText->Text() + fromOffset, numChars); 3454 3455 fromOffset += numChars; 3456 length -= numChars; 3457 } 3458 3459 if (outAscent != NULL) 3460 *outAscent = maxAscent; 3461 if (outDescent != NULL) 3462 *outDescent = maxDescent; 3463 3464 return result; 3465 } 3466 3467 3468 // Unlike the StyledWidth method, this one takes as parameter 3469 // the number of chars, not the number of bytes. 3470 float 3471 BTextView::StyledWidthUTF8Safe(int32 fromOffset, int32 numChars, 3472 float *outAscent, float *outDescent) const 3473 { 3474 int32 toOffset = fromOffset; 3475 while (numChars--) 3476 toOffset = NextInitialByte(toOffset); 3477 3478 const int32 length = toOffset - fromOffset; 3479 return StyledWidth(fromOffset, length, outAscent, outDescent); 3480 } 3481 3482 3483 /*! \brief Calculate the actual tab width for the given location. 3484 \param location The location to calculate the tab width of. 3485 \return The actual tab width for the given location 3486 */ 3487 float 3488 BTextView::ActualTabWidth(float location) const 3489 { 3490 return fTabWidth - fmod(location, fTabWidth); 3491 } 3492 3493 3494 void 3495 BTextView::DoInsertText(const char *inText, int32 inLength, int32 inOffset, 3496 const text_run_array *inRuns, _BTextChangeResult_ *outResult) 3497 { 3498 CancelInputMethod(); 3499 3500 // Don't do any check, the public methods will have adjusted 3501 // eventual bogus values... 3502 3503 const int32 textLength = TextLength(); 3504 if (inOffset > textLength) 3505 inOffset = textLength; 3506 3507 // copy data into buffer 3508 InsertText(inText, inLength, inOffset, inRuns); 3509 3510 // offset the caret/selection 3511 int32 saveStart = fSelStart; 3512 fSelStart += inLength; 3513 fSelEnd += inLength; 3514 3515 // recalc line breaks and draw the text 3516 Refresh(saveStart, fSelEnd, true, false); 3517 } 3518 3519 3520 void 3521 BTextView::DoDeleteText(int32 fromOffset, int32 toOffset, _BTextChangeResult_ *outResult) 3522 { 3523 CALLED(); 3524 } 3525 3526 3527 void 3528 BTextView::_DrawLine(BView *view, const int32 &lineNum, const int32 &startOffset, 3529 const bool &erase, BRect &eraseRect, BRegion &inputRegion) 3530 { 3531 STELine *line = (*fLines)[lineNum]; 3532 float startLeft = fTextRect.left; 3533 if (startOffset != -1) { 3534 if (ByteAt(startOffset) == B_ENTER) { 3535 // StartOffset is a newline 3536 startLeft = PointAt(line->offset).x; 3537 } else 3538 startLeft = PointAt(startOffset).x; 3539 } 3540 3541 int32 length = (line + 1)->offset; 3542 if (startOffset != -1) 3543 length -= startOffset; 3544 else 3545 length -= line->offset; 3546 3547 // DrawString() chokes if you draw a newline 3548 if (ByteAt((line + 1)->offset - 1) == B_ENTER) 3549 length--; 3550 if (fAlignment != B_ALIGN_LEFT) { 3551 // B_ALIGN_RIGHT 3552 startLeft = (fTextRect.right - LineWidth(lineNum)); 3553 if (fAlignment == B_ALIGN_CENTER) 3554 startLeft /= 2; 3555 startLeft += fTextRect.left; 3556 } 3557 view->MovePenTo(startLeft, line->origin + line->ascent + fTextRect.top + 1); 3558 if (erase) { 3559 eraseRect.top = line->origin + fTextRect.top; 3560 eraseRect.bottom = (line + 1)->origin + fTextRect.top; 3561 3562 view->FillRect(eraseRect, B_SOLID_LOW); 3563 } 3564 3565 // do we have any text to draw? 3566 if (length > 0) { 3567 bool foundTab = false; 3568 int32 tabChars = 0; 3569 int32 numTabs = 0; 3570 int32 offset = startOffset != -1 ? startOffset : line->offset; 3571 const BFont *font = NULL; 3572 const rgb_color *color = NULL; 3573 int32 numBytes; 3574 // iterate through each style on this line 3575 while ((numBytes = fStyles->Iterate(offset, length, fInline, &font, &color)) != 0) { 3576 view->SetFont(font); 3577 view->SetHighColor(*color); 3578 3579 tabChars = numBytes; 3580 do { 3581 foundTab = fText->FindChar(B_TAB, offset, &tabChars); 3582 if (foundTab) { 3583 do { 3584 numTabs++; 3585 if (ByteAt(offset + tabChars + numTabs) != B_TAB) 3586 break; 3587 } while ((tabChars + numTabs) < numBytes); 3588 } 3589 3590 if (inputRegion.CountRects() > 0) { 3591 BRegion textRegion; 3592 GetTextRegion(offset, offset + length, &textRegion); 3593 3594 textRegion.IntersectWith(&inputRegion); 3595 view->PushState(); 3596 3597 // Highlight in blue the inputted text 3598 view->SetHighColor(kBlueInputColor); 3599 view->FillRect(textRegion.Frame()); 3600 3601 // Highlight in red the selected part 3602 if (fInline->SelectionLength() > 0) { 3603 BRegion selectedRegion; 3604 GetTextRegion(fInline->Offset() + fInline->SelectionOffset(), 3605 fInline->Offset() + fInline->SelectionOffset() + fInline->SelectionLength(), 3606 &selectedRegion); 3607 3608 textRegion.IntersectWith(&selectedRegion); 3609 3610 view->SetHighColor(kRedInputColor); 3611 view->FillRect(textRegion.Frame()); 3612 } 3613 3614 view->PopState(); 3615 } 3616 3617 int32 returnedBytes = tabChars; 3618 view->DrawString(fText->GetString(offset, &returnedBytes), returnedBytes); 3619 3620 if (foundTab) { 3621 float penPos = PenLocation().x - fTextRect.left; 3622 float tabWidth = ActualTabWidth(penPos); 3623 if (numTabs > 1) 3624 tabWidth += ((numTabs - 1) * fTabWidth); 3625 3626 view->MovePenBy(tabWidth, 0.0); 3627 tabChars += numTabs; 3628 } 3629 3630 offset += tabChars; 3631 length -= tabChars; 3632 numBytes -= tabChars; 3633 tabChars = numBytes; 3634 numTabs = 0; 3635 } while (foundTab && tabChars > 0); 3636 } 3637 } 3638 } 3639 3640 3641 void 3642 BTextView::DrawLines(int32 startLine, int32 endLine, int32 startOffset, bool erase) 3643 { 3644 if (!Window()) 3645 return; 3646 3647 // clip the text 3648 BRect clipRect = Bounds() & fTextRect; 3649 clipRect.InsetBy(-1, -1); 3650 3651 BRegion newClip; 3652 newClip.Set(clipRect); 3653 ConstrainClippingRegion(&newClip); 3654 3655 // set the low color to the view color so that 3656 // drawing to a non-white background will work 3657 SetLowColor(ViewColor()); 3658 3659 BView *view = NULL; 3660 if (fOffscreen == NULL) 3661 view = this; 3662 else { 3663 fOffscreen->Lock(); 3664 view = fOffscreen->ChildAt(0); 3665 view->SetLowColor(ViewColor()); 3666 view->FillRect(view->Bounds(), B_SOLID_LOW); 3667 } 3668 3669 long maxLine = fLines->NumLines() - 1; 3670 if (startLine < 0) 3671 startLine = 0; 3672 if (endLine > maxLine) 3673 endLine = maxLine; 3674 3675 // TODO: See if we can avoid this 3676 if (fAlignment != B_ALIGN_LEFT) 3677 erase = true; 3678 3679 // Actually hide the caret 3680 if (fCaretVisible) 3681 DrawCaret(fSelStart); 3682 3683 BRect eraseRect = clipRect; 3684 int32 startEraseLine = startLine; 3685 STELine* line = (*fLines)[startLine]; 3686 if (erase && startOffset != -1 && fAlignment == B_ALIGN_LEFT) { 3687 // erase only to the right of startOffset 3688 startEraseLine++; 3689 long startErase = startOffset; 3690 3691 BPoint erasePoint = PointAt(startErase); 3692 eraseRect.left = erasePoint.x; 3693 eraseRect.top = erasePoint.y; 3694 eraseRect.bottom = (line + 1)->origin + fTextRect.top; 3695 3696 view->FillRect(eraseRect, B_SOLID_LOW); 3697 3698 eraseRect = clipRect; 3699 } 3700 3701 BRegion inputRegion; 3702 if (fInline != NULL && fInline->IsActive()) 3703 GetTextRegion(fInline->Offset(), fInline->Offset() + fInline->Length(), &inputRegion); 3704 3705 //BPoint leftTop(startLeft, line->origin); 3706 for (int32 lineNum = startLine; lineNum <= endLine; lineNum++) { 3707 const bool eraseThisLine = erase && lineNum >= startEraseLine; 3708 _DrawLine(view, lineNum, startOffset, eraseThisLine, eraseRect, inputRegion); 3709 startOffset = -1; 3710 // Set this to -1 so the next iteration will use the line offset 3711 } 3712 3713 // draw the caret/hilite the selection 3714 if (fActive) { 3715 if (fSelStart != fSelEnd && fSelectable) 3716 Highlight(fSelStart, fSelEnd); 3717 else { 3718 if (fCaretVisible) 3719 DrawCaret(fSelStart); 3720 } 3721 } 3722 3723 if (fOffscreen != NULL) { 3724 view->Sync(); 3725 /*BPoint penLocation = view->PenLocation(); 3726 BRect drawRect(leftTop.x, leftTop.y, penLocation.x, penLocation.y); 3727 DrawBitmap(fOffscreen, drawRect, drawRect);*/ 3728 fOffscreen->Unlock(); 3729 } 3730 3731 ConstrainClippingRegion(NULL); 3732 } 3733 3734 3735 void 3736 BTextView::DrawCaret(int32 offset) 3737 { 3738 float lineHeight; 3739 BPoint caretPoint = PointAt(offset, &lineHeight); 3740 caretPoint.x = min_c(caretPoint.x, fTextRect.right); 3741 3742 BRect caretRect; 3743 caretRect.left = caretRect.right = caretPoint.x; 3744 caretRect.top = caretPoint.y; 3745 caretRect.bottom = caretPoint.y + lineHeight - 1; 3746 3747 InvertRect(caretRect); 3748 } 3749 3750 3751 /*! \brief Inverts the blinking caret status. 3752 Hides the caret if it is being shown, and if it's hidden, shows it. 3753 */ 3754 void 3755 BTextView::InvertCaret() 3756 { 3757 DrawCaret(fSelStart); 3758 fCaretVisible = !fCaretVisible; 3759 fCaretTime = system_time(); 3760 } 3761 3762 3763 /*! \brief Place the dragging caret at the given offset. 3764 \param offset The offset (zero based within the object's text) where to place 3765 the dragging caret. If it's -1, hide the caret. 3766 */ 3767 void 3768 BTextView::DragCaret(int32 offset) 3769 { 3770 // does the caret need to move? 3771 if (offset == fDragOffset) 3772 return; 3773 3774 // hide the previous drag caret 3775 if (fDragOffset != -1) 3776 DrawCaret(fDragOffset); 3777 3778 // do we have a new location? 3779 if (offset != -1) { 3780 if (fActive) { 3781 // ignore if offset is within active selection 3782 if (offset >= fSelStart && offset <= fSelEnd) { 3783 fDragOffset = -1; 3784 return; 3785 } 3786 } 3787 3788 DrawCaret(offset); 3789 } 3790 3791 fDragOffset = offset; 3792 } 3793 3794 3795 void 3796 BTextView::StopMouseTracking() 3797 { 3798 delete fTrackingMouse; 3799 fTrackingMouse = NULL; 3800 } 3801 3802 3803 bool 3804 BTextView::PerformMouseUp(BPoint where) 3805 { 3806 if (fTrackingMouse == NULL) 3807 return false; 3808 3809 if (fTrackingMouse->selectionRect.IsValid() && fTrackingMouse->selectionRect.Contains(where)) 3810 Select(fTrackingMouse->clickOffset, fTrackingMouse->clickOffset); 3811 3812 StopMouseTracking(); 3813 3814 return true; 3815 } 3816 3817 3818 bool 3819 BTextView::PerformMouseMoved(BPoint where, uint32 code) 3820 { 3821 fWhere = where; 3822 3823 if (fTrackingMouse == NULL) 3824 return false; 3825 3826 if (fTrackingMouse->selectionRect.IsValid() && fTrackingMouse->selectionRect.Contains(where)) { 3827 StopMouseTracking(); 3828 InitiateDrag(); 3829 return true; 3830 } 3831 3832 int32 oldOffset = fTrackingMouse->anchor; 3833 int32 currentOffset = OffsetAt(where); 3834 3835 switch (fClickCount) { 3836 case 0: 3837 // triple click, select line by line 3838 fTrackingMouse->selStart = (*fLines)[LineAt(fTrackingMouse->selStart)]->offset; 3839 fTrackingMouse->selEnd = (*fLines)[LineAt(fTrackingMouse->selEnd) + 1]->offset; 3840 break; 3841 3842 case 2: 3843 // double click, select word by word 3844 FindWord(currentOffset, &fTrackingMouse->selStart, &fTrackingMouse->selEnd); 3845 break; 3846 3847 default: 3848 // new click, select char by char 3849 if (oldOffset < currentOffset) { 3850 fTrackingMouse->selStart = oldOffset; 3851 fTrackingMouse->selEnd = currentOffset; 3852 } else { 3853 fTrackingMouse->selStart = currentOffset; 3854 fTrackingMouse->selEnd = oldOffset; 3855 } 3856 break; 3857 } 3858 3859 Select(fTrackingMouse->selStart, fTrackingMouse->selEnd); 3860 3861 TrackMouse(where, NULL); 3862 3863 return true; 3864 } 3865 3866 3867 /*! \brief Tracks the mouse position, doing special actions like changing the view cursor. 3868 \param where The point where the mouse has moved. 3869 \param message The dragging message, if there is any. 3870 \param force Passed as second parameter of SetViewCursor() 3871 */ 3872 void 3873 BTextView::TrackMouse(BPoint where, const BMessage *message, bool force) 3874 { 3875 BRegion textRegion; 3876 GetTextRegion(fSelStart, fSelEnd, &textRegion); 3877 3878 if (message && AcceptsDrop(message)) 3879 TrackDrag(where); 3880 else if ((fSelectable || fEditable) && !textRegion.Contains(where)) 3881 SetViewCursor(B_CURSOR_I_BEAM, force); 3882 else 3883 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT, force); 3884 } 3885 3886 3887 /*! \brief Tracks the mouse position when the user is dragging some data. 3888 \param where The point where the mouse has moved. 3889 */ 3890 void 3891 BTextView::TrackDrag(BPoint where) 3892 { 3893 CALLED(); 3894 if (Bounds().Contains(where)) 3895 DragCaret(OffsetAt(where)); 3896 } 3897 3898 3899 /*! \brief Function called to initiate a drag operation. 3900 */ 3901 void 3902 BTextView::InitiateDrag() 3903 { 3904 BMessage *dragMessage = new BMessage(B_MIME_DATA); 3905 BBitmap *dragBitmap = NULL; 3906 BPoint bitmapPoint; 3907 BHandler *dragHandler = NULL; 3908 3909 GetDragParameters(dragMessage, &dragBitmap, &bitmapPoint, &dragHandler); 3910 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); 3911 3912 if (dragBitmap != NULL) 3913 DragMessage(dragMessage, dragBitmap, bitmapPoint, dragHandler); 3914 else { 3915 BRegion region; 3916 GetTextRegion(fSelStart, fSelEnd, ®ion); 3917 BRect bounds = Bounds(); 3918 BRect dragRect = region.Frame(); 3919 if (!bounds.Contains(dragRect)) 3920 dragRect = bounds & dragRect; 3921 3922 DragMessage(dragMessage, dragRect, dragHandler); 3923 } 3924 3925 BMessenger messenger(this); 3926 BMessage message(_DISPOSE_DRAG_); 3927 fDragRunner = new (nothrow) BMessageRunner(messenger, &message, 100000); 3928 } 3929 3930 3931 /*! \brief Called when some data is dropped on the view. 3932 \param inMessage The message which has been dropped. 3933 \param where The location where the message has been dropped. 3934 \param offset ? 3935 \return \c true if the message was handled, \c false if not. 3936 */ 3937 bool 3938 BTextView::MessageDropped(BMessage *inMessage, BPoint where, BPoint offset) 3939 { 3940 ASSERT(inMessage); 3941 3942 void *from = NULL; 3943 bool internalDrop = false; 3944 if (inMessage->FindPointer("be:originator", &from) == B_OK 3945 && from == this && fSelEnd != fSelStart) 3946 internalDrop = true; 3947 3948 DragCaret(-1); 3949 3950 delete fDragRunner; 3951 fDragRunner = NULL; 3952 3953 TrackMouse(where, NULL); 3954 3955 // are we sure we like this message? 3956 if (!AcceptsDrop(inMessage)) 3957 return false; 3958 3959 int32 dropOffset = OffsetAt(where); 3960 if (dropOffset > TextLength()) 3961 dropOffset = TextLength(); 3962 3963 // if this view initiated the drag, move instead of copy 3964 if (internalDrop) { 3965 // dropping onto itself? 3966 if (dropOffset >= fSelStart && dropOffset <= fSelEnd) 3967 return true; 3968 } 3969 3970 ssize_t dataLen = 0; 3971 const char *text = NULL; 3972 if (inMessage->FindData("text/plain", B_MIME_TYPE, (const void **)&text, &dataLen) == B_OK) { 3973 3974 text_run_array *runArray = NULL; 3975 ssize_t runLen = 0; 3976 if (fStylable) 3977 inMessage->FindData("application/x-vnd.Be-text_run_array", B_MIME_TYPE, 3978 (const void **)&runArray, &runLen); 3979 3980 if (fUndo) { 3981 delete fUndo; 3982 fUndo = new _BDropUndoBuffer_(this, text, dataLen, runArray, runLen, dropOffset, internalDrop); 3983 } 3984 3985 if (internalDrop) { 3986 if (dropOffset > fSelEnd) 3987 dropOffset -= dataLen; 3988 Delete(); 3989 } 3990 3991 Insert(dropOffset, text, dataLen, runArray); 3992 } 3993 3994 return true; 3995 } 3996 3997 3998 void 3999 BTextView::PerformAutoScrolling() 4000 { 4001 // Scroll the view a bit if mouse is outside the view bounds 4002 BRect bounds = Bounds(); 4003 BPoint scrollBy; 4004 4005 BPoint constraint = fWhere; 4006 constraint.ConstrainTo(bounds); 4007 // Scroll char by char horizontally 4008 // TODO: Check how BeOS R5 behaves 4009 float value = StyledWidthUTF8Safe(OffsetAt(constraint), 1); 4010 if (fWhere.x > bounds.right) { 4011 if (bounds.right + value <= fTextRect.Width()) 4012 scrollBy.x = value; 4013 } else if (fWhere.x < bounds.left) { 4014 if (bounds.left - value >= 0) 4015 scrollBy.x = -value; 4016 } 4017 4018 float lineHeight = 0; 4019 float vertDiff = 0; 4020 if (fWhere.y > bounds.bottom) { 4021 lineHeight = LineHeight(LineAt(bounds.LeftBottom())); 4022 vertDiff = fWhere.y - bounds.bottom; 4023 } else if (fWhere.y < bounds.top) { 4024 lineHeight = LineHeight(LineAt(bounds.LeftTop())); 4025 vertDiff = fWhere.y - bounds.top; // negative value 4026 } 4027 4028 // Always scroll vertically line by line or by multiples of that 4029 // based on the distance of the cursor from the border of the view 4030 // TODO: Refine this, I can't even remember how beos works here 4031 scrollBy.y = lineHeight > 0 ? lineHeight * (int32)(floorf(vertDiff) / lineHeight) : 0; 4032 4033 if (scrollBy != B_ORIGIN) 4034 ScrollBy(scrollBy.x, scrollBy.y); 4035 } 4036 4037 4038 /*! \brief Updates the scrollbars associated with the object (if any). 4039 */ 4040 void 4041 BTextView::UpdateScrollbars() 4042 { 4043 BRect bounds(Bounds()); 4044 BScrollBar *horizontalScrollBar = ScrollBar(B_HORIZONTAL); 4045 BScrollBar *verticalScrollBar = ScrollBar(B_VERTICAL); 4046 4047 // do we have a horizontal scroll bar? 4048 if (horizontalScrollBar != NULL) { 4049 long viewWidth = bounds.IntegerWidth(); 4050 long dataWidth = fTextRect.IntegerWidth(); 4051 dataWidth += (long)ceilf(fTextRect.left) + 1; 4052 4053 long maxRange = dataWidth - viewWidth; 4054 maxRange = max_c(maxRange, 0); 4055 4056 horizontalScrollBar->SetRange(0, (float)maxRange); 4057 horizontalScrollBar->SetProportion((float)viewWidth / (float)dataWidth); 4058 horizontalScrollBar->SetSteps(10, dataWidth / 10); 4059 } 4060 4061 // how about a vertical scroll bar? 4062 if (verticalScrollBar != NULL) { 4063 long viewHeight = bounds.IntegerHeight(); 4064 long dataHeight = fTextRect.IntegerHeight(); 4065 dataHeight += (long)ceilf(fTextRect.top) + 1; 4066 4067 long maxRange = dataHeight - viewHeight; 4068 maxRange = max_c(maxRange, 0); 4069 4070 verticalScrollBar->SetRange(0, maxRange); 4071 verticalScrollBar->SetProportion((float)viewHeight / (float)dataHeight); 4072 verticalScrollBar->SetSteps(12, viewHeight); 4073 } 4074 } 4075 4076 4077 /*! \brief Autoresizes the view to fit the contained text. 4078 */ 4079 void 4080 BTextView::AutoResize(bool redraw) 4081 { 4082 // TODO: Review this 4083 if (fResizable) { 4084 float oldWidth = Bounds().Width() + 1; 4085 float newWidth = 0; 4086 for (int32 i = 0; i < CountLines(); i++) 4087 newWidth += LineWidth(i); 4088 4089 if (newWidth < 3) 4090 newWidth = 3; 4091 BRect newRect(0, 0, ceilf(newWidth) + 1, ceilf(TextHeight(0, 0)) + 1); 4092 4093 if (fContainerView != NULL) { 4094 fContainerView->ResizeTo(newRect.Width(), newRect.Height()); 4095 if (fAlignment == B_ALIGN_CENTER) 4096 fContainerView->MoveBy((oldWidth - newRect.Width()) / 2, 0); 4097 else if (fAlignment == B_ALIGN_RIGHT) 4098 fContainerView->MoveBy(oldWidth - newRect.Width(), 0); 4099 fContainerView->Invalidate(); 4100 } 4101 4102 fTextRect = newRect.InsetBySelf(1, 1); 4103 4104 if (redraw) 4105 DrawLines(0, 0); 4106 4107 // Erase the old text (TODO: Might not work for alignments different than B_ALIGN_LEFT) 4108 SetLowColor(ViewColor()); 4109 FillRect(BRect(fTextRect.right, fTextRect.top, Bounds().right, fTextRect.bottom), B_SOLID_LOW); 4110 } 4111 } 4112 4113 4114 /*! \brief Creates a new offscreen BBitmap with an associated BView. 4115 param padding Padding (?) 4116 4117 Creates an offscreen BBitmap which will be used to draw. 4118 */ 4119 void 4120 BTextView::NewOffscreen(float padding) 4121 { 4122 if (fOffscreen != NULL) 4123 DeleteOffscreen(); 4124 4125 #if USE_DOUBLEBUFFERING 4126 BRect bitmapRect(0, 0, fTextRect.Width() + padding, fTextRect.Height()); 4127 fOffscreen = new BBitmap(bitmapRect, fColorSpace, true, false); 4128 if (fOffscreen != NULL && fOffscreen->Lock()) { 4129 BView *bufferView = new BView(bitmapRect, "drawing view", 0, 0); 4130 fOffscreen->AddChild(bufferView); 4131 fOffscreen->Unlock(); 4132 } 4133 #endif 4134 } 4135 4136 4137 /*! \brief Deletes the textview's offscreen bitmap, if any. 4138 */ 4139 void 4140 BTextView::DeleteOffscreen() 4141 { 4142 if (fOffscreen != NULL && fOffscreen->Lock()) { 4143 delete fOffscreen; 4144 fOffscreen = NULL; 4145 } 4146 } 4147 4148 4149 /*! \brief Creates a new offscreen bitmap, highlight the selection, and set the 4150 cursor to B_CURSOR_I_BEAM. 4151 */ 4152 void 4153 BTextView::Activate() 4154 { 4155 fActive = true; 4156 4157 // Create a new offscreen BBitmap 4158 NewOffscreen(); 4159 4160 if (fSelStart != fSelEnd) { 4161 if (fSelectable) 4162 Highlight(fSelStart, fSelEnd); 4163 } else { 4164 if (fEditable) 4165 InvertCaret(); 4166 } 4167 4168 BPoint where; 4169 ulong buttons; 4170 GetMouse(&where, &buttons, false); 4171 if (Bounds().Contains(where)) 4172 TrackMouse(where, NULL); 4173 } 4174 4175 4176 /*! \brief Unhilights the selection, set the cursor to B_CURSOR_SYSTEM_DEFAULT. 4177 */ 4178 void 4179 BTextView::Deactivate() 4180 { 4181 fActive = false; 4182 4183 CancelInputMethod(); 4184 DeleteOffscreen(); 4185 4186 if (fSelStart != fSelEnd) { 4187 if (fSelectable) 4188 Highlight(fSelStart, fSelEnd); 4189 } else { 4190 if (fCaretVisible) 4191 InvertCaret(); 4192 } 4193 4194 BPoint where; 4195 ulong buttons; 4196 GetMouse(&where, &buttons); 4197 if (Bounds().Contains(where)) 4198 SetViewCursor(B_CURSOR_SYSTEM_DEFAULT); 4199 } 4200 4201 4202 /*! \brief Changes the passed font to be displayable by the object. 4203 \param font A pointer to the font to normalize. 4204 4205 Set font rotation to 0, removes any font flag, set font spacing 4206 to \c B_BITMAP_SPACING and font encoding to \c B_UNICODE_UTF8 4207 */ 4208 void 4209 BTextView::NormalizeFont(BFont *font) 4210 { 4211 if (font) { 4212 font->SetRotation(0.0f); 4213 font->SetFlags(0); 4214 font->SetSpacing(B_BITMAP_SPACING); 4215 font->SetEncoding(B_UNICODE_UTF8); 4216 } 4217 } 4218 4219 4220 /*! \brief Returns a value which tells if the given character is a separator 4221 character or not. 4222 \param offset The offset where the wanted character can be found. 4223 \return A value which represents the character's classification. 4224 */ 4225 uint32 4226 BTextView::CharClassification(int32 offset) const 4227 { 4228 // TODO:Should check against a list of characters containing also 4229 // japanese word breakers. 4230 // And what about other languages ? Isn't there a better way to check 4231 // for separator characters ? 4232 // Andrew suggested to have a look at UnicodeBlockObject.h 4233 switch (fText->RealCharAt(offset)) { 4234 case B_SPACE: 4235 case '_': 4236 case '.': 4237 case '\0': 4238 case B_TAB: 4239 case B_ENTER: 4240 case '&': 4241 case '*': 4242 case '+': 4243 case '-': 4244 case '/': 4245 case '<': 4246 case '=': 4247 case '>': 4248 case '\\': 4249 case '^': 4250 case '|': 4251 return B_SEPARATOR_CHARACTER; 4252 default: 4253 return B_OTHER_CHARACTER; 4254 } 4255 } 4256 4257 4258 /*! \brief Returns the offset of the next UTF8 character within the BTextView's text. 4259 \param offset The offset where to start looking. 4260 \return The offset of the next UTF8 character. 4261 */ 4262 int32 4263 BTextView::NextInitialByte(int32 offset) const 4264 { 4265 int32 textLength = TextLength(); 4266 if (offset >= textLength) 4267 return textLength; 4268 4269 for (++offset; (ByteAt(offset) & 0xC0) == 0x80; ++offset) 4270 ; 4271 4272 return offset; 4273 } 4274 4275 4276 /*! \brief Returns the offset of the previous UTF8 character within the BTextView's text. 4277 \param offset The offset where to start looking. 4278 \return The offset of the previous UTF8 character. 4279 */ 4280 int32 4281 BTextView::PreviousInitialByte(int32 offset) const 4282 { 4283 if (offset <= 0) 4284 return 0; 4285 4286 int32 count = 6; 4287 4288 for (--offset; offset > 0 && count; --offset, --count) { 4289 if ((ByteAt(offset) & 0xC0) != 0x80) 4290 break; 4291 } 4292 4293 return count ? offset : 0; 4294 } 4295 4296 4297 bool 4298 BTextView::GetProperty(BMessage *specifier, int32 form, const char *property, BMessage *reply) 4299 { 4300 CALLED(); 4301 if (strcmp(property, "selection") == 0) { 4302 reply->what = B_REPLY; 4303 reply->AddInt32("result", fSelStart); 4304 reply->AddInt32("result", fSelEnd); 4305 reply->AddInt32("error", B_OK); 4306 4307 return true; 4308 4309 } else if (strcmp(property, "Text") == 0) { 4310 int32 index, range; 4311 4312 specifier->FindInt32("index", &index); 4313 specifier->FindInt32("range", &range); 4314 4315 char *buffer = new char[range + 1]; 4316 GetText(index, range, buffer); 4317 4318 reply->what = B_REPLY; 4319 reply->AddString("result", buffer); 4320 delete buffer; 4321 reply->AddInt32("error", B_OK); 4322 4323 return true; 4324 4325 } else if (strcmp(property, "text_run_array") == 0) 4326 return false; 4327 else 4328 return false; 4329 } 4330 4331 4332 bool 4333 BTextView::SetProperty(BMessage *specifier, int32 form, const char *property, BMessage *reply) 4334 { 4335 CALLED(); 4336 if (strcmp(property, "selection") == 0) { 4337 int32 index, range; 4338 4339 specifier->FindInt32("index", &index); 4340 specifier->FindInt32("range", &range); 4341 4342 Select(index, index + range); 4343 4344 reply->what = B_REPLY; 4345 reply->AddInt32("error", B_OK); 4346 4347 return true; 4348 4349 } else if (strcmp(property, "Text") == 0) { 4350 int32 index, range; 4351 4352 specifier->FindInt32("index", &index); 4353 specifier->FindInt32("range", &range); 4354 4355 const char *buffer = NULL; 4356 if (specifier->FindString("data", &buffer) == B_OK) 4357 InsertText(buffer, range, index, NULL); 4358 else 4359 DeleteText(index, range); 4360 4361 reply->what = B_REPLY; 4362 reply->AddInt32("error", B_OK); 4363 4364 return true; 4365 4366 } else if (strcmp(property, "text_run_array") == 0) 4367 return false; 4368 4369 else 4370 return false; 4371 } 4372 4373 4374 bool 4375 BTextView::CountProperties(BMessage *specifier, int32 form, const char *property, BMessage *reply) 4376 { 4377 CALLED(); 4378 if (strcmp(property, "Text") == 0) { 4379 reply->what = B_REPLY; 4380 reply->AddInt32("result", TextLength()); 4381 reply->AddInt32("error", B_OK); 4382 return true; 4383 } 4384 4385 return false; 4386 } 4387 4388 4389 /*! \brief Called when the object receives a B_INPUT_METHOD_CHANGED message. 4390 \param message A B_INPUT_METHOD_CHANGED message. 4391 */ 4392 void 4393 BTextView::HandleInputMethodChanged(BMessage *message) 4394 { 4395 // TODO: block input if not editable (Andrew) 4396 if (!fInline) 4397 return; 4398 4399 const char *string = NULL; 4400 if (message->FindString("be:string", &string) < B_OK || string == NULL) 4401 return; 4402 4403 be_app->ObscureCursor(); 4404 4405 // If we find the "be:confirmed" boolean (and the boolean is true), 4406 // it means it's over for now, so the current _BInlineInput_ object 4407 // should become inactive. We will probably receive a B_INPUT_METHOD_STOPPED 4408 // message after this one. 4409 bool confirmed; 4410 if (message->FindBool("be:confirmed", &confirmed) != B_OK) 4411 confirmed = false; 4412 4413 // Delete the previously inserted text (if any) 4414 if (fInline->IsActive()) { 4415 int32 oldOffset = fInline->Offset(); 4416 DeleteText(oldOffset, oldOffset + fInline->Length()); 4417 fClickOffset = fSelStart = fSelEnd = oldOffset; 4418 4419 if (confirmed) 4420 fInline->SetActive(false); 4421 } 4422 4423 int32 stringLen = strlen(string); 4424 4425 fInline->SetOffset(fSelStart); 4426 fInline->SetLength(stringLen); 4427 fInline->ResetClauses(); 4428 4429 // Get the clauses, and pass them to the _BInlineInput_ object 4430 // TODO: Find out if what we did it's ok, currently we don't consider clauses 4431 // at all, while the bebook says we should; though the visual effect we obtained 4432 // seems correct. Weird. 4433 int32 clauseCount = 0; 4434 int32 clauseStart; 4435 int32 clauseEnd; 4436 while (message->FindInt32("be:clause_start", clauseCount, &clauseStart) == B_OK && 4437 message->FindInt32("be:clause_end", clauseCount, &clauseEnd) == B_OK) { 4438 if (!fInline->AddClause(clauseStart, clauseEnd)) 4439 break; 4440 clauseCount++; 4441 } 4442 4443 int32 selectionStart = 0; 4444 int32 selectionEnd = 0; 4445 message->FindInt32("be:selection", 0, &selectionStart); 4446 message->FindInt32("be:selection", 1, &selectionEnd); 4447 4448 fInline->SetSelectionOffset(selectionStart); 4449 fInline->SetSelectionLength(selectionEnd - selectionStart); 4450 4451 // Insert the new text 4452 InsertText(string, stringLen, fSelStart, NULL); 4453 fSelStart += stringLen; 4454 fClickOffset = fSelEnd = fSelStart; 4455 4456 if (!confirmed && !fInline->IsActive()) 4457 fInline->SetActive(true); 4458 4459 Refresh(fInline->Offset(), fSelEnd, true, false); 4460 } 4461 4462 4463 /*! \brief Called when the object receives a B_INPUT_METHOD_LOCATION_REQUEST message. 4464 */ 4465 void 4466 BTextView::HandleInputMethodLocationRequest() 4467 { 4468 if (!fInline) 4469 return; 4470 4471 int32 offset = fInline->Offset(); 4472 const int32 limit = offset + fInline->Length(); 4473 4474 BMessage message(B_INPUT_METHOD_EVENT); 4475 message.AddInt32("be:opcode", B_INPUT_METHOD_LOCATION_REQUEST); 4476 4477 // Add the location of the UTF8 characters 4478 while (offset < limit) { 4479 float height; 4480 BPoint where = PointAt(offset, &height); 4481 ConvertToScreen(&where); 4482 4483 message.AddPoint("be:location_reply", where); 4484 message.AddFloat("be:height_reply", height); 4485 4486 offset = NextInitialByte(offset); 4487 } 4488 4489 fInline->Method()->SendMessage(&message); 4490 } 4491 4492 4493 /*! \brief Tells the input server method addon to stop the current transaction. 4494 */ 4495 void 4496 BTextView::CancelInputMethod() 4497 { 4498 if (!fInline) 4499 return; 4500 4501 BMessage message(B_INPUT_METHOD_EVENT); 4502 message.AddInt32("be:opcode", B_INPUT_METHOD_STOPPED); 4503 fInline->Method()->SendMessage(&message); 4504 4505 // Delete the previously inserted text (if any) 4506 if (fInline->IsActive()) { 4507 int32 oldOffset = fInline->Offset(); 4508 DeleteText(oldOffset, oldOffset + fInline->Length()); 4509 fClickOffset = fSelStart = fSelEnd = oldOffset; 4510 } 4511 4512 delete fInline; 4513 fInline = NULL; 4514 4515 if (Window()) 4516 Refresh(0, fText->Length(), true, false); 4517 } 4518 4519 4520 /*! \brief Locks the static _BWidthBuffer_ object to be able to access it safely. 4521 */ 4522 void 4523 BTextView::LockWidthBuffer() 4524 { 4525 if (atomic_add(&sWidthAtom, 1) > 0) { 4526 while (acquire_sem(sWidthSem) == B_INTERRUPTED) 4527 ; 4528 } 4529 } 4530 4531 4532 /*! \brief Unlocks the static _BWidthBuffer_ object. 4533 */ 4534 void 4535 BTextView::UnlockWidthBuffer() 4536 { 4537 if (atomic_add(&sWidthAtom, -1) > 1) 4538 release_sem(sWidthSem); 4539 } 4540