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.Get() == NULL) 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 if (!fSelectionEnabled) 141 return; 142 143 MakeFocus(); 144 145 int32 modifiers = 0; 146 if (Window() != NULL && Window()->CurrentMessage() != NULL) 147 Window()->CurrentMessage()->FindInt32("modifiers", &modifiers); 148 149 fMouseDown = true; 150 SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS); 151 152 bool extendSelection = (modifiers & B_SHIFT_KEY) != 0; 153 SetCaret(where, extendSelection); 154 } 155 156 157 void 158 TextDocumentView::MouseUp(BPoint where) 159 { 160 fMouseDown = false; 161 } 162 163 164 void 165 TextDocumentView::MouseMoved(BPoint where, uint32 transit, 166 const BMessage* dragMessage) 167 { 168 if (!fSelectionEnabled) 169 return; 170 171 BCursor iBeamCursor(B_CURSOR_ID_I_BEAM); 172 SetViewCursor(&iBeamCursor); 173 174 if (fMouseDown) 175 SetCaret(where, true); 176 } 177 178 179 void 180 TextDocumentView::KeyDown(const char* bytes, int32 numBytes) 181 { 182 if (fTextEditor.Get() == NULL) 183 return; 184 185 KeyEvent event; 186 event.bytes = bytes; 187 event.length = numBytes; 188 event.key = 0; 189 event.modifiers = modifiers(); 190 191 if (Window() != NULL && Window()->CurrentMessage() != NULL) { 192 BMessage* message = Window()->CurrentMessage(); 193 message->FindInt32("raw_char", &event.key); 194 message->FindInt32("modifiers", &event.modifiers); 195 } 196 197 fTextEditor->KeyDown(event); 198 _ShowCaret(true); 199 // TODO: It is necessary to invalidate all, since neither the caret bounds 200 // are updated in a way that would work here, nor is the text updated 201 // correcty which has been edited. 202 Invalidate(); 203 } 204 205 206 void 207 TextDocumentView::KeyUp(const char* bytes, int32 numBytes) 208 { 209 } 210 211 212 BSize 213 TextDocumentView::MinSize() 214 { 215 return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom); 216 } 217 218 219 BSize 220 TextDocumentView::MaxSize() 221 { 222 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED); 223 } 224 225 226 BSize 227 TextDocumentView::PreferredSize() 228 { 229 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED); 230 } 231 232 233 bool 234 TextDocumentView::HasHeightForWidth() 235 { 236 return true; 237 } 238 239 240 void 241 TextDocumentView::GetHeightForWidth(float width, float* min, float* max, 242 float* preferred) 243 { 244 TextDocumentLayout layout(fTextDocumentLayout); 245 layout.SetWidth(_TextLayoutWidth(width)); 246 247 float height = layout.Height() + 1 + fInsetTop + fInsetBottom; 248 249 if (min != NULL) 250 *min = height; 251 if (max != NULL) 252 *max = height; 253 if (preferred != NULL) 254 *preferred = height; 255 } 256 257 258 // #pragma mark - 259 260 261 void 262 TextDocumentView::SetTextDocument(const TextDocumentRef& document) 263 { 264 fTextDocument = document; 265 fTextDocumentLayout.SetTextDocument(fTextDocument); 266 if (fTextEditor.Get() != NULL) 267 fTextEditor->SetDocument(document); 268 269 InvalidateLayout(); 270 Invalidate(); 271 _UpdateScrollBars(); 272 } 273 274 275 void 276 TextDocumentView::SetEditingEnabled(bool enabled) 277 { 278 if (fTextEditor.Get() != NULL) 279 fTextEditor->SetEditingEnabled(enabled); 280 } 281 282 283 void 284 TextDocumentView::SetTextEditor(const TextEditorRef& editor) 285 { 286 if (fTextEditor == editor) 287 return; 288 289 if (fTextEditor.Get() != NULL) { 290 fTextEditor->SetDocument(TextDocumentRef()); 291 fTextEditor->SetLayout(TextDocumentLayoutRef()); 292 // TODO: Probably has to remove listeners 293 } 294 295 fTextEditor = editor; 296 297 if (fTextEditor.Get() != NULL) { 298 fTextEditor->SetDocument(fTextDocument); 299 fTextEditor->SetLayout(TextDocumentLayoutRef( 300 &fTextDocumentLayout)); 301 // TODO: Probably has to add listeners 302 } 303 } 304 305 306 void 307 TextDocumentView::SetInsets(float inset) 308 { 309 SetInsets(inset, inset, inset, inset); 310 } 311 312 313 void 314 TextDocumentView::SetInsets(float horizontal, float vertical) 315 { 316 SetInsets(horizontal, vertical, horizontal, vertical); 317 } 318 319 320 void 321 TextDocumentView::SetInsets(float left, float top, float right, float bottom) 322 { 323 if (fInsetLeft == left && fInsetTop == top 324 && fInsetRight == right && fInsetBottom == bottom) { 325 return; 326 } 327 328 fInsetLeft = left; 329 fInsetTop = top; 330 fInsetRight = right; 331 fInsetBottom = bottom; 332 333 InvalidateLayout(); 334 Invalidate(); 335 } 336 337 338 void 339 TextDocumentView::SetSelectionEnabled(bool enabled) 340 { 341 if (fSelectionEnabled == enabled) 342 return; 343 fSelectionEnabled = enabled; 344 Invalidate(); 345 // TODO: Deselect 346 } 347 348 349 void 350 TextDocumentView::SetCaret(BPoint location, bool extendSelection) 351 { 352 if (!fSelectionEnabled || fTextEditor.Get() == NULL) 353 return; 354 355 location.x -= fInsetLeft; 356 location.y -= fInsetTop; 357 358 fTextEditor->SetCaret(location, extendSelection); 359 _ShowCaret(!extendSelection); 360 Invalidate(); 361 } 362 363 364 void 365 TextDocumentView::SelectAll() 366 { 367 if (!fSelectionEnabled || fTextEditor.Get() == NULL) 368 return; 369 370 fTextEditor->SelectAll(); 371 _ShowCaret(false); 372 Invalidate(); 373 } 374 375 376 bool 377 TextDocumentView::HasSelection() const 378 { 379 return fTextEditor.Get() != NULL && fTextEditor->HasSelection(); 380 } 381 382 383 void 384 TextDocumentView::GetSelection(int32& start, int32& end) const 385 { 386 if (fTextEditor.Get() != NULL) { 387 start = fTextEditor->SelectionStart(); 388 end = fTextEditor->SelectionEnd(); 389 } 390 } 391 392 393 void 394 TextDocumentView::Copy(BClipboard* clipboard) 395 { 396 if (!HasSelection() || fTextDocument.Get() == NULL) { 397 // Nothing to copy, don't clear clipboard contents for now reason. 398 return; 399 } 400 401 if (clipboard == NULL || !clipboard->Lock()) 402 return; 403 404 clipboard->Clear(); 405 406 BMessage* clip = clipboard->Data(); 407 if (clip != NULL) { 408 int32 start; 409 int32 end; 410 GetSelection(start, end); 411 412 BString text = fTextDocument->Text(start, end - start); 413 clip->AddData("text/plain", B_MIME_TYPE, text.String(), 414 text.Length()); 415 416 // TODO: Support for "application/x-vnd.Be-text_run_array" 417 418 clipboard->Commit(); 419 } 420 421 clipboard->Unlock(); 422 } 423 424 425 // #pragma mark - private 426 427 428 float 429 TextDocumentView::_TextLayoutWidth(float viewWidth) const 430 { 431 return viewWidth - (fInsetLeft + fInsetRight); 432 } 433 434 435 static const float kHorizontalScrollBarStep = 10.0f; 436 static const float kVerticalScrollBarStep = 12.0f; 437 438 439 void 440 TextDocumentView::_UpdateScrollBars() 441 { 442 BRect bounds(Bounds()); 443 444 BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL); 445 if (horizontalScrollBar != NULL) { 446 long viewWidth = bounds.IntegerWidth(); 447 long dataWidth = (long)ceilf( 448 fTextDocumentLayout.Width() + fInsetLeft + fInsetRight); 449 450 long maxRange = dataWidth - viewWidth; 451 maxRange = std::max(maxRange, 0L); 452 453 horizontalScrollBar->SetRange(0, (float)maxRange); 454 horizontalScrollBar->SetProportion((float)viewWidth / dataWidth); 455 horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10); 456 } 457 458 BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL); 459 if (verticalScrollBar != NULL) { 460 long viewHeight = bounds.IntegerHeight(); 461 long dataHeight = (long)ceilf( 462 fTextDocumentLayout.Height() + fInsetTop + fInsetBottom); 463 464 long maxRange = dataHeight - viewHeight; 465 maxRange = std::max(maxRange, 0L); 466 467 verticalScrollBar->SetRange(0, maxRange); 468 verticalScrollBar->SetProportion((float)viewHeight / dataHeight); 469 verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight); 470 } 471 } 472 473 474 void 475 TextDocumentView::_ShowCaret(bool show) 476 { 477 fShowCaret = show; 478 if (fCaretBounds.IsValid()) 479 Invalidate(fCaretBounds); 480 else 481 Invalidate(); 482 // Cancel previous blinker, increment blink token so we only accept 483 // the message from the blinker we just created 484 fCaretBlinkToken++; 485 BMessage message(MSG_BLINK_CARET); 486 message.AddInt32("token", fCaretBlinkToken); 487 delete fCaretBlinker; 488 fCaretBlinker = new BMessageRunner(BMessenger(this), &message, 489 500000, 1); 490 } 491 492 493 void 494 TextDocumentView::_BlinkCaret() 495 { 496 if (!fSelectionEnabled || fTextEditor.Get() == NULL) 497 return; 498 499 _ShowCaret(!fShowCaret); 500 } 501 502 503 void 504 TextDocumentView::_DrawCaret(int32 textOffset) 505 { 506 if (!IsFocus() || Window() == NULL || !Window()->IsActive()) 507 return; 508 509 float x1; 510 float y1; 511 float x2; 512 float y2; 513 514 fTextDocumentLayout.GetTextBounds(textOffset, x1, y1, x2, y2); 515 x2 = x1 + 1; 516 517 fCaretBounds = BRect(x1, y1, x2, y2); 518 fCaretBounds.OffsetBy(fInsetLeft, fInsetTop); 519 520 SetDrawingMode(B_OP_INVERT); 521 FillRect(fCaretBounds); 522 } 523 524 525 void 526 TextDocumentView::_DrawSelection() 527 { 528 int32 start; 529 int32 end; 530 GetSelection(start, end); 531 532 BShape shape; 533 _GetSelectionShape(shape, start, end); 534 535 SetDrawingMode(B_OP_SUBTRACT); 536 537 SetLineMode(B_ROUND_CAP, B_ROUND_JOIN); 538 MovePenTo(fInsetLeft - 0.5f, fInsetTop - 0.5f); 539 540 if (IsFocus() && Window() != NULL && Window()->IsActive()) { 541 SetHighColor(30, 30, 30); 542 FillShape(&shape); 543 } 544 545 SetHighColor(40, 40, 40); 546 StrokeShape(&shape); 547 } 548 549 550 void 551 TextDocumentView::_GetSelectionShape(BShape& shape, int32 start, int32 end) 552 { 553 float startX1; 554 float startY1; 555 float startX2; 556 float startY2; 557 fTextDocumentLayout.GetTextBounds(start, startX1, startY1, startX2, 558 startY2); 559 560 startX1 = floorf(startX1); 561 startY1 = floorf(startY1); 562 startX2 = ceilf(startX2); 563 startY2 = ceilf(startY2); 564 565 float endX1; 566 float endY1; 567 float endX2; 568 float endY2; 569 fTextDocumentLayout.GetTextBounds(end, endX1, endY1, endX2, endY2); 570 571 endX1 = floorf(endX1); 572 endY1 = floorf(endY1); 573 endX2 = ceilf(endX2); 574 endY2 = ceilf(endY2); 575 576 int32 startLineIndex = fTextDocumentLayout.LineIndexForOffset(start); 577 int32 endLineIndex = fTextDocumentLayout.LineIndexForOffset(end); 578 579 if (startLineIndex == endLineIndex) { 580 // Selection on one line 581 BPoint lt(startX1, startY1); 582 BPoint rt(endX1, endY1); 583 BPoint rb(endX1, endY2); 584 BPoint lb(startX1, startY2); 585 586 shape.MoveTo(lt); 587 shape.LineTo(rt); 588 shape.LineTo(rb); 589 shape.LineTo(lb); 590 shape.Close(); 591 } else if (startLineIndex == endLineIndex - 1 && endX1 <= startX1) { 592 // Selection on two lines, with gap: 593 // --------- 594 // ------### 595 // ##------- 596 // --------- 597 float width = ceilf(fTextDocumentLayout.Width()); 598 599 BPoint lt(startX1, startY1); 600 BPoint rt(width, startY1); 601 BPoint rb(width, startY2); 602 BPoint lb(startX1, startY2); 603 604 shape.MoveTo(lt); 605 shape.LineTo(rt); 606 shape.LineTo(rb); 607 shape.LineTo(lb); 608 shape.Close(); 609 610 lt = BPoint(0, endY1); 611 rt = BPoint(endX1, endY1); 612 rb = BPoint(endX1, endY2); 613 lb = BPoint(0, endY2); 614 615 shape.MoveTo(lt); 616 shape.LineTo(rt); 617 shape.LineTo(rb); 618 shape.LineTo(lb); 619 shape.Close(); 620 } else { 621 // Selection over multiple lines 622 float width = ceilf(fTextDocumentLayout.Width()); 623 624 shape.MoveTo(BPoint(startX1, startY1)); 625 shape.LineTo(BPoint(width, startY1)); 626 shape.LineTo(BPoint(width, endY1)); 627 shape.LineTo(BPoint(endX1, endY1)); 628 shape.LineTo(BPoint(endX1, endY2)); 629 shape.LineTo(BPoint(0, endY2)); 630 shape.LineTo(BPoint(0, startY2)); 631 shape.LineTo(BPoint(startX1, startY2)); 632 shape.Close(); 633 } 634 } 635 636 637