xref: /haiku/src/apps/haikudepot/textview/TextDocumentView.cpp (revision 21258e2674226d6aa732321b6f8494841895af5f)
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.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 
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 	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 
157 void
158 TextDocumentView::MouseUp(BPoint where)
159 {
160 	fMouseDown = false;
161 }
162 
163 
164 void
165 TextDocumentView::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 
179 void
180 TextDocumentView::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 
206 void
207 TextDocumentView::KeyUp(const char* bytes, int32 numBytes)
208 {
209 }
210 
211 
212 BSize
213 TextDocumentView::MinSize()
214 {
215 	return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom);
216 }
217 
218 
219 BSize
220 TextDocumentView::MaxSize()
221 {
222 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
223 }
224 
225 
226 BSize
227 TextDocumentView::PreferredSize()
228 {
229 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
230 }
231 
232 
233 bool
234 TextDocumentView::HasHeightForWidth()
235 {
236 	return true;
237 }
238 
239 
240 void
241 TextDocumentView::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 
261 void
262 TextDocumentView::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 
275 void
276 TextDocumentView::SetEditingEnabled(bool enabled)
277 {
278 	if (fTextEditor.Get() != NULL)
279 		fTextEditor->SetEditingEnabled(enabled);
280 }
281 
282 
283 void
284 TextDocumentView::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 
306 void
307 TextDocumentView::SetInsets(float inset)
308 {
309 	SetInsets(inset, inset, inset, inset);
310 }
311 
312 
313 void
314 TextDocumentView::SetInsets(float horizontal, float vertical)
315 {
316 	SetInsets(horizontal, vertical, horizontal, vertical);
317 }
318 
319 
320 void
321 TextDocumentView::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 
338 void
339 TextDocumentView::SetSelectionEnabled(bool enabled)
340 {
341 	if (fSelectionEnabled == enabled)
342 		return;
343 	fSelectionEnabled = enabled;
344 	Invalidate();
345 	// TODO: Deselect
346 }
347 
348 
349 void
350 TextDocumentView::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 
364 void
365 TextDocumentView::SelectAll()
366 {
367 	if (!fSelectionEnabled || fTextEditor.Get() == NULL)
368 		return;
369 
370 	fTextEditor->SelectAll();
371 	_ShowCaret(false);
372 	Invalidate();
373 }
374 
375 
376 bool
377 TextDocumentView::HasSelection() const
378 {
379 	return fTextEditor.Get() != NULL && fTextEditor->HasSelection();
380 }
381 
382 
383 void
384 TextDocumentView::GetSelection(int32& start, int32& end) const
385 {
386 	if (fTextEditor.Get() != NULL) {
387 		start = fTextEditor->SelectionStart();
388 		end = fTextEditor->SelectionEnd();
389 	}
390 }
391 
392 
393 void
394 TextDocumentView::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 
428 float
429 TextDocumentView::_TextLayoutWidth(float viewWidth) const
430 {
431 	return viewWidth - (fInsetLeft + fInsetRight);
432 }
433 
434 
435 static const float kHorizontalScrollBarStep = 10.0f;
436 static const float kVerticalScrollBarStep = 12.0f;
437 
438 
439 void
440 TextDocumentView::_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 
474 void
475 TextDocumentView::_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 
493 void
494 TextDocumentView::_BlinkCaret()
495 {
496 	if (!fSelectionEnabled || fTextEditor.Get() == NULL)
497 		return;
498 
499 	_ShowCaret(!fShowCaret);
500 }
501 
502 
503 void
504 TextDocumentView::_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 
525 void
526 TextDocumentView::_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 
550 void
551 TextDocumentView::_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