xref: /haiku/src/apps/haikudepot/textview/TextDocumentView.cpp (revision 82bfaa954dcfd90582fb2c1a0e918971eea57091)
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 enum {
20 	MSG_BLINK_CARET		= 'blnk',
21 };
22 
23 
24 TextDocumentView::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 
49 TextDocumentView::~TextDocumentView()
50 {
51 	// Don't forget to remove listeners
52 	SetTextEditor(TextEditorRef());
53 	delete fCaretBlinker;
54 }
55 
56 
57 void
58 TextDocumentView::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 
84 void
85 TextDocumentView::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 
106 void
107 TextDocumentView::AttachedToWindow()
108 {
109 	_UpdateScrollBars();
110 }
111 
112 
113 void
114 TextDocumentView::FrameResized(float width, float height)
115 {
116 	fTextDocumentLayout.SetWidth(width);
117 	_UpdateScrollBars();
118 }
119 
120 
121 void
122 TextDocumentView::WindowActivated(bool active)
123 {
124 	Invalidate();
125 }
126 
127 
128 void
129 TextDocumentView::MakeFocus(bool focus)
130 {
131 	if (focus != IsFocus())
132 		Invalidate();
133 	BView::MakeFocus(focus);
134 }
135 
136 
137 void
138 TextDocumentView::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 
171 void
172 TextDocumentView::MouseUp(BPoint where)
173 {
174 	fMouseDown = false;
175 }
176 
177 
178 void
179 TextDocumentView::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 
204 void
205 TextDocumentView::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 
231 void
232 TextDocumentView::KeyUp(const char* bytes, int32 numBytes)
233 {
234 }
235 
236 
237 BSize
238 TextDocumentView::MinSize()
239 {
240 	return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom);
241 }
242 
243 
244 BSize
245 TextDocumentView::MaxSize()
246 {
247 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
248 }
249 
250 
251 BSize
252 TextDocumentView::PreferredSize()
253 {
254 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
255 }
256 
257 
258 bool
259 TextDocumentView::HasHeightForWidth()
260 {
261 	return true;
262 }
263 
264 
265 void
266 TextDocumentView::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 
286 void
287 TextDocumentView::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 
300 void
301 TextDocumentView::SetEditingEnabled(bool enabled)
302 {
303 	if (fTextEditor.IsSet())
304 		fTextEditor->SetEditingEnabled(enabled);
305 }
306 
307 
308 void
309 TextDocumentView::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 
331 void
332 TextDocumentView::SetInsets(float inset)
333 {
334 	SetInsets(inset, inset, inset, inset);
335 }
336 
337 
338 void
339 TextDocumentView::SetInsets(float horizontal, float vertical)
340 {
341 	SetInsets(horizontal, vertical, horizontal, vertical);
342 }
343 
344 
345 void
346 TextDocumentView::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 
363 void
364 TextDocumentView::SetSelectionEnabled(bool enabled)
365 {
366 	if (fSelectionEnabled == enabled)
367 		return;
368 	fSelectionEnabled = enabled;
369 	Invalidate();
370 	// TODO: Deselect
371 }
372 
373 
374 void
375 TextDocumentView::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 
389 void
390 TextDocumentView::SelectAll()
391 {
392 	if (!fSelectionEnabled || !fTextEditor.IsSet())
393 		return;
394 
395 	fTextEditor->SelectAll();
396 	_ShowCaret(false);
397 	Invalidate();
398 }
399 
400 
401 bool
402 TextDocumentView::HasSelection() const
403 {
404 	return fTextEditor.IsSet() && fTextEditor->HasSelection();
405 }
406 
407 
408 void
409 TextDocumentView::GetSelection(int32& start, int32& end) const
410 {
411 	if (fTextEditor.IsSet()) {
412 		start = fTextEditor->SelectionStart();
413 		end = fTextEditor->SelectionEnd();
414 	}
415 }
416 
417 
418 void
419 TextDocumentView::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 
450 void
451 TextDocumentView::Relayout()
452 {
453 	fTextDocumentLayout.Invalidate();
454 	_UpdateScrollBars();
455 }
456 
457 
458 // #pragma mark - private
459 
460 
461 float
462 TextDocumentView::_TextLayoutWidth(float viewWidth) const
463 {
464 	return viewWidth - (fInsetLeft + fInsetRight);
465 }
466 
467 
468 static const float kHorizontalScrollBarStep = 10.0f;
469 static const float kVerticalScrollBarStep = 12.0f;
470 
471 
472 void
473 TextDocumentView::_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 
507 void
508 TextDocumentView::_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 
526 void
527 TextDocumentView::_BlinkCaret()
528 {
529 	if (!fSelectionEnabled || !fTextEditor.IsSet())
530 		return;
531 
532 	_ShowCaret(!fShowCaret);
533 }
534 
535 
536 void
537 TextDocumentView::_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 
558 void
559 TextDocumentView::_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 
583 void
584 TextDocumentView::_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