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