xref: /haiku/src/apps/haikudepot/textview/TextDocumentView.cpp (revision b8a45b3a2df2379b4301bf3bd5949b9a105be4ba)
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 	fInsetLeft(0.0f),
31 	fInsetTop(0.0f),
32 	fInsetRight(0.0f),
33 	fInsetBottom(0.0f),
34 
35 	fCaretBounds(),
36 	fCaretBlinker(NULL),
37 	fCaretBlinkToken(0),
38 	fSelectionEnabled(true),
39 	fShowCaret(false),
40 	fMouseDown(false)
41 {
42 	fTextDocumentLayout.SetWidth(_TextLayoutWidth(Bounds().Width()));
43 
44 	// Set default TextEditor
45 	SetTextEditor(TextEditorRef(new(std::nothrow) TextEditor(), true));
46 
47 	SetViewUIColor(B_PANEL_BACKGROUND_COLOR);
48 	SetLowUIColor(ViewUIColor());
49 }
50 
51 
52 TextDocumentView::~TextDocumentView()
53 {
54 	// Don't forget to remove listeners
55 	SetTextEditor(TextEditorRef());
56 	delete fCaretBlinker;
57 }
58 
59 
60 void
61 TextDocumentView::MessageReceived(BMessage* message)
62 {
63 	switch (message->what) {
64 		case B_COPY:
65 			Copy(be_clipboard);
66 			break;
67 		case B_PASTE:
68 			Paste(be_clipboard);
69 			break;
70 		case B_SELECT_ALL:
71 			SelectAll();
72 			break;
73 
74 		case MSG_BLINK_CARET:
75 		{
76 			int32 token;
77 			if (message->FindInt32("token", &token) == B_OK
78 				&& token == fCaretBlinkToken) {
79 				_BlinkCaret();
80 			}
81 			break;
82 		}
83 
84 		default:
85 			BView::MessageReceived(message);
86 	}
87 }
88 
89 
90 void
91 TextDocumentView::Draw(BRect updateRect)
92 {
93 	FillRect(updateRect, B_SOLID_LOW);
94 
95 	fTextDocumentLayout.SetWidth(_TextLayoutWidth(Bounds().Width()));
96 	fTextDocumentLayout.Draw(this, BPoint(fInsetLeft, fInsetTop), updateRect);
97 
98 	if (!fSelectionEnabled || !fTextEditor.IsSet())
99 		return;
100 
101 	bool isCaret = fTextEditor->SelectionLength() == 0;
102 
103 	if (isCaret) {
104 		if (fShowCaret && fTextEditor->IsEditingEnabled())
105 			_DrawCaret(fTextEditor->CaretOffset());
106 	} else {
107 		_DrawSelection();
108 	}
109 }
110 
111 
112 void
113 TextDocumentView::AttachedToWindow()
114 {
115 	_UpdateScrollBars();
116 }
117 
118 
119 void
120 TextDocumentView::FrameResized(float width, float height)
121 {
122 	fTextDocumentLayout.SetWidth(width);
123 	_UpdateScrollBars();
124 }
125 
126 
127 void
128 TextDocumentView::WindowActivated(bool active)
129 {
130 	Invalidate();
131 }
132 
133 
134 void
135 TextDocumentView::MakeFocus(bool focus)
136 {
137 	if (focus != IsFocus())
138 		Invalidate();
139 	BView::MakeFocus(focus);
140 }
141 
142 
143 void
144 TextDocumentView::MouseDown(BPoint where)
145 {
146 	BMessage* currentMessage = NULL;
147 	if (Window() != NULL)
148 		currentMessage = Window()->CurrentMessage();
149 
150 	// First of all, check for links and other clickable things
151 	bool unused;
152 	int32 offset = fTextDocumentLayout.TextOffsetAt(where.x, where.y, unused);
153 	const BMessage* message = fTextDocument->ClickMessageAt(offset);
154 	if (message != NULL) {
155 		BMessage clickMessage(*message);
156 		clickMessage.Append(*currentMessage);
157 		Invoke(&clickMessage);
158 	}
159 
160 	if (!fSelectionEnabled)
161 		return;
162 
163 	MakeFocus();
164 
165 	int32 modifiers = 0;
166 	if (currentMessage != NULL)
167 		currentMessage->FindInt32("modifiers", &modifiers);
168 
169 	fMouseDown = true;
170 	SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
171 
172 	bool extendSelection = (modifiers & B_SHIFT_KEY) != 0;
173 	SetCaret(where, extendSelection);
174 }
175 
176 
177 void
178 TextDocumentView::MouseUp(BPoint where)
179 {
180 	fMouseDown = false;
181 }
182 
183 
184 void
185 TextDocumentView::MouseMoved(BPoint where, uint32 transit,
186 	const BMessage* 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 	if (fMouseDown)
206 		SetCaret(where, true);
207 }
208 
209 
210 void
211 TextDocumentView::KeyDown(const char* bytes, int32 numBytes)
212 {
213 	if (!fTextEditor.IsSet())
214 		return;
215 
216 	KeyEvent event;
217 	event.bytes = bytes;
218 	event.length = numBytes;
219 	event.key = 0;
220 	event.modifiers = modifiers();
221 
222 	if (Window() != NULL && Window()->CurrentMessage() != NULL) {
223 		BMessage* message = Window()->CurrentMessage();
224 		message->FindInt32("raw_char", &event.key);
225 		message->FindInt32("modifiers", &event.modifiers);
226 	}
227 
228 	float viewHeightPrior = fTextEditor->Layout()->Height();
229 
230 	fTextEditor->KeyDown(event);
231 	_ShowCaret(true);
232 	// TODO: It is necessary to invalidate all, since neither the caret bounds
233 	// are updated in a way that would work here, nor is the text updated
234 	// correctly which has been edited.
235 	Invalidate();
236 
237 	if (fTextEditor->Layout()->Height() != viewHeightPrior)
238 		_UpdateScrollBars();
239 }
240 
241 
242 void
243 TextDocumentView::KeyUp(const char* bytes, int32 numBytes)
244 {
245 }
246 
247 
248 BSize
249 TextDocumentView::MinSize()
250 {
251 	return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom);
252 }
253 
254 
255 BSize
256 TextDocumentView::MaxSize()
257 {
258 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
259 }
260 
261 
262 BSize
263 TextDocumentView::PreferredSize()
264 {
265 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
266 }
267 
268 
269 bool
270 TextDocumentView::HasHeightForWidth()
271 {
272 	return true;
273 }
274 
275 
276 void
277 TextDocumentView::GetHeightForWidth(float width, float* min, float* max,
278 	float* preferred)
279 {
280 	TextDocumentLayout layout(fTextDocumentLayout);
281 	layout.SetWidth(_TextLayoutWidth(width));
282 
283 	float height = layout.Height() + 1 + fInsetTop + fInsetBottom;
284 
285 	if (min != NULL)
286 		*min = height;
287 	if (max != NULL)
288 		*max = height;
289 	if (preferred != NULL)
290 		*preferred = height;
291 }
292 
293 
294 // #pragma mark -
295 
296 
297 void
298 TextDocumentView::SetTextDocument(const TextDocumentRef& document)
299 {
300 	fTextDocument = document;
301 	fTextDocumentLayout.SetTextDocument(fTextDocument);
302 	if (fTextEditor.IsSet())
303 		fTextEditor->SetDocument(document);
304 
305 	InvalidateLayout();
306 	Invalidate();
307 	_UpdateScrollBars();
308 }
309 
310 
311 void
312 TextDocumentView::SetEditingEnabled(bool enabled)
313 {
314 	if (fTextEditor.IsSet())
315 		fTextEditor->SetEditingEnabled(enabled);
316 }
317 
318 
319 void
320 TextDocumentView::SetTextEditor(const TextEditorRef& editor)
321 {
322 	if (fTextEditor == editor)
323 		return;
324 
325 	if (fTextEditor.IsSet()) {
326 		fTextEditor->SetDocument(TextDocumentRef());
327 		fTextEditor->SetLayout(TextDocumentLayoutRef());
328 		// TODO: Probably has to remove listeners
329 	}
330 
331 	fTextEditor = editor;
332 
333 	if (fTextEditor.IsSet()) {
334 		fTextEditor->SetDocument(fTextDocument);
335 		fTextEditor->SetLayout(TextDocumentLayoutRef(
336 			&fTextDocumentLayout));
337 		// TODO: Probably has to add listeners
338 	}
339 }
340 
341 
342 void
343 TextDocumentView::SetInsets(float inset)
344 {
345 	SetInsets(inset, inset, inset, inset);
346 }
347 
348 
349 void
350 TextDocumentView::SetInsets(float horizontal, float vertical)
351 {
352 	SetInsets(horizontal, vertical, horizontal, vertical);
353 }
354 
355 
356 void
357 TextDocumentView::SetInsets(float left, float top, float right, float bottom)
358 {
359 	if (fInsetLeft == left && fInsetTop == top
360 		&& fInsetRight == right && fInsetBottom == bottom) {
361 		return;
362 	}
363 
364 	fInsetLeft = left;
365 	fInsetTop = top;
366 	fInsetRight = right;
367 	fInsetBottom = bottom;
368 
369 	InvalidateLayout();
370 	Invalidate();
371 }
372 
373 
374 void
375 TextDocumentView::SetSelectionEnabled(bool enabled)
376 {
377 	if (fSelectionEnabled == enabled)
378 		return;
379 	fSelectionEnabled = enabled;
380 	Invalidate();
381 	// TODO: Deselect
382 }
383 
384 
385 void
386 TextDocumentView::SetCaret(BPoint location, bool extendSelection)
387 {
388 	if (!fSelectionEnabled || !fTextEditor.IsSet())
389 		return;
390 
391 	location.x -= fInsetLeft;
392 	location.y -= fInsetTop;
393 
394 	fTextEditor->SetCaret(location, extendSelection);
395 	_ShowCaret(!extendSelection);
396 	Invalidate();
397 }
398 
399 
400 void
401 TextDocumentView::SelectAll()
402 {
403 	if (!fSelectionEnabled || !fTextEditor.IsSet())
404 		return;
405 
406 	fTextEditor->SelectAll();
407 	_ShowCaret(false);
408 	Invalidate();
409 }
410 
411 
412 bool
413 TextDocumentView::HasSelection() const
414 {
415 	return fTextEditor.IsSet() && fTextEditor->HasSelection();
416 }
417 
418 
419 void
420 TextDocumentView::GetSelection(int32& start, int32& end) const
421 {
422 	if (fTextEditor.IsSet()) {
423 		start = fTextEditor->SelectionStart();
424 		end = fTextEditor->SelectionEnd();
425 	}
426 }
427 
428 
429 void
430 TextDocumentView::Paste(BClipboard* clipboard)
431 {
432 	if (!fTextDocument.IsSet() || !fTextEditor.IsSet())
433 		return;
434 
435 	if (!clipboard->Lock())
436 		return;
437 
438 	BMessage* clip = clipboard->Data();
439 
440 	if (clip != NULL) {
441 		const void* plainTextData;
442 		ssize_t plainTextDataSize;
443 
444 		if (clip->FindData(kMimeTypePlainText, B_MIME_TYPE, &plainTextData, &plainTextDataSize)
445 			== B_OK) {
446 
447 			if (plainTextDataSize > 0) {
448 				if (_PastePossiblyDisallowedChars(static_cast<const char*>(plainTextData),
449 					static_cast<int32>(plainTextDataSize)) != B_OK) {
450 					fprintf(stderr, "unable to paste text owing to internal error");
451 						// don't use HaikuDepot logging system as this is in the text engine
452 				}
453 			}
454 		}
455 	}
456 
457 	clipboard->Unlock();
458 }
459 
460 
461 /*!	This method will check that all of the characters in the provided
462 	string are allowed in the text document. Returns true if this is the case.
463 */
464 /*static*/ bool
465 TextDocumentView::_AreCharsAllowed(const char* str, int32 maxLength)
466 {
467 	for (int32 i = 0; str[i] != 0 && i < maxLength; i++) {
468 		if (!TextDocumentView::_IsAllowedChar(i))
469 			return false;
470 	}
471 	return true;
472 }
473 
474 
475 /*static*/ bool
476 TextDocumentView::_IsAllowedChar(char c)
477 {
478 	return c >= ' '
479 		|| c == '\t'
480 		|| c == '\n'
481 		|| c == 127 // delete
482 		;
483 }
484 
485 
486 void
487 TextDocumentView::Copy(BClipboard* clipboard)
488 {
489 	if (!HasSelection() || !fTextDocument.IsSet()) {
490 		// Nothing to copy, don't clear clipboard contents for now reason.
491 		return;
492 	}
493 
494 	if (clipboard == NULL || !clipboard->Lock())
495 		return;
496 
497 	clipboard->Clear();
498 
499 	BMessage* clip = clipboard->Data();
500 	if (clip != NULL) {
501 		int32 start;
502 		int32 end;
503 		GetSelection(start, end);
504 
505 		BString text = fTextDocument->Text(start, end - start);
506 		clip->AddData(kMimeTypePlainText, B_MIME_TYPE, text.String(), text.Length());
507 
508 		// TODO: Support for "application/x-vnd.Be-text_run_array"
509 
510 		clipboard->Commit();
511 	}
512 
513 	clipboard->Unlock();
514 }
515 
516 
517 void
518 TextDocumentView::Relayout()
519 {
520 	fTextDocumentLayout.Invalidate();
521 	_UpdateScrollBars();
522 }
523 
524 
525 // #pragma mark - private
526 
527 
528 float
529 TextDocumentView::_TextLayoutWidth(float viewWidth) const
530 {
531 	return viewWidth - (fInsetLeft + fInsetRight);
532 }
533 
534 
535 static const float kHorizontalScrollBarStep = 10.0f;
536 static const float kVerticalScrollBarStep = 12.0f;
537 
538 
539 void
540 TextDocumentView::_UpdateScrollBars()
541 {
542 	BRect bounds(Bounds());
543 
544 	BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL);
545 	if (horizontalScrollBar != NULL) {
546 		long viewWidth = bounds.IntegerWidth();
547 		long dataWidth = (long)ceilf(
548 			fTextDocumentLayout.Width() + fInsetLeft + fInsetRight);
549 
550 		long maxRange = dataWidth - viewWidth;
551 		maxRange = std::max(maxRange, 0L);
552 
553 		horizontalScrollBar->SetRange(0, (float)maxRange);
554 		horizontalScrollBar->SetProportion((float)viewWidth / dataWidth);
555 		horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10);
556 	}
557 
558  	BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL);
559 	if (verticalScrollBar != NULL) {
560 		long viewHeight = bounds.IntegerHeight();
561 		long dataHeight = (long)ceilf(
562 			fTextDocumentLayout.Height() + fInsetTop + fInsetBottom);
563 
564 		long maxRange = dataHeight - viewHeight;
565 		maxRange = std::max(maxRange, 0L);
566 
567 		verticalScrollBar->SetRange(0, maxRange);
568 		verticalScrollBar->SetProportion((float)viewHeight / dataHeight);
569 		verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight);
570 	}
571 }
572 
573 
574 void
575 TextDocumentView::_ShowCaret(bool show)
576 {
577 	fShowCaret = show;
578 	if (fCaretBounds.IsValid())
579 		Invalidate(fCaretBounds);
580 	else
581 		Invalidate();
582 	// Cancel previous blinker, increment blink token so we only accept
583 	// the message from the blinker we just created
584 	fCaretBlinkToken++;
585 	BMessage message(MSG_BLINK_CARET);
586 	message.AddInt32("token", fCaretBlinkToken);
587 	delete fCaretBlinker;
588 	fCaretBlinker = new BMessageRunner(BMessenger(this), &message,
589 		500000, 1);
590 }
591 
592 
593 void
594 TextDocumentView::_BlinkCaret()
595 {
596 	if (!fSelectionEnabled || !fTextEditor.IsSet())
597 		return;
598 
599 	_ShowCaret(!fShowCaret);
600 }
601 
602 
603 void
604 TextDocumentView::_DrawCaret(int32 textOffset)
605 {
606 	if (!IsFocus() || Window() == NULL || !Window()->IsActive())
607 		return;
608 
609 	float x1;
610 	float y1;
611 	float x2;
612 	float y2;
613 
614 	fTextDocumentLayout.GetTextBounds(textOffset, x1, y1, x2, y2);
615 	x2 = x1 + 1;
616 
617 	fCaretBounds = BRect(x1, y1, x2, y2);
618 	fCaretBounds.OffsetBy(fInsetLeft, fInsetTop);
619 
620 	SetDrawingMode(B_OP_INVERT);
621 	FillRect(fCaretBounds);
622 }
623 
624 
625 void
626 TextDocumentView::_DrawSelection()
627 {
628 	int32 start;
629 	int32 end;
630 	GetSelection(start, end);
631 
632 	BShape shape;
633 	_GetSelectionShape(shape, start, end);
634 
635 	SetDrawingMode(B_OP_SUBTRACT);
636 
637 	SetLineMode(B_ROUND_CAP, B_ROUND_JOIN);
638 	MovePenTo(fInsetLeft - 0.5f, fInsetTop - 0.5f);
639 
640 	if (IsFocus() && Window() != NULL && Window()->IsActive()) {
641 		SetHighColor(30, 30, 30);
642 		FillShape(&shape);
643 	}
644 
645 	SetHighColor(40, 40, 40);
646 	StrokeShape(&shape);
647 }
648 
649 
650 void
651 TextDocumentView::_GetSelectionShape(BShape& shape, int32 start, int32 end)
652 {
653 	float startX1;
654 	float startY1;
655 	float startX2;
656 	float startY2;
657 	fTextDocumentLayout.GetTextBounds(start, startX1, startY1, startX2,
658 		startY2);
659 
660 	startX1 = floorf(startX1);
661 	startY1 = floorf(startY1);
662 	startX2 = ceilf(startX2);
663 	startY2 = ceilf(startY2);
664 
665 	float endX1;
666 	float endY1;
667 	float endX2;
668 	float endY2;
669 	fTextDocumentLayout.GetTextBounds(end, endX1, endY1, endX2, endY2);
670 
671 	endX1 = floorf(endX1);
672 	endY1 = floorf(endY1);
673 	endX2 = ceilf(endX2);
674 	endY2 = ceilf(endY2);
675 
676 	int32 startLineIndex = fTextDocumentLayout.LineIndexForOffset(start);
677 	int32 endLineIndex = fTextDocumentLayout.LineIndexForOffset(end);
678 
679 	if (startLineIndex == endLineIndex) {
680 		// Selection on one line
681 		BPoint lt(startX1, startY1);
682 		BPoint rt(endX1, endY1);
683 		BPoint rb(endX1, endY2);
684 		BPoint lb(startX1, startY2);
685 
686 		shape.MoveTo(lt);
687 		shape.LineTo(rt);
688 		shape.LineTo(rb);
689 		shape.LineTo(lb);
690 		shape.Close();
691 	} else if (startLineIndex == endLineIndex - 1 && endX1 <= startX1) {
692 		// Selection on two lines, with gap:
693 		// ---------
694 		// ------###
695 		// ##-------
696 		// ---------
697 		float width = ceilf(fTextDocumentLayout.Width());
698 
699 		BPoint lt(startX1, startY1);
700 		BPoint rt(width, startY1);
701 		BPoint rb(width, startY2);
702 		BPoint lb(startX1, startY2);
703 
704 		shape.MoveTo(lt);
705 		shape.LineTo(rt);
706 		shape.LineTo(rb);
707 		shape.LineTo(lb);
708 		shape.Close();
709 
710 		lt = BPoint(0, endY1);
711 		rt = BPoint(endX1, endY1);
712 		rb = BPoint(endX1, endY2);
713 		lb = BPoint(0, endY2);
714 
715 		shape.MoveTo(lt);
716 		shape.LineTo(rt);
717 		shape.LineTo(rb);
718 		shape.LineTo(lb);
719 		shape.Close();
720 	} else {
721 		// Selection over multiple lines
722 		float width = ceilf(fTextDocumentLayout.Width());
723 
724 		shape.MoveTo(BPoint(startX1, startY1));
725 		shape.LineTo(BPoint(width, startY1));
726 		shape.LineTo(BPoint(width, endY1));
727 		shape.LineTo(BPoint(endX1, endY1));
728 		shape.LineTo(BPoint(endX1, endY2));
729 		shape.LineTo(BPoint(0, endY2));
730 		shape.LineTo(BPoint(0, startY2));
731 		shape.LineTo(BPoint(startX1, startY2));
732 		shape.Close();
733 	}
734 }
735 
736 
737 /*!	The data provided in the `str` parameter may contain characters that are
738 	not allowed. This method should filter those out and then apply them to
739 	the text body.
740 */
741 status_t
742 TextDocumentView::_PastePossiblyDisallowedChars(const char* str, int32 maxLength)
743 {
744 	if (maxLength <= 0)
745 		return B_OK;
746 
747 	if (TextDocumentView::_AreCharsAllowed(str, maxLength)) {
748 		_PasteAllowedChars(str, maxLength);
749 	} else {
750 		char* strFiltered = new(std::nothrow) char[maxLength];
751 
752 		if (strFiltered == NULL)
753 			return B_NO_MEMORY;
754 
755 		int32 strFilteredLength = 0;
756 
757 		for (int i = 0; str[i] != '\0' && i < maxLength; i++) {
758 			if (_IsAllowedChar(str[i])) {
759 				strFiltered[strFilteredLength] = str[i];
760 				strFilteredLength++;
761 			}
762 		}
763 
764 		strFiltered[strFilteredLength] = '\0';
765 		_PasteAllowedChars(strFiltered, strFilteredLength);
766 
767 		delete[] strFiltered;
768 	}
769 
770 	return B_OK;
771 }
772 
773 
774 /*! Here the data in `str` should be clean of control characters.
775  */
776 void
777 TextDocumentView::_PasteAllowedChars(const char* str, int32 maxLength)
778 {
779 	BString plainText(str, maxLength);
780 
781 	if (plainText.IsEmpty())
782 		return;
783 
784 	if (fTextEditor.IsSet()) {
785 		if (fTextEditor->HasSelection()) {
786 			int32 start = fTextEditor->SelectionStart();
787 			int32 end = fTextEditor->SelectionEnd();
788 			fTextEditor->Replace(start, end - start, plainText);
789 			Invalidate();
790 			_UpdateScrollBars();
791 		} else {
792 			int32 caretOffset = fTextEditor->CaretOffset();
793 			if (caretOffset >= 0) {
794 				fTextEditor->Insert(caretOffset, plainText);
795 				Invalidate();
796 				_UpdateScrollBars();
797 			}
798 		}
799 	}
800 }