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