xref: /haiku/src/apps/haikudepot/textview/TextDocumentView.cpp (revision 9a6a20d4689307142a7ed26a1437ba47e244e73f)
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 	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 
53 TextDocumentView::~TextDocumentView()
54 {
55 	// Don't forget to remove listeners
56 	SetTextEditor(TextEditorRef());
57 	delete fCaretBlinker;
58 }
59 
60 
61 void
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
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
114 TextDocumentView::AttachedToWindow()
115 {
116 	_UpdateScrollBars();
117 }
118 
119 
120 void
121 TextDocumentView::FrameResized(float width, float height)
122 {
123 	fTextDocumentLayout.SetWidth(width);
124 	_UpdateScrollBars();
125 }
126 
127 
128 void
129 TextDocumentView::WindowActivated(bool active)
130 {
131 	Invalidate();
132 }
133 
134 
135 void
136 TextDocumentView::MakeFocus(bool focus)
137 {
138 	if (focus != IsFocus())
139 		Invalidate();
140 	BView::MakeFocus(focus);
141 }
142 
143 
144 void
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
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
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
248 TextDocumentView::KeyUp(const char* bytes, int32 numBytes)
249 {
250 }
251 
252 
253 BSize
254 TextDocumentView::MinSize()
255 {
256 	return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom);
257 }
258 
259 
260 BSize
261 TextDocumentView::MaxSize()
262 {
263 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
264 }
265 
266 
267 BSize
268 TextDocumentView::PreferredSize()
269 {
270 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
271 }
272 
273 
274 bool
275 TextDocumentView::HasHeightForWidth()
276 {
277 	return true;
278 }
279 
280 
281 void
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
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
317 TextDocumentView::SetEditingEnabled(bool enabled)
318 {
319 	if (fTextEditor.IsSet())
320 		fTextEditor->SetEditingEnabled(enabled);
321 }
322 
323 
324 void
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
348 TextDocumentView::SetInsets(float inset)
349 {
350 	SetInsets(inset, inset, inset, inset);
351 }
352 
353 
354 void
355 TextDocumentView::SetInsets(float horizontal, float vertical)
356 {
357 	SetInsets(horizontal, vertical, horizontal, vertical);
358 }
359 
360 
361 void
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
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
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
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
418 TextDocumentView::HasSelection() const
419 {
420 	return fTextEditor.IsSet() && fTextEditor->HasSelection();
421 }
422 
423 
424 void
425 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
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
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
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
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
523 TextDocumentView::Relayout()
524 {
525 	fTextDocumentLayout.Invalidate();
526 	_UpdateScrollBars();
527 }
528 
529 
530 // #pragma mark - private
531 
532 
533 float
534 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
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
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
599 TextDocumentView::_BlinkCaret()
600 {
601 	if (!fSelectionEnabled || !fTextEditor.IsSet())
602 		return;
603 
604 	_ShowCaret(!fShowCaret);
605 }
606 
607 
608 void
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
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
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
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
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 }