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