1 /* 2 * Copyright 2013-2015, Stephan Aßmus <superstippi@gmx.de>. 3 * All rights reserved. Distributed under the terms of the MIT License. 4 */ 5 6 #include "TextDocumentView.h" 7 8 #include <algorithm> 9 #include <stdio.h> 10 11 #include <Clipboard.h> 12 #include <Cursor.h> 13 #include <MessageRunner.h> 14 #include <ScrollBar.h> 15 #include <Shape.h> 16 #include <Window.h> 17 18 19 const char* kMimeTypePlainText = "text/plain"; 20 21 22 enum { 23 MSG_BLINK_CARET = 'blnk', 24 }; 25 26 27 TextDocumentView::TextDocumentView(const char* name) 28 : 29 BView(name, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS), 30 fTextDocument(NULL), 31 fTextEditor(NULL), 32 fInsetLeft(0.0f), 33 fInsetTop(0.0f), 34 fInsetRight(0.0f), 35 fInsetBottom(0.0f), 36 37 fCaretBounds(), 38 fCaretBlinker(NULL), 39 fCaretBlinkToken(0), 40 fSelectionEnabled(true), 41 fShowCaret(false) 42 { 43 fTextDocumentLayout.SetWidth(_TextLayoutWidth(Bounds().Width())); 44 45 // Set default TextEditor 46 SetTextEditor(TextEditorRef(new(std::nothrow) TextEditor(), true)); 47 48 SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 49 SetLowUIColor(ViewUIColor()); 50 } 51 52 53 TextDocumentView::~TextDocumentView() 54 { 55 // Don't forget to remove listeners 56 SetTextEditor(TextEditorRef()); 57 delete fCaretBlinker; 58 } 59 60 61 void 62 TextDocumentView::MessageReceived(BMessage* message) 63 { 64 switch (message->what) { 65 case B_COPY: 66 Copy(be_clipboard); 67 break; 68 case B_PASTE: 69 Paste(be_clipboard); 70 break; 71 case B_SELECT_ALL: 72 SelectAll(); 73 break; 74 75 case MSG_BLINK_CARET: 76 { 77 int32 token; 78 if (message->FindInt32("token", &token) == B_OK 79 && token == fCaretBlinkToken) { 80 _BlinkCaret(); 81 } 82 break; 83 } 84 85 default: 86 BView::MessageReceived(message); 87 } 88 } 89 90 91 void 92 TextDocumentView::Draw(BRect updateRect) 93 { 94 FillRect(updateRect, B_SOLID_LOW); 95 96 fTextDocumentLayout.SetWidth(_TextLayoutWidth(Bounds().Width())); 97 fTextDocumentLayout.Draw(this, BPoint(fInsetLeft, fInsetTop), updateRect); 98 99 if (!fSelectionEnabled || !fTextEditor.IsSet()) 100 return; 101 102 bool isCaret = fTextEditor->SelectionLength() == 0; 103 104 if (isCaret) { 105 if (fShowCaret && fTextEditor->IsEditingEnabled()) 106 _DrawCaret(fTextEditor->CaretOffset()); 107 } else { 108 _DrawSelection(); 109 } 110 } 111 112 113 void 114 TextDocumentView::AttachedToWindow() 115 { 116 _UpdateScrollBars(); 117 } 118 119 120 void 121 TextDocumentView::FrameResized(float width, float height) 122 { 123 fTextDocumentLayout.SetWidth(width); 124 _UpdateScrollBars(); 125 } 126 127 128 void 129 TextDocumentView::WindowActivated(bool active) 130 { 131 Invalidate(); 132 } 133 134 135 void 136 TextDocumentView::MakeFocus(bool focus) 137 { 138 if (focus != IsFocus()) 139 Invalidate(); 140 BView::MakeFocus(focus); 141 } 142 143 144 void 145 TextDocumentView::MouseDown(BPoint where) 146 { 147 if (!fTextEditor.IsSet() || !fTextDocument.IsSet()) 148 return BView::MouseDown(where); 149 150 BMessage* currentMessage = NULL; 151 if (Window() != NULL) 152 currentMessage = Window()->CurrentMessage(); 153 154 // First of all, check for links and other clickable things 155 bool unused; 156 int32 offset = fTextDocumentLayout.TextOffsetAt(where.x, where.y, unused); 157 const BMessage* message = fTextDocument->ClickMessageAt(offset); 158 if (message != NULL) { 159 BMessage clickMessage(*message); 160 clickMessage.Append(*currentMessage); 161 Invoke(&clickMessage); 162 } 163 164 if (!fSelectionEnabled) 165 return; 166 167 MakeFocus(); 168 169 int32 modifiers = 0; 170 if (currentMessage != NULL) 171 currentMessage->FindInt32("modifiers", &modifiers); 172 173 SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS); 174 175 bool extendSelection = (modifiers & B_SHIFT_KEY) != 0; 176 SetCaret(where, extendSelection); 177 178 BView::MouseDown(where); 179 } 180 181 182 void 183 TextDocumentView::MouseMoved(BPoint where, uint32 transit, const BMessage* dragMessage) 184 { 185 if (!fTextEditor.IsSet() || !fTextDocument.IsSet()) 186 return BView::MouseMoved(where, transit, dragMessage); 187 188 BCursor cursor(B_CURSOR_ID_I_BEAM); 189 190 if (transit != B_EXITED_VIEW) { 191 bool unused; 192 int32 offset = fTextDocumentLayout.TextOffsetAt(where.x, where.y, unused); 193 const BCursor& newCursor = fTextDocument->CursorAt(offset); 194 if (newCursor.InitCheck() == B_OK) { 195 cursor = newCursor; 196 SetViewCursor(&cursor); 197 } 198 } 199 200 if (!fSelectionEnabled) 201 return; 202 203 SetViewCursor(&cursor); 204 205 uint32 buttons = 0; 206 if (Window() != NULL) 207 Window()->CurrentMessage()->FindInt32("buttons", (int32*)&buttons); 208 if (buttons > 0) 209 SetCaret(where, true); 210 211 BView::MouseMoved(where, transit, dragMessage); 212 } 213 214 215 void 216 TextDocumentView::KeyDown(const char* bytes, int32 numBytes) 217 { 218 if (!fTextEditor.IsSet()) 219 return; 220 221 KeyEvent event; 222 event.bytes = bytes; 223 event.length = numBytes; 224 event.key = 0; 225 event.modifiers = modifiers(); 226 227 if (Window() != NULL && Window()->CurrentMessage() != NULL) { 228 BMessage* message = Window()->CurrentMessage(); 229 message->FindInt32("raw_char", &event.key); 230 message->FindInt32("modifiers", &event.modifiers); 231 } 232 233 float viewHeightPrior = fTextEditor->Layout()->Height(); 234 235 fTextEditor->KeyDown(event); 236 _ShowCaret(true); 237 // TODO: It is necessary to invalidate all, since neither the caret bounds 238 // are updated in a way that would work here, nor is the text updated 239 // correctly which has been edited. 240 Invalidate(); 241 242 if (fTextEditor->Layout()->Height() != viewHeightPrior) 243 _UpdateScrollBars(); 244 } 245 246 247 void 248 TextDocumentView::KeyUp(const char* bytes, int32 numBytes) 249 { 250 } 251 252 253 BSize 254 TextDocumentView::MinSize() 255 { 256 return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom); 257 } 258 259 260 BSize 261 TextDocumentView::MaxSize() 262 { 263 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED); 264 } 265 266 267 BSize 268 TextDocumentView::PreferredSize() 269 { 270 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED); 271 } 272 273 274 bool 275 TextDocumentView::HasHeightForWidth() 276 { 277 return true; 278 } 279 280 281 void 282 TextDocumentView::GetHeightForWidth(float width, float* min, float* max, 283 float* preferred) 284 { 285 TextDocumentLayout layout(fTextDocumentLayout); 286 layout.SetWidth(_TextLayoutWidth(width)); 287 288 float height = layout.Height() + 1 + fInsetTop + fInsetBottom; 289 290 if (min != NULL) 291 *min = height; 292 if (max != NULL) 293 *max = height; 294 if (preferred != NULL) 295 *preferred = height; 296 } 297 298 299 // #pragma mark - 300 301 302 void 303 TextDocumentView::SetTextDocument(const TextDocumentRef& document) 304 { 305 fTextDocument = document; 306 fTextDocumentLayout.SetTextDocument(fTextDocument); 307 if (fTextEditor.IsSet()) 308 fTextEditor->SetDocument(document); 309 310 InvalidateLayout(); 311 Invalidate(); 312 _UpdateScrollBars(); 313 } 314 315 316 void 317 TextDocumentView::SetEditingEnabled(bool enabled) 318 { 319 if (fTextEditor.IsSet()) 320 fTextEditor->SetEditingEnabled(enabled); 321 } 322 323 324 void 325 TextDocumentView::SetTextEditor(const TextEditorRef& editor) 326 { 327 if (fTextEditor == editor) 328 return; 329 330 if (fTextEditor.IsSet()) { 331 fTextEditor->SetDocument(TextDocumentRef()); 332 fTextEditor->SetLayout(TextDocumentLayoutRef()); 333 // TODO: Probably has to remove listeners 334 } 335 336 fTextEditor = editor; 337 338 if (fTextEditor.IsSet()) { 339 fTextEditor->SetDocument(fTextDocument); 340 fTextEditor->SetLayout(TextDocumentLayoutRef( 341 &fTextDocumentLayout)); 342 // TODO: Probably has to add listeners 343 } 344 } 345 346 347 void 348 TextDocumentView::SetInsets(float inset) 349 { 350 SetInsets(inset, inset, inset, inset); 351 } 352 353 354 void 355 TextDocumentView::SetInsets(float horizontal, float vertical) 356 { 357 SetInsets(horizontal, vertical, horizontal, vertical); 358 } 359 360 361 void 362 TextDocumentView::SetInsets(float left, float top, float right, float bottom) 363 { 364 if (fInsetLeft == left && fInsetTop == top 365 && fInsetRight == right && fInsetBottom == bottom) { 366 return; 367 } 368 369 fInsetLeft = left; 370 fInsetTop = top; 371 fInsetRight = right; 372 fInsetBottom = bottom; 373 374 InvalidateLayout(); 375 Invalidate(); 376 } 377 378 379 void 380 TextDocumentView::SetSelectionEnabled(bool enabled) 381 { 382 if (fSelectionEnabled == enabled) 383 return; 384 fSelectionEnabled = enabled; 385 Invalidate(); 386 // TODO: Deselect 387 } 388 389 390 void 391 TextDocumentView::SetCaret(BPoint location, bool extendSelection) 392 { 393 if (!fSelectionEnabled || !fTextEditor.IsSet()) 394 return; 395 396 location.x -= fInsetLeft; 397 location.y -= fInsetTop; 398 399 fTextEditor->SetCaret(location, extendSelection); 400 _ShowCaret(!extendSelection); 401 Invalidate(); 402 } 403 404 405 void 406 TextDocumentView::SelectAll() 407 { 408 if (!fSelectionEnabled || !fTextEditor.IsSet()) 409 return; 410 411 fTextEditor->SelectAll(); 412 _ShowCaret(false); 413 Invalidate(); 414 } 415 416 417 bool 418 TextDocumentView::HasSelection() const 419 { 420 return fTextEditor.IsSet() && fTextEditor->HasSelection(); 421 } 422 423 424 void 425 TextDocumentView::GetSelection(int32& start, int32& end) const 426 { 427 if (fTextEditor.IsSet()) { 428 start = fTextEditor->SelectionStart(); 429 end = fTextEditor->SelectionEnd(); 430 } 431 } 432 433 434 void 435 TextDocumentView::Paste(BClipboard* clipboard) 436 { 437 if (!fTextDocument.IsSet() || !fTextEditor.IsSet()) 438 return; 439 440 if (!clipboard->Lock()) 441 return; 442 443 BMessage* clip = clipboard->Data(); 444 445 if (clip != NULL) { 446 const void* plainTextData; 447 ssize_t plainTextDataSize; 448 449 if (clip->FindData(kMimeTypePlainText, B_MIME_TYPE, &plainTextData, &plainTextDataSize) 450 == B_OK) { 451 452 if (plainTextDataSize > 0) { 453 if (_PastePossiblyDisallowedChars(static_cast<const char*>(plainTextData), 454 static_cast<int32>(plainTextDataSize)) != B_OK) { 455 fprintf(stderr, "unable to paste text owing to internal error"); 456 // don't use HaikuDepot logging system as this is in the text engine 457 } 458 } 459 } 460 } 461 462 clipboard->Unlock(); 463 } 464 465 466 /*! This method will check that all of the characters in the provided 467 string are allowed in the text document. Returns true if this is the case. 468 */ 469 /*static*/ bool 470 TextDocumentView::_AreCharsAllowed(const char* str, int32 maxLength) 471 { 472 for (int32 i = 0; str[i] != 0 && i < maxLength; i++) { 473 if (!TextDocumentView::_IsAllowedChar(i)) 474 return false; 475 } 476 return true; 477 } 478 479 480 /*static*/ bool 481 TextDocumentView::_IsAllowedChar(char c) 482 { 483 return c >= ' ' 484 || c == '\t' 485 || c == '\n' 486 || c == 127 // delete 487 ; 488 } 489 490 491 void 492 TextDocumentView::Copy(BClipboard* clipboard) 493 { 494 if (!HasSelection() || !fTextDocument.IsSet()) { 495 // Nothing to copy, don't clear clipboard contents for now reason. 496 return; 497 } 498 499 if (clipboard == NULL || !clipboard->Lock()) 500 return; 501 502 clipboard->Clear(); 503 504 BMessage* clip = clipboard->Data(); 505 if (clip != NULL) { 506 int32 start; 507 int32 end; 508 GetSelection(start, end); 509 510 BString text = fTextDocument->Text(start, end - start); 511 clip->AddData(kMimeTypePlainText, B_MIME_TYPE, text.String(), text.Length()); 512 513 // TODO: Support for "application/x-vnd.Be-text_run_array" 514 515 clipboard->Commit(); 516 } 517 518 clipboard->Unlock(); 519 } 520 521 522 void 523 TextDocumentView::Relayout() 524 { 525 fTextDocumentLayout.Invalidate(); 526 _UpdateScrollBars(); 527 } 528 529 530 // #pragma mark - private 531 532 533 float 534 TextDocumentView::_TextLayoutWidth(float viewWidth) const 535 { 536 return viewWidth - (fInsetLeft + fInsetRight); 537 } 538 539 540 static const float kHorizontalScrollBarStep = 10.0f; 541 static const float kVerticalScrollBarStep = 12.0f; 542 543 544 void 545 TextDocumentView::_UpdateScrollBars() 546 { 547 BRect bounds(Bounds()); 548 549 BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL); 550 if (horizontalScrollBar != NULL) { 551 long viewWidth = bounds.IntegerWidth(); 552 long dataWidth = (long)ceilf( 553 fTextDocumentLayout.Width() + fInsetLeft + fInsetRight); 554 555 long maxRange = dataWidth - viewWidth; 556 maxRange = std::max(maxRange, 0L); 557 558 horizontalScrollBar->SetRange(0, (float)maxRange); 559 horizontalScrollBar->SetProportion((float)viewWidth / dataWidth); 560 horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10); 561 } 562 563 BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL); 564 if (verticalScrollBar != NULL) { 565 long viewHeight = bounds.IntegerHeight(); 566 long dataHeight = (long)ceilf( 567 fTextDocumentLayout.Height() + fInsetTop + fInsetBottom); 568 569 long maxRange = dataHeight - viewHeight; 570 maxRange = std::max(maxRange, 0L); 571 572 verticalScrollBar->SetRange(0, maxRange); 573 verticalScrollBar->SetProportion((float)viewHeight / dataHeight); 574 verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight); 575 } 576 } 577 578 579 void 580 TextDocumentView::_ShowCaret(bool show) 581 { 582 fShowCaret = show; 583 if (fCaretBounds.IsValid()) 584 Invalidate(fCaretBounds); 585 else 586 Invalidate(); 587 // Cancel previous blinker, increment blink token so we only accept 588 // the message from the blinker we just created 589 fCaretBlinkToken++; 590 BMessage message(MSG_BLINK_CARET); 591 message.AddInt32("token", fCaretBlinkToken); 592 delete fCaretBlinker; 593 fCaretBlinker = new BMessageRunner(BMessenger(this), &message, 594 500000, 1); 595 } 596 597 598 void 599 TextDocumentView::_BlinkCaret() 600 { 601 if (!fSelectionEnabled || !fTextEditor.IsSet()) 602 return; 603 604 _ShowCaret(!fShowCaret); 605 } 606 607 608 void 609 TextDocumentView::_DrawCaret(int32 textOffset) 610 { 611 if (!IsFocus() || Window() == NULL || !Window()->IsActive()) 612 return; 613 614 float x1; 615 float y1; 616 float x2; 617 float y2; 618 619 fTextDocumentLayout.GetTextBounds(textOffset, x1, y1, x2, y2); 620 x2 = x1 + 1; 621 622 fCaretBounds = BRect(x1, y1, x2, y2); 623 fCaretBounds.OffsetBy(fInsetLeft, fInsetTop); 624 625 SetDrawingMode(B_OP_INVERT); 626 FillRect(fCaretBounds); 627 } 628 629 630 void 631 TextDocumentView::_DrawSelection() 632 { 633 int32 start; 634 int32 end; 635 GetSelection(start, end); 636 637 BShape shape; 638 _GetSelectionShape(shape, start, end); 639 640 SetDrawingMode(B_OP_SUBTRACT); 641 642 SetLineMode(B_ROUND_CAP, B_ROUND_JOIN); 643 MovePenTo(fInsetLeft - 0.5f, fInsetTop - 0.5f); 644 645 if (IsFocus() && Window() != NULL && Window()->IsActive()) { 646 SetHighColor(30, 30, 30); 647 FillShape(&shape); 648 } 649 650 SetHighColor(40, 40, 40); 651 StrokeShape(&shape); 652 } 653 654 655 void 656 TextDocumentView::_GetSelectionShape(BShape& shape, int32 start, int32 end) 657 { 658 float startX1; 659 float startY1; 660 float startX2; 661 float startY2; 662 fTextDocumentLayout.GetTextBounds(start, startX1, startY1, startX2, 663 startY2); 664 665 startX1 = floorf(startX1); 666 startY1 = floorf(startY1); 667 startX2 = ceilf(startX2); 668 startY2 = ceilf(startY2); 669 670 float endX1; 671 float endY1; 672 float endX2; 673 float endY2; 674 fTextDocumentLayout.GetTextBounds(end, endX1, endY1, endX2, endY2); 675 676 endX1 = floorf(endX1); 677 endY1 = floorf(endY1); 678 endX2 = ceilf(endX2); 679 endY2 = ceilf(endY2); 680 681 int32 startLineIndex = fTextDocumentLayout.LineIndexForOffset(start); 682 int32 endLineIndex = fTextDocumentLayout.LineIndexForOffset(end); 683 684 if (startLineIndex == endLineIndex) { 685 // Selection on one line 686 BPoint lt(startX1, startY1); 687 BPoint rt(endX1, endY1); 688 BPoint rb(endX1, endY2); 689 BPoint lb(startX1, startY2); 690 691 shape.MoveTo(lt); 692 shape.LineTo(rt); 693 shape.LineTo(rb); 694 shape.LineTo(lb); 695 shape.Close(); 696 } else if (startLineIndex == endLineIndex - 1 && endX1 <= startX1) { 697 // Selection on two lines, with gap: 698 // --------- 699 // ------### 700 // ##------- 701 // --------- 702 float width = ceilf(fTextDocumentLayout.Width()); 703 704 BPoint lt(startX1, startY1); 705 BPoint rt(width, startY1); 706 BPoint rb(width, startY2); 707 BPoint lb(startX1, startY2); 708 709 shape.MoveTo(lt); 710 shape.LineTo(rt); 711 shape.LineTo(rb); 712 shape.LineTo(lb); 713 shape.Close(); 714 715 lt = BPoint(0, endY1); 716 rt = BPoint(endX1, endY1); 717 rb = BPoint(endX1, endY2); 718 lb = BPoint(0, endY2); 719 720 shape.MoveTo(lt); 721 shape.LineTo(rt); 722 shape.LineTo(rb); 723 shape.LineTo(lb); 724 shape.Close(); 725 } else { 726 // Selection over multiple lines 727 float width = ceilf(fTextDocumentLayout.Width()); 728 729 shape.MoveTo(BPoint(startX1, startY1)); 730 shape.LineTo(BPoint(width, startY1)); 731 shape.LineTo(BPoint(width, endY1)); 732 shape.LineTo(BPoint(endX1, endY1)); 733 shape.LineTo(BPoint(endX1, endY2)); 734 shape.LineTo(BPoint(0, endY2)); 735 shape.LineTo(BPoint(0, startY2)); 736 shape.LineTo(BPoint(startX1, startY2)); 737 shape.Close(); 738 } 739 } 740 741 742 /*! The data provided in the `str` parameter may contain characters that are 743 not allowed. This method should filter those out and then apply them to 744 the text body. 745 */ 746 status_t 747 TextDocumentView::_PastePossiblyDisallowedChars(const char* str, int32 maxLength) 748 { 749 if (maxLength <= 0) 750 return B_OK; 751 752 if (TextDocumentView::_AreCharsAllowed(str, maxLength)) { 753 _PasteAllowedChars(str, maxLength); 754 } else { 755 char* strFiltered = new(std::nothrow) char[maxLength]; 756 757 if (strFiltered == NULL) 758 return B_NO_MEMORY; 759 760 int32 strFilteredLength = 0; 761 762 for (int i = 0; str[i] != '\0' && i < maxLength; i++) { 763 if (_IsAllowedChar(str[i])) { 764 strFiltered[strFilteredLength] = str[i]; 765 strFilteredLength++; 766 } 767 } 768 769 strFiltered[strFilteredLength] = '\0'; 770 _PasteAllowedChars(strFiltered, strFilteredLength); 771 772 delete[] strFiltered; 773 } 774 775 return B_OK; 776 } 777 778 779 /*! Here the data in `str` should be clean of control characters. 780 */ 781 void 782 TextDocumentView::_PasteAllowedChars(const char* str, int32 maxLength) 783 { 784 BString plainText(str, maxLength); 785 786 if (plainText.IsEmpty()) 787 return; 788 789 if (fTextEditor.IsSet()) { 790 if (fTextEditor->HasSelection()) { 791 int32 start = fTextEditor->SelectionStart(); 792 int32 end = fTextEditor->SelectionEnd(); 793 fTextEditor->Replace(start, end - start, plainText); 794 Invalidate(); 795 _UpdateScrollBars(); 796 } else { 797 int32 caretOffset = fTextEditor->CaretOffset(); 798 if (caretOffset >= 0) { 799 fTextEditor->Insert(caretOffset, plainText); 800 Invalidate(); 801 _UpdateScrollBars(); 802 } 803 } 804 } 805 }