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 enum { 20 MSG_BLINK_CARET = 'blnk', 21 }; 22 23 24 TextDocumentView::TextDocumentView(const char* name) 25 : 26 BView(name, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS), 27 fInsetLeft(0.0f), 28 fInsetTop(0.0f), 29 fInsetRight(0.0f), 30 fInsetBottom(0.0f), 31 32 fCaretBounds(), 33 fCaretBlinker(NULL), 34 fCaretBlinkToken(0), 35 fSelectionEnabled(true), 36 fShowCaret(false), 37 fMouseDown(false) 38 { 39 fTextDocumentLayout.SetWidth(_TextLayoutWidth(Bounds().Width())); 40 41 // Set default TextEditor 42 SetTextEditor(TextEditorRef(new(std::nothrow) TextEditor(), true)); 43 44 SetViewUIColor(B_PANEL_BACKGROUND_COLOR); 45 SetLowUIColor(ViewUIColor()); 46 } 47 48 49 TextDocumentView::~TextDocumentView() 50 { 51 // Don't forget to remove listeners 52 SetTextEditor(TextEditorRef()); 53 delete fCaretBlinker; 54 } 55 56 57 void 58 TextDocumentView::MessageReceived(BMessage* message) 59 { 60 switch (message->what) { 61 case B_COPY: 62 Copy(be_clipboard); 63 break; 64 case B_SELECT_ALL: 65 SelectAll(); 66 break; 67 68 case MSG_BLINK_CARET: 69 { 70 int32 token; 71 if (message->FindInt32("token", &token) == B_OK 72 && token == fCaretBlinkToken) { 73 _BlinkCaret(); 74 } 75 break; 76 } 77 78 default: 79 BView::MessageReceived(message); 80 } 81 } 82 83 84 void 85 TextDocumentView::Draw(BRect updateRect) 86 { 87 FillRect(updateRect, B_SOLID_LOW); 88 89 fTextDocumentLayout.SetWidth(_TextLayoutWidth(Bounds().Width())); 90 fTextDocumentLayout.Draw(this, BPoint(fInsetLeft, fInsetTop), updateRect); 91 92 if (!fSelectionEnabled || !fTextEditor.IsSet()) 93 return; 94 95 bool isCaret = fTextEditor->SelectionLength() == 0; 96 97 if (isCaret) { 98 if (fShowCaret && fTextEditor->IsEditingEnabled()) 99 _DrawCaret(fTextEditor->CaretOffset()); 100 } else { 101 _DrawSelection(); 102 } 103 } 104 105 106 void 107 TextDocumentView::AttachedToWindow() 108 { 109 _UpdateScrollBars(); 110 } 111 112 113 void 114 TextDocumentView::FrameResized(float width, float height) 115 { 116 fTextDocumentLayout.SetWidth(width); 117 _UpdateScrollBars(); 118 } 119 120 121 void 122 TextDocumentView::WindowActivated(bool active) 123 { 124 Invalidate(); 125 } 126 127 128 void 129 TextDocumentView::MakeFocus(bool focus) 130 { 131 if (focus != IsFocus()) 132 Invalidate(); 133 BView::MakeFocus(focus); 134 } 135 136 137 void 138 TextDocumentView::MouseDown(BPoint where) 139 { 140 BMessage* currentMessage = NULL; 141 if (Window() != NULL) 142 currentMessage = Window()->CurrentMessage(); 143 144 // First of all, check for links and other clickable things 145 bool unused; 146 int32 offset = fTextDocumentLayout.TextOffsetAt(where.x, where.y, unused); 147 const BMessage* message = fTextDocument->ClickMessageAt(offset); 148 if (message != NULL) { 149 BMessage clickMessage(*message); 150 clickMessage.Append(*currentMessage); 151 Invoke(&clickMessage); 152 } 153 154 if (!fSelectionEnabled) 155 return; 156 157 MakeFocus(); 158 159 int32 modifiers = 0; 160 if (currentMessage != NULL) 161 currentMessage->FindInt32("modifiers", &modifiers); 162 163 fMouseDown = true; 164 SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS); 165 166 bool extendSelection = (modifiers & B_SHIFT_KEY) != 0; 167 SetCaret(where, extendSelection); 168 } 169 170 171 void 172 TextDocumentView::MouseUp(BPoint where) 173 { 174 fMouseDown = false; 175 } 176 177 178 void 179 TextDocumentView::MouseMoved(BPoint where, uint32 transit, 180 const BMessage* dragMessage) 181 { 182 BCursor cursor(B_CURSOR_ID_I_BEAM); 183 184 if (transit != B_EXITED_VIEW) { 185 bool unused; 186 int32 offset = fTextDocumentLayout.TextOffsetAt(where.x, where.y, unused); 187 const BCursor& newCursor = fTextDocument->CursorAt(offset); 188 if (newCursor.InitCheck() == B_OK) { 189 cursor = newCursor; 190 SetViewCursor(&cursor); 191 } 192 } 193 194 if (!fSelectionEnabled) 195 return; 196 197 SetViewCursor(&cursor); 198 199 if (fMouseDown) 200 SetCaret(where, true); 201 } 202 203 204 void 205 TextDocumentView::KeyDown(const char* bytes, int32 numBytes) 206 { 207 if (!fTextEditor.IsSet()) 208 return; 209 210 KeyEvent event; 211 event.bytes = bytes; 212 event.length = numBytes; 213 event.key = 0; 214 event.modifiers = modifiers(); 215 216 if (Window() != NULL && Window()->CurrentMessage() != NULL) { 217 BMessage* message = Window()->CurrentMessage(); 218 message->FindInt32("raw_char", &event.key); 219 message->FindInt32("modifiers", &event.modifiers); 220 } 221 222 fTextEditor->KeyDown(event); 223 _ShowCaret(true); 224 // TODO: It is necessary to invalidate all, since neither the caret bounds 225 // are updated in a way that would work here, nor is the text updated 226 // correcty which has been edited. 227 Invalidate(); 228 } 229 230 231 void 232 TextDocumentView::KeyUp(const char* bytes, int32 numBytes) 233 { 234 } 235 236 237 BSize 238 TextDocumentView::MinSize() 239 { 240 return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom); 241 } 242 243 244 BSize 245 TextDocumentView::MaxSize() 246 { 247 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED); 248 } 249 250 251 BSize 252 TextDocumentView::PreferredSize() 253 { 254 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED); 255 } 256 257 258 bool 259 TextDocumentView::HasHeightForWidth() 260 { 261 return true; 262 } 263 264 265 void 266 TextDocumentView::GetHeightForWidth(float width, float* min, float* max, 267 float* preferred) 268 { 269 TextDocumentLayout layout(fTextDocumentLayout); 270 layout.SetWidth(_TextLayoutWidth(width)); 271 272 float height = layout.Height() + 1 + fInsetTop + fInsetBottom; 273 274 if (min != NULL) 275 *min = height; 276 if (max != NULL) 277 *max = height; 278 if (preferred != NULL) 279 *preferred = height; 280 } 281 282 283 // #pragma mark - 284 285 286 void 287 TextDocumentView::SetTextDocument(const TextDocumentRef& document) 288 { 289 fTextDocument = document; 290 fTextDocumentLayout.SetTextDocument(fTextDocument); 291 if (fTextEditor.IsSet()) 292 fTextEditor->SetDocument(document); 293 294 InvalidateLayout(); 295 Invalidate(); 296 _UpdateScrollBars(); 297 } 298 299 300 void 301 TextDocumentView::SetEditingEnabled(bool enabled) 302 { 303 if (fTextEditor.IsSet()) 304 fTextEditor->SetEditingEnabled(enabled); 305 } 306 307 308 void 309 TextDocumentView::SetTextEditor(const TextEditorRef& editor) 310 { 311 if (fTextEditor == editor) 312 return; 313 314 if (fTextEditor.IsSet()) { 315 fTextEditor->SetDocument(TextDocumentRef()); 316 fTextEditor->SetLayout(TextDocumentLayoutRef()); 317 // TODO: Probably has to remove listeners 318 } 319 320 fTextEditor = editor; 321 322 if (fTextEditor.IsSet()) { 323 fTextEditor->SetDocument(fTextDocument); 324 fTextEditor->SetLayout(TextDocumentLayoutRef( 325 &fTextDocumentLayout)); 326 // TODO: Probably has to add listeners 327 } 328 } 329 330 331 void 332 TextDocumentView::SetInsets(float inset) 333 { 334 SetInsets(inset, inset, inset, inset); 335 } 336 337 338 void 339 TextDocumentView::SetInsets(float horizontal, float vertical) 340 { 341 SetInsets(horizontal, vertical, horizontal, vertical); 342 } 343 344 345 void 346 TextDocumentView::SetInsets(float left, float top, float right, float bottom) 347 { 348 if (fInsetLeft == left && fInsetTop == top 349 && fInsetRight == right && fInsetBottom == bottom) { 350 return; 351 } 352 353 fInsetLeft = left; 354 fInsetTop = top; 355 fInsetRight = right; 356 fInsetBottom = bottom; 357 358 InvalidateLayout(); 359 Invalidate(); 360 } 361 362 363 void 364 TextDocumentView::SetSelectionEnabled(bool enabled) 365 { 366 if (fSelectionEnabled == enabled) 367 return; 368 fSelectionEnabled = enabled; 369 Invalidate(); 370 // TODO: Deselect 371 } 372 373 374 void 375 TextDocumentView::SetCaret(BPoint location, bool extendSelection) 376 { 377 if (!fSelectionEnabled || !fTextEditor.IsSet()) 378 return; 379 380 location.x -= fInsetLeft; 381 location.y -= fInsetTop; 382 383 fTextEditor->SetCaret(location, extendSelection); 384 _ShowCaret(!extendSelection); 385 Invalidate(); 386 } 387 388 389 void 390 TextDocumentView::SelectAll() 391 { 392 if (!fSelectionEnabled || !fTextEditor.IsSet()) 393 return; 394 395 fTextEditor->SelectAll(); 396 _ShowCaret(false); 397 Invalidate(); 398 } 399 400 401 bool 402 TextDocumentView::HasSelection() const 403 { 404 return fTextEditor.IsSet() && fTextEditor->HasSelection(); 405 } 406 407 408 void 409 TextDocumentView::GetSelection(int32& start, int32& end) const 410 { 411 if (fTextEditor.IsSet()) { 412 start = fTextEditor->SelectionStart(); 413 end = fTextEditor->SelectionEnd(); 414 } 415 } 416 417 418 void 419 TextDocumentView::Copy(BClipboard* clipboard) 420 { 421 if (!HasSelection() || !fTextDocument.IsSet()) { 422 // Nothing to copy, don't clear clipboard contents for now reason. 423 return; 424 } 425 426 if (clipboard == NULL || !clipboard->Lock()) 427 return; 428 429 clipboard->Clear(); 430 431 BMessage* clip = clipboard->Data(); 432 if (clip != NULL) { 433 int32 start; 434 int32 end; 435 GetSelection(start, end); 436 437 BString text = fTextDocument->Text(start, end - start); 438 clip->AddData("text/plain", B_MIME_TYPE, text.String(), 439 text.Length()); 440 441 // TODO: Support for "application/x-vnd.Be-text_run_array" 442 443 clipboard->Commit(); 444 } 445 446 clipboard->Unlock(); 447 } 448 449 450 void 451 TextDocumentView::Relayout() 452 { 453 fTextDocumentLayout.Invalidate(); 454 _UpdateScrollBars(); 455 } 456 457 458 // #pragma mark - private 459 460 461 float 462 TextDocumentView::_TextLayoutWidth(float viewWidth) const 463 { 464 return viewWidth - (fInsetLeft + fInsetRight); 465 } 466 467 468 static const float kHorizontalScrollBarStep = 10.0f; 469 static const float kVerticalScrollBarStep = 12.0f; 470 471 472 void 473 TextDocumentView::_UpdateScrollBars() 474 { 475 BRect bounds(Bounds()); 476 477 BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL); 478 if (horizontalScrollBar != NULL) { 479 long viewWidth = bounds.IntegerWidth(); 480 long dataWidth = (long)ceilf( 481 fTextDocumentLayout.Width() + fInsetLeft + fInsetRight); 482 483 long maxRange = dataWidth - viewWidth; 484 maxRange = std::max(maxRange, 0L); 485 486 horizontalScrollBar->SetRange(0, (float)maxRange); 487 horizontalScrollBar->SetProportion((float)viewWidth / dataWidth); 488 horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10); 489 } 490 491 BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL); 492 if (verticalScrollBar != NULL) { 493 long viewHeight = bounds.IntegerHeight(); 494 long dataHeight = (long)ceilf( 495 fTextDocumentLayout.Height() + fInsetTop + fInsetBottom); 496 497 long maxRange = dataHeight - viewHeight; 498 maxRange = std::max(maxRange, 0L); 499 500 verticalScrollBar->SetRange(0, maxRange); 501 verticalScrollBar->SetProportion((float)viewHeight / dataHeight); 502 verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight); 503 } 504 } 505 506 507 void 508 TextDocumentView::_ShowCaret(bool show) 509 { 510 fShowCaret = show; 511 if (fCaretBounds.IsValid()) 512 Invalidate(fCaretBounds); 513 else 514 Invalidate(); 515 // Cancel previous blinker, increment blink token so we only accept 516 // the message from the blinker we just created 517 fCaretBlinkToken++; 518 BMessage message(MSG_BLINK_CARET); 519 message.AddInt32("token", fCaretBlinkToken); 520 delete fCaretBlinker; 521 fCaretBlinker = new BMessageRunner(BMessenger(this), &message, 522 500000, 1); 523 } 524 525 526 void 527 TextDocumentView::_BlinkCaret() 528 { 529 if (!fSelectionEnabled || !fTextEditor.IsSet()) 530 return; 531 532 _ShowCaret(!fShowCaret); 533 } 534 535 536 void 537 TextDocumentView::_DrawCaret(int32 textOffset) 538 { 539 if (!IsFocus() || Window() == NULL || !Window()->IsActive()) 540 return; 541 542 float x1; 543 float y1; 544 float x2; 545 float y2; 546 547 fTextDocumentLayout.GetTextBounds(textOffset, x1, y1, x2, y2); 548 x2 = x1 + 1; 549 550 fCaretBounds = BRect(x1, y1, x2, y2); 551 fCaretBounds.OffsetBy(fInsetLeft, fInsetTop); 552 553 SetDrawingMode(B_OP_INVERT); 554 FillRect(fCaretBounds); 555 } 556 557 558 void 559 TextDocumentView::_DrawSelection() 560 { 561 int32 start; 562 int32 end; 563 GetSelection(start, end); 564 565 BShape shape; 566 _GetSelectionShape(shape, start, end); 567 568 SetDrawingMode(B_OP_SUBTRACT); 569 570 SetLineMode(B_ROUND_CAP, B_ROUND_JOIN); 571 MovePenTo(fInsetLeft - 0.5f, fInsetTop - 0.5f); 572 573 if (IsFocus() && Window() != NULL && Window()->IsActive()) { 574 SetHighColor(30, 30, 30); 575 FillShape(&shape); 576 } 577 578 SetHighColor(40, 40, 40); 579 StrokeShape(&shape); 580 } 581 582 583 void 584 TextDocumentView::_GetSelectionShape(BShape& shape, int32 start, int32 end) 585 { 586 float startX1; 587 float startY1; 588 float startX2; 589 float startY2; 590 fTextDocumentLayout.GetTextBounds(start, startX1, startY1, startX2, 591 startY2); 592 593 startX1 = floorf(startX1); 594 startY1 = floorf(startY1); 595 startX2 = ceilf(startX2); 596 startY2 = ceilf(startY2); 597 598 float endX1; 599 float endY1; 600 float endX2; 601 float endY2; 602 fTextDocumentLayout.GetTextBounds(end, endX1, endY1, endX2, endY2); 603 604 endX1 = floorf(endX1); 605 endY1 = floorf(endY1); 606 endX2 = ceilf(endX2); 607 endY2 = ceilf(endY2); 608 609 int32 startLineIndex = fTextDocumentLayout.LineIndexForOffset(start); 610 int32 endLineIndex = fTextDocumentLayout.LineIndexForOffset(end); 611 612 if (startLineIndex == endLineIndex) { 613 // Selection on one line 614 BPoint lt(startX1, startY1); 615 BPoint rt(endX1, endY1); 616 BPoint rb(endX1, endY2); 617 BPoint lb(startX1, startY2); 618 619 shape.MoveTo(lt); 620 shape.LineTo(rt); 621 shape.LineTo(rb); 622 shape.LineTo(lb); 623 shape.Close(); 624 } else if (startLineIndex == endLineIndex - 1 && endX1 <= startX1) { 625 // Selection on two lines, with gap: 626 // --------- 627 // ------### 628 // ##------- 629 // --------- 630 float width = ceilf(fTextDocumentLayout.Width()); 631 632 BPoint lt(startX1, startY1); 633 BPoint rt(width, startY1); 634 BPoint rb(width, startY2); 635 BPoint lb(startX1, startY2); 636 637 shape.MoveTo(lt); 638 shape.LineTo(rt); 639 shape.LineTo(rb); 640 shape.LineTo(lb); 641 shape.Close(); 642 643 lt = BPoint(0, endY1); 644 rt = BPoint(endX1, endY1); 645 rb = BPoint(endX1, endY2); 646 lb = BPoint(0, endY2); 647 648 shape.MoveTo(lt); 649 shape.LineTo(rt); 650 shape.LineTo(rb); 651 shape.LineTo(lb); 652 shape.Close(); 653 } else { 654 // Selection over multiple lines 655 float width = ceilf(fTextDocumentLayout.Width()); 656 657 shape.MoveTo(BPoint(startX1, startY1)); 658 shape.LineTo(BPoint(width, startY1)); 659 shape.LineTo(BPoint(width, endY1)); 660 shape.LineTo(BPoint(endX1, endY1)); 661 shape.LineTo(BPoint(endX1, endY2)); 662 shape.LineTo(BPoint(0, endY2)); 663 shape.LineTo(BPoint(0, startY2)); 664 shape.LineTo(BPoint(startX1, startY2)); 665 shape.Close(); 666 } 667 } 668 669 670