xref: /haiku/src/apps/haikudepot/textview/TextDocumentView.cpp (revision 0975f16f7ca94b654cb6a55b3316daae89843abb)
1 /*
2  * Copyright 2013-2014, 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 <ScrollBar.h>
14 #include <Shape.h>
15 #include <Window.h>
16 
17 
18 TextDocumentView::TextDocumentView(const char* name)
19 	:
20 	BView(name, B_WILL_DRAW | B_FULL_UPDATE_ON_RESIZE | B_FRAME_EVENTS
21 		| B_PULSE_NEEDED),
22 	fInsetLeft(0.0f),
23 	fInsetTop(0.0f),
24 	fInsetRight(0.0f),
25 	fInsetBottom(0.0f),
26 
27 	fCaretBounds(),
28 	fSelectionEnabled(true),
29 	fShowCaret(false),
30 	fMouseDown(false)
31 {
32 	fTextDocumentLayout.SetWidth(_TextLayoutWidth(Bounds().Width()));
33 
34 	// Set default TextEditor
35 	SetTextEditor(TextEditorRef(new(std::nothrow) TextEditor(), true));
36 
37 	SetViewColor(B_TRANSPARENT_COLOR);
38 	SetLowColor(255, 255, 255, 255);
39 }
40 
41 
42 TextDocumentView::~TextDocumentView()
43 {
44 	// Don't forget to remove listeners
45 	SetTextEditor(TextEditorRef());
46 }
47 
48 
49 void
50 TextDocumentView::MessageReceived(BMessage* message)
51 {
52 	switch (message->what) {
53 		case B_COPY:
54 			Copy(be_clipboard);
55 			break;
56 		case B_SELECT_ALL:
57 			SelectAll();
58 			break;
59 
60 		default:
61 			BView::MessageReceived(message);
62 	}
63 }
64 
65 
66 void
67 TextDocumentView::Draw(BRect updateRect)
68 {
69 	FillRect(updateRect, B_SOLID_LOW);
70 
71 	fTextDocumentLayout.SetWidth(_TextLayoutWidth(Bounds().Width()));
72 	fTextDocumentLayout.Draw(this, BPoint(fInsetLeft, fInsetTop), updateRect);
73 
74 	if (!fSelectionEnabled || fTextEditor.Get() == NULL)
75 		return;
76 
77 	bool isCaret = fTextEditor->SelectionLength() == 0;
78 
79 	if (isCaret) {
80 		if (fShowCaret && fTextEditor->IsEditingEnabled())
81 			_DrawCaret(fTextEditor->CaretOffset());
82 	} else {
83 		_DrawSelection();
84 	}
85 }
86 
87 
88 void
89 TextDocumentView::Pulse()
90 {
91 	if (!fSelectionEnabled || fTextEditor.Get() == NULL)
92 		return;
93 
94 	// Blink cursor
95 	fShowCaret = !fShowCaret;
96 	if (fCaretBounds.IsValid())
97 		Invalidate(fCaretBounds);
98 	else
99 		Invalidate();
100 }
101 
102 
103 void
104 TextDocumentView::AttachedToWindow()
105 {
106 	_UpdateScrollBars();
107 }
108 
109 
110 void
111 TextDocumentView::FrameResized(float width, float height)
112 {
113 	fTextDocumentLayout.SetWidth(width);
114 	_UpdateScrollBars();
115 }
116 
117 
118 void
119 TextDocumentView::WindowActivated(bool active)
120 {
121 	Invalidate();
122 }
123 
124 
125 void
126 TextDocumentView::MakeFocus(bool focus)
127 {
128 	if (focus != IsFocus())
129 		Invalidate();
130 	BView::MakeFocus(focus);
131 }
132 
133 
134 void
135 TextDocumentView::MouseDown(BPoint where)
136 {
137 	if (!fSelectionEnabled)
138 		return;
139 
140 	MakeFocus();
141 
142 	int32 modifiers = 0;
143 	if (Window() != NULL && Window()->CurrentMessage() != NULL)
144 		Window()->CurrentMessage()->FindInt32("modifiers", &modifiers);
145 
146 	fMouseDown = true;
147 	SetMouseEventMask(B_POINTER_EVENTS, B_LOCK_WINDOW_FOCUS);
148 
149 	bool extendSelection = (modifiers & B_SHIFT_KEY) != 0;
150 	SetCaret(where, extendSelection);
151 }
152 
153 
154 void
155 TextDocumentView::MouseUp(BPoint where)
156 {
157 	fMouseDown = false;
158 }
159 
160 
161 void
162 TextDocumentView::MouseMoved(BPoint where, uint32 transit,
163 	const BMessage* dragMessage)
164 {
165 	if (!fSelectionEnabled)
166 		return;
167 
168 	BCursor iBeamCursor(B_CURSOR_ID_I_BEAM);
169 	SetViewCursor(&iBeamCursor);
170 
171 	if (fMouseDown)
172 		SetCaret(where, true);
173 }
174 
175 
176 void
177 TextDocumentView::KeyDown(const char* bytes, int32 numBytes)
178 {
179 	if (fTextEditor.Get() == NULL)
180 		return;
181 
182 	KeyEvent event;
183 	event.bytes = bytes;
184 	event.length = numBytes;
185 	event.key = 0;
186 	event.modifiers = modifiers();
187 
188 	if (Window() != NULL && Window()->CurrentMessage() != NULL) {
189 		BMessage* message = Window()->CurrentMessage();
190 		message->FindInt32("raw_char", &event.key);
191 		message->FindInt32("modifiers", &event.modifiers);
192 	}
193 
194 	fTextEditor->KeyDown(event);
195 	fShowCaret = true;
196 	Invalidate();
197 }
198 
199 
200 void
201 TextDocumentView::KeyUp(const char* bytes, int32 numBytes)
202 {
203 }
204 
205 
206 BSize
207 TextDocumentView::MinSize()
208 {
209 	return BSize(fInsetLeft + fInsetRight + 50.0f, fInsetTop + fInsetBottom);
210 }
211 
212 
213 BSize
214 TextDocumentView::MaxSize()
215 {
216 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
217 }
218 
219 
220 BSize
221 TextDocumentView::PreferredSize()
222 {
223 	return BSize(B_SIZE_UNLIMITED, B_SIZE_UNLIMITED);
224 }
225 
226 
227 bool
228 TextDocumentView::HasHeightForWidth()
229 {
230 	return true;
231 }
232 
233 
234 void
235 TextDocumentView::GetHeightForWidth(float width, float* min, float* max,
236 	float* preferred)
237 {
238 	TextDocumentLayout layout(fTextDocumentLayout);
239 	layout.SetWidth(_TextLayoutWidth(width));
240 
241 	float height = layout.Height() + 1 + fInsetTop + fInsetBottom;
242 
243 	if (min != NULL)
244 		*min = height;
245 	if (max != NULL)
246 		*max = height;
247 	if (preferred != NULL)
248 		*preferred = height;
249 }
250 
251 
252 // #pragma mark -
253 
254 
255 void
256 TextDocumentView::SetTextDocument(const TextDocumentRef& document)
257 {
258 	fTextDocument = document;
259 	fTextDocumentLayout.SetTextDocument(fTextDocument);
260 	if (fTextEditor.Get() != NULL)
261 		fTextEditor->SetDocument(document);
262 
263 	InvalidateLayout();
264 	Invalidate();
265 	_UpdateScrollBars();
266 }
267 
268 
269 void
270 TextDocumentView::SetEditingEnabled(bool enabled)
271 {
272 	if (fTextEditor.Get() != NULL)
273 		fTextEditor->SetEditingEnabled(enabled);
274 }
275 
276 
277 void
278 TextDocumentView::SetTextEditor(const TextEditorRef& editor)
279 {
280 	if (fTextEditor == editor)
281 		return;
282 
283 	if (fTextEditor.Get() != NULL) {
284 		fTextEditor->SetDocument(TextDocumentRef());
285 		fTextEditor->SetLayout(TextDocumentLayoutRef());
286 		// TODO: Probably has to remove listeners
287 	}
288 
289 	fTextEditor = editor;
290 
291 	if (fTextEditor.Get() != NULL) {
292 		fTextEditor->SetDocument(fTextDocument);
293 		fTextEditor->SetLayout(TextDocumentLayoutRef(
294 			&fTextDocumentLayout));
295 		// TODO: Probably has to add listeners
296 	}
297 }
298 
299 
300 void
301 TextDocumentView::SetInsets(float inset)
302 {
303 	SetInsets(inset, inset, inset, inset);
304 }
305 
306 
307 void
308 TextDocumentView::SetInsets(float horizontal, float vertical)
309 {
310 	SetInsets(horizontal, vertical, horizontal, vertical);
311 }
312 
313 
314 void
315 TextDocumentView::SetInsets(float left, float top, float right, float bottom)
316 {
317 	if (fInsetLeft == left && fInsetTop == top
318 		&& fInsetRight == right && fInsetBottom == bottom) {
319 		return;
320 	}
321 
322 	fInsetLeft = left;
323 	fInsetTop = top;
324 	fInsetRight = right;
325 	fInsetBottom = bottom;
326 
327 	InvalidateLayout();
328 	Invalidate();
329 }
330 
331 
332 void
333 TextDocumentView::SetSelectionEnabled(bool enabled)
334 {
335 	if (fSelectionEnabled == enabled)
336 		return;
337 	fSelectionEnabled = enabled;
338 	Invalidate();
339 	// TODO: Deselect
340 }
341 
342 
343 void
344 TextDocumentView::SetCaret(BPoint location, bool extendSelection)
345 {
346 	if (!fSelectionEnabled || fTextEditor.Get() == NULL)
347 		return;
348 
349 	location.x -= fInsetLeft;
350 	location.y -= fInsetTop;
351 
352 	fTextEditor->SetCaret(location, extendSelection);
353 	fShowCaret = !extendSelection;
354 	Invalidate();
355 }
356 
357 
358 void
359 TextDocumentView::SelectAll()
360 {
361 	if (!fSelectionEnabled || fTextEditor.Get() == NULL)
362 		return;
363 
364 	fTextEditor->SelectAll();
365 	fShowCaret = false;
366 	Invalidate();
367 }
368 
369 
370 bool
371 TextDocumentView::HasSelection() const
372 {
373 	return fTextEditor.Get() != NULL && fTextEditor->HasSelection();
374 }
375 
376 
377 void
378 TextDocumentView::GetSelection(int32& start, int32& end) const
379 {
380 	if (fTextEditor.Get() != NULL) {
381 		start = fTextEditor->SelectionStart();
382 		end = fTextEditor->SelectionEnd();
383 	}
384 }
385 
386 
387 void
388 TextDocumentView::Copy(BClipboard* clipboard)
389 {
390 	if (!HasSelection() || fTextDocument.Get() == NULL) {
391 		// Nothing to copy, don't clear clipboard contents for now reason.
392 		return;
393 	}
394 
395 	if (clipboard == NULL || !clipboard->Lock())
396 		return;
397 
398 	clipboard->Clear();
399 
400 	BMessage* clip = clipboard->Data();
401 	if (clip != NULL) {
402 		int32 start;
403 		int32 end;
404 		GetSelection(start, end);
405 
406 		BString text = fTextDocument->Text(start, end - start);
407 		clip->AddData("text/plain", B_MIME_TYPE, text.String(),
408 			text.Length());
409 
410 		// TODO: Support for "application/x-vnd.Be-text_run_array"
411 
412 		clipboard->Commit();
413 	}
414 
415 	clipboard->Unlock();
416 }
417 
418 
419 // #pragma mark - private
420 
421 
422 float
423 TextDocumentView::_TextLayoutWidth(float viewWidth) const
424 {
425 	return viewWidth - (fInsetLeft + fInsetRight);
426 }
427 
428 
429 static const float kHorizontalScrollBarStep = 10.0f;
430 static const float kVerticalScrollBarStep = 12.0f;
431 
432 
433 void
434 TextDocumentView::_UpdateScrollBars()
435 {
436 	BRect bounds(Bounds());
437 
438 	BScrollBar* horizontalScrollBar = ScrollBar(B_HORIZONTAL);
439 	if (horizontalScrollBar != NULL) {
440 		long viewWidth = bounds.IntegerWidth();
441 		long dataWidth = (long)ceilf(
442 			fTextDocumentLayout.Width() + fInsetLeft + fInsetRight);
443 
444 		long maxRange = dataWidth - viewWidth;
445 		maxRange = std::max(maxRange, 0L);
446 
447 		horizontalScrollBar->SetRange(0, (float)maxRange);
448 		horizontalScrollBar->SetProportion((float)viewWidth / dataWidth);
449 		horizontalScrollBar->SetSteps(kHorizontalScrollBarStep, dataWidth / 10);
450 	}
451 
452  	BScrollBar* verticalScrollBar = ScrollBar(B_VERTICAL);
453 	if (verticalScrollBar != NULL) {
454 		long viewHeight = bounds.IntegerHeight();
455 		long dataHeight = (long)ceilf(
456 			fTextDocumentLayout.Height() + fInsetTop + fInsetBottom);
457 
458 		long maxRange = dataHeight - viewHeight;
459 		maxRange = std::max(maxRange, 0L);
460 
461 		verticalScrollBar->SetRange(0, maxRange);
462 		verticalScrollBar->SetProportion((float)viewHeight / dataHeight);
463 		verticalScrollBar->SetSteps(kVerticalScrollBarStep, viewHeight);
464 	}
465 }
466 
467 
468 void
469 TextDocumentView::_DrawCaret(int32 textOffset)
470 {
471 	if (!IsFocus() || Window() == NULL || !Window()->IsActive())
472 		return;
473 
474 	float x1;
475 	float y1;
476 	float x2;
477 	float y2;
478 
479 	fTextDocumentLayout.GetTextBounds(textOffset, x1, y1, x2, y2);
480 	x2 = x1 + 1;
481 
482 	fCaretBounds = BRect(x1, y1, x2, y2);
483 	fCaretBounds.OffsetBy(fInsetLeft, fInsetTop);
484 
485 	SetDrawingMode(B_OP_INVERT);
486 	FillRect(fCaretBounds);
487 }
488 
489 
490 void
491 TextDocumentView::_DrawSelection()
492 {
493 	int32 start;
494 	int32 end;
495 	GetSelection(start, end);
496 
497 	BShape shape;
498 	_GetSelectionShape(shape, start, end);
499 
500 	SetDrawingMode(B_OP_SUBTRACT);
501 
502 	SetLineMode(B_ROUND_CAP, B_ROUND_JOIN);
503 	MovePenTo(fInsetLeft - 0.5f, fInsetTop - 0.5f);
504 
505 	if (IsFocus() && Window() != NULL && Window()->IsActive()) {
506 		SetHighColor(30, 30, 30);
507 		FillShape(&shape);
508 	}
509 
510 	SetHighColor(40, 40, 40);
511 	StrokeShape(&shape);
512 }
513 
514 
515 void
516 TextDocumentView::_GetSelectionShape(BShape& shape, int32 start, int32 end)
517 {
518 	float startX1;
519 	float startY1;
520 	float startX2;
521 	float startY2;
522 	fTextDocumentLayout.GetTextBounds(start, startX1, startY1, startX2,
523 		startY2);
524 
525 	startX1 = floorf(startX1);
526 	startY1 = floorf(startY1);
527 	startX2 = ceilf(startX2);
528 	startY2 = ceilf(startY2);
529 
530 	float endX1;
531 	float endY1;
532 	float endX2;
533 	float endY2;
534 	fTextDocumentLayout.GetTextBounds(end, endX1, endY1, endX2, endY2);
535 
536 	endX1 = floorf(endX1);
537 	endY1 = floorf(endY1);
538 	endX2 = ceilf(endX2);
539 	endY2 = ceilf(endY2);
540 
541 	int32 startLineIndex = fTextDocumentLayout.LineIndexForOffset(start);
542 	int32 endLineIndex = fTextDocumentLayout.LineIndexForOffset(end);
543 
544 	if (startLineIndex == endLineIndex) {
545 		// Selection on one line
546 		BPoint lt(startX1, startY1);
547 		BPoint rt(endX1, endY1);
548 		BPoint rb(endX1, endY2);
549 		BPoint lb(startX1, startY2);
550 
551 		shape.MoveTo(lt);
552 		shape.LineTo(rt);
553 		shape.LineTo(rb);
554 		shape.LineTo(lb);
555 		shape.Close();
556 	} else if (startLineIndex == endLineIndex - 1 && endX1 <= startX1) {
557 		// Selection on two lines, with gap:
558 		// ---------
559 		// ------###
560 		// ##-------
561 		// ---------
562 		float width = ceilf(fTextDocumentLayout.Width());
563 
564 		BPoint lt(startX1, startY1);
565 		BPoint rt(width, startY1);
566 		BPoint rb(width, startY2);
567 		BPoint lb(startX1, startY2);
568 
569 		shape.MoveTo(lt);
570 		shape.LineTo(rt);
571 		shape.LineTo(rb);
572 		shape.LineTo(lb);
573 		shape.Close();
574 
575 		lt = BPoint(0, endY1);
576 		rt = BPoint(endX1, endY1);
577 		rb = BPoint(endX1, endY2);
578 		lb = BPoint(0, endY2);
579 
580 		shape.MoveTo(lt);
581 		shape.LineTo(rt);
582 		shape.LineTo(rb);
583 		shape.LineTo(lb);
584 		shape.Close();
585 	} else {
586 		// Selection over multiple lines
587 		float width = ceilf(fTextDocumentLayout.Width());
588 
589 		shape.MoveTo(BPoint(startX1, startY1));
590 		shape.LineTo(BPoint(width, startY1));
591 		shape.LineTo(BPoint(width, endY1));
592 		shape.LineTo(BPoint(endX1, endY1));
593 		shape.LineTo(BPoint(endX1, endY2));
594 		shape.LineTo(BPoint(0, endY2));
595 		shape.LineTo(BPoint(0, startY2));
596 		shape.LineTo(BPoint(startX1, startY2));
597 		shape.Close();
598 	}
599 }
600 
601 
602