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