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
TextDocumentView(const char * name)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
~TextDocumentView()53 TextDocumentView::~TextDocumentView()
54 {
55 // Don't forget to remove listeners
56 SetTextEditor(TextEditorRef());
57 delete fCaretBlinker;
58 }
59
60
61 void
MessageReceived(BMessage * message)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
Draw(BRect updateRect)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
AttachedToWindow()114 TextDocumentView::AttachedToWindow()
115 {
116 _UpdateScrollBars();
117 }
118
119
120 void
FrameResized(float width,float height)121 TextDocumentView::FrameResized(float width, float height)
122 {
123 fTextDocumentLayout.SetWidth(width);
124 _UpdateScrollBars();
125 }
126
127
128 void
WindowActivated(bool active)129 TextDocumentView::WindowActivated(bool active)
130 {
131 Invalidate();
132 }
133
134
135 void
MakeFocus(bool focus)136 TextDocumentView::MakeFocus(bool focus)
137 {
138 if (focus != IsFocus())
139 Invalidate();
140 BView::MakeFocus(focus);
141 }
142
143
144 void
MouseDown(BPoint where)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
MouseMoved(BPoint where,uint32 transit,const BMessage * dragMessage)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
KeyDown(const char * bytes,int32 numBytes)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
KeyUp(const char * bytes,int32 numBytes)248 TextDocumentView::KeyUp(const char* bytes, int32 numBytes)
249 {
250 }
251
252
253 BSize
MinSize()254 TextDocumentView::MinSize()
255 {
256 return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom);
257 }
258
259
260 BSize
MaxSize()261 TextDocumentView::MaxSize()
262 {
263 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
264 }
265
266
267 BSize
PreferredSize()268 TextDocumentView::PreferredSize()
269 {
270 return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
271 }
272
273
274 bool
HasHeightForWidth()275 TextDocumentView::HasHeightForWidth()
276 {
277 return true;
278 }
279
280
281 void
GetHeightForWidth(float width,float * min,float * max,float * preferred)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
SetTextDocument(const TextDocumentRef & document)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
SetEditingEnabled(bool enabled)317 TextDocumentView::SetEditingEnabled(bool enabled)
318 {
319 if (fTextEditor.IsSet())
320 fTextEditor->SetEditingEnabled(enabled);
321 }
322
323
324 void
SetTextEditor(const TextEditorRef & editor)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
SetInsets(float inset)348 TextDocumentView::SetInsets(float inset)
349 {
350 SetInsets(inset, inset, inset, inset);
351 }
352
353
354 void
SetInsets(float horizontal,float vertical)355 TextDocumentView::SetInsets(float horizontal, float vertical)
356 {
357 SetInsets(horizontal, vertical, horizontal, vertical);
358 }
359
360
361 void
SetInsets(float left,float top,float right,float bottom)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
SetSelectionEnabled(bool enabled)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
SetCaret(BPoint location,bool extendSelection)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
SelectAll()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
HasSelection() const418 TextDocumentView::HasSelection() const
419 {
420 return fTextEditor.IsSet() && fTextEditor->HasSelection();
421 }
422
423
424 void
GetSelection(int32 & start,int32 & end) const425 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
Paste(BClipboard * clipboard)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
_AreCharsAllowed(const char * str,int32 maxLength)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
_IsAllowedChar(char c)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
Copy(BClipboard * clipboard)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
Relayout()523 TextDocumentView::Relayout()
524 {
525 fTextDocumentLayout.Invalidate();
526 _UpdateScrollBars();
527 }
528
529
530 // #pragma mark - private
531
532
533 float
_TextLayoutWidth(float viewWidth) const534 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
_UpdateScrollBars()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
_ShowCaret(bool show)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
_BlinkCaret()599 TextDocumentView::_BlinkCaret()
600 {
601 if (!fSelectionEnabled || !fTextEditor.IsSet())
602 return;
603
604 _ShowCaret(!fShowCaret);
605 }
606
607
608 void
_DrawCaret(int32 textOffset)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
_DrawSelection()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
_GetSelectionShape(BShape & shape,int32 start,int32 end)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
_PastePossiblyDisallowedChars(const char * str,int32 maxLength)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
_PasteAllowedChars(const char * str,int32 maxLength)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 }