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