1/*
2 * Copyright 2014, Stephan A��mus <superstippi@gmx.de>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
5
6#include "TextEditor.h"
7
8#include <algorithm>
9#include <stdio.h>
10
11
12TextEditor::TextEditor()
13	:
14	fDocument(),
15	fLayout(),
16	fSelection(),
17	fCaretAnchorX(0.0f),
18	fStyleAtCaret(),
19	fEditingEnabled(true)
20{
21}
22
23
24TextEditor::TextEditor(const TextEditor& other)
25	:
26	fDocument(other.fDocument),
27	fLayout(other.fLayout),
28	fSelection(other.fSelection),
29	fCaretAnchorX(other.fCaretAnchorX),
30	fStyleAtCaret(other.fStyleAtCaret),
31	fEditingEnabled(other.fEditingEnabled)
32{
33}
34
35
36TextEditor::~TextEditor()
37{
38}
39
40
41TextEditor&
42TextEditor::operator=(const TextEditor& other)
43{
44	if (this == &other)
45		return *this;
46
47	fDocument = other.fDocument;
48	fLayout = other.fLayout;
49	fSelection = other.fSelection;
50	fCaretAnchorX = other.fCaretAnchorX;
51	fStyleAtCaret = other.fStyleAtCaret;
52	fEditingEnabled = other.fEditingEnabled;
53	return *this;
54}
55
56
57bool
58TextEditor::operator==(const TextEditor& other) const
59{
60	if (this == &other)
61		return true;
62
63	return fDocument == other.fDocument
64		&& fLayout == other.fLayout
65		&& fSelection == other.fSelection
66		&& fCaretAnchorX == other.fCaretAnchorX
67		&& fStyleAtCaret == other.fStyleAtCaret
68		&& fEditingEnabled == other.fEditingEnabled;
69}
70
71
72bool
73TextEditor::operator!=(const TextEditor& other) const
74{
75	return !(*this == other);
76}
77
78
79// #pragma mark -
80
81
82void
83TextEditor::SetDocument(const TextDocumentRef& ref)
84{
85	fDocument = ref;
86	SetSelection(TextSelection());
87}
88
89
90void
91TextEditor::SetLayout(const TextDocumentLayoutRef& ref)
92{
93	fLayout = ref;
94	SetSelection(TextSelection());
95}
96
97
98void
99TextEditor::SetEditingEnabled(bool enabled)
100{
101	fEditingEnabled = enabled;
102}
103
104
105void
106TextEditor::SetCaret(BPoint location, bool extendSelection)
107{
108	if (!fDocument.IsSet() || !fLayout.IsSet())
109		return;
110
111	bool rightOfChar = false;
112	int32 caretOffset = fLayout->TextOffsetAt(location.x, location.y,
113		rightOfChar);
114
115	if (rightOfChar)
116		caretOffset++;
117
118	_SetCaretOffset(caretOffset, true, extendSelection, true);
119}
120
121
122void
123TextEditor::SelectAll()
124{
125	if (!fDocument.IsSet())
126		return;
127
128	SetSelection(TextSelection(0, fDocument->Length()));
129}
130
131
132void
133TextEditor::SetSelection(TextSelection selection)
134{
135	_SetSelection(selection.Caret(), selection.Anchor(), true, true);
136}
137
138
139void
140TextEditor::SetCharacterStyle(::CharacterStyle style)
141{
142	if (fStyleAtCaret == style)
143		return;
144
145	fStyleAtCaret = style;
146
147	if (HasSelection()) {
148		// TODO: Apply style to selection range
149	}
150}
151
152
153void
154TextEditor::KeyDown(KeyEvent event)
155{
156	if (!fDocument.IsSet())
157		return;
158
159	bool select = (event.modifiers & B_SHIFT_KEY) != 0;
160
161	switch (event.key) {
162		case B_UP_ARROW:
163			LineUp(select);
164			break;
165
166		case B_DOWN_ARROW:
167			LineDown(select);
168			break;
169
170		case B_LEFT_ARROW:
171			if (HasSelection() && !select) {
172				_SetCaretOffset(
173					std::min(fSelection.Caret(), fSelection.Anchor()),
174					true, false, true);
175			} else
176				_SetCaretOffset(fSelection.Caret() - 1, true, select, true);
177			break;
178
179		case B_RIGHT_ARROW:
180			if (HasSelection() && !select) {
181				_SetCaretOffset(
182					std::max(fSelection.Caret(), fSelection.Anchor()),
183					true, false, true);
184			} else
185				_SetCaretOffset(fSelection.Caret() + 1, true, select, true);
186			break;
187
188		case B_HOME:
189			LineStart(select);
190			break;
191
192		case B_END:
193			LineEnd(select);
194			break;
195
196		case B_ENTER:
197			Insert(fSelection.Caret(), "\n");
198			break;
199
200		case B_TAB:
201			// TODO: Tab support in TextLayout
202			Insert(fSelection.Caret(), " ");
203			break;
204
205		case B_ESCAPE:
206			break;
207
208		case B_BACKSPACE:
209			if (HasSelection()) {
210				Remove(SelectionStart(), SelectionLength());
211			} else {
212				if (fSelection.Caret() > 0)
213					Remove(fSelection.Caret() - 1, 1);
214			}
215			break;
216
217		case B_DELETE:
218			if (HasSelection()) {
219				Remove(SelectionStart(), SelectionLength());
220			} else {
221				if (fSelection.Caret() < fDocument->Length())
222					Remove(fSelection.Caret(), 1);
223			}
224			break;
225
226		case B_INSERT:
227			// TODO: Toggle insert mode (or maybe just don't support it)
228			break;
229
230		case B_PAGE_UP:
231		case B_PAGE_DOWN:
232		case B_SUBSTITUTE:
233		case B_FUNCTION_KEY:
234		case B_KATAKANA_HIRAGANA:
235		case B_HANKAKU_ZENKAKU:
236			break;
237
238		default:
239			if (event.bytes != NULL && event.length > 0) {
240				// Handle null-termintating the string
241				BString text(event.bytes, event.length);
242
243				Replace(SelectionStart(), SelectionLength(), text);
244			}
245			break;
246	}
247}
248
249
250status_t
251TextEditor::Insert(int32 offset, const BString& string)
252{
253	if (!fEditingEnabled || !fDocument.IsSet())
254		return B_ERROR;
255
256	status_t ret = fDocument->Insert(offset, string, fStyleAtCaret);
257
258	if (ret == B_OK) {
259		_SetCaretOffset(offset + string.CountChars(), true, false, true);
260	}
261
262	return ret;
263}
264
265
266status_t
267TextEditor::Remove(int32 offset, int32 length)
268{
269	if (!fEditingEnabled || !fDocument.IsSet())
270		return B_ERROR;
271
272	status_t ret = fDocument->Remove(offset, length);
273
274	if (ret == B_OK) {
275		_SetCaretOffset(offset, true, false, true);
276	}
277
278	return ret;
279}
280
281
282status_t
283TextEditor::Replace(int32 offset, int32 length, const BString& string)
284{
285	if (!fEditingEnabled || !fDocument.IsSet())
286		return B_ERROR;
287
288	status_t ret = fDocument->Replace(offset, length, string);
289
290	if (ret == B_OK) {
291		_SetCaretOffset(offset + string.CountChars(), true, false, true);
292	}
293
294	return ret;
295}
296
297
298// #pragma mark -
299
300
301void
302TextEditor::LineUp(bool select)
303{
304	if (!fLayout.IsSet())
305		return;
306
307	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
308	_MoveToLine(lineIndex - 1, select);
309}
310
311
312void
313TextEditor::LineDown(bool select)
314{
315	if (!fLayout.IsSet())
316		return;
317
318	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
319	_MoveToLine(lineIndex + 1, select);
320}
321
322
323void
324TextEditor::LineStart(bool select)
325{
326	if (!fLayout.IsSet())
327		return;
328
329	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
330	_SetCaretOffset(fLayout->FirstOffsetOnLine(lineIndex), true, select,
331		true);
332}
333
334
335void
336TextEditor::LineEnd(bool select)
337{
338	if (!fLayout.IsSet())
339		return;
340
341	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
342	_SetCaretOffset(fLayout->LastOffsetOnLine(lineIndex), true, select,
343		true);
344}
345
346
347// #pragma mark -
348
349
350bool
351TextEditor::HasSelection() const
352{
353	return SelectionLength() > 0;
354}
355
356
357int32
358TextEditor::SelectionStart() const
359{
360	return std::min(fSelection.Caret(), fSelection.Anchor());
361}
362
363
364int32
365TextEditor::SelectionEnd() const
366{
367	return std::max(fSelection.Caret(), fSelection.Anchor());
368}
369
370
371int32
372TextEditor::SelectionLength() const
373{
374	return SelectionEnd() - SelectionStart();
375}
376
377
378// #pragma mark - private
379
380
381// _MoveToLine
382void
383TextEditor::_MoveToLine(int32 lineIndex, bool select)
384{
385	if (lineIndex < 0) {
386		// Move to beginning of line instead. Most editors do. Some only when
387		// selecting. Note that we are not updating the horizontal anchor here,
388		// even though the horizontal caret position changes. Most editors
389		// return to the previous horizonal offset when moving back down from
390		// the beginning of the line.
391		_SetCaretOffset(0, false, select, true);
392		return;
393	}
394	if (lineIndex >= fLayout->CountLines()) {
395		// Move to end of line instead, see above for why we do not update the
396		// horizontal anchor.
397		_SetCaretOffset(fDocument->Length(), false, select, true);
398		return;
399	}
400
401	float x1;
402	float y1;
403	float x2;
404	float y2;
405	fLayout->GetLineBounds(lineIndex , x1, y1, x2, y2);
406
407	bool rightOfCenter;
408	int32 textOffset = fLayout->TextOffsetAt(fCaretAnchorX, (y1 + y2) / 2,
409		rightOfCenter);
410
411	if (rightOfCenter)
412		textOffset++;
413
414	_SetCaretOffset(textOffset, false, select, true);
415}
416
417void
418TextEditor::_SetCaretOffset(int32 offset, bool updateAnchor,
419	bool lockSelectionAnchor, bool updateSelectionStyle)
420{
421	if (!fDocument.IsSet())
422		return;
423
424	if (offset < 0)
425		offset = 0;
426	int32 textLength = fDocument->Length();
427	if (offset > textLength)
428		offset = textLength;
429
430	int32 caret = offset;
431	int32 anchor = lockSelectionAnchor ? fSelection.Anchor() : offset;
432	_SetSelection(caret, anchor, updateAnchor, updateSelectionStyle);
433}
434
435
436void
437TextEditor::_SetSelection(int32 caret, int32 anchor, bool updateAnchor,
438	bool updateSelectionStyle)
439{
440	if (!fLayout.IsSet())
441		return;
442
443	if (caret == fSelection.Caret() && anchor == fSelection.Anchor())
444		return;
445
446	fSelection.SetCaret(caret);
447	fSelection.SetAnchor(anchor);
448
449	if (updateAnchor) {
450		float x1;
451		float y1;
452		float x2;
453		float y2;
454
455		fLayout->GetTextBounds(caret, x1, y1, x2, y2);
456		fCaretAnchorX = x1;
457	}
458
459	if (updateSelectionStyle)
460		_UpdateStyleAtCaret();
461}
462
463
464void
465TextEditor::_UpdateStyleAtCaret()
466{
467	if (!fDocument.IsSet())
468		return;
469
470	int32 offset = fSelection.Caret() - 1;
471	if (offset < 0)
472		offset = 0;
473	SetCharacterStyle(fDocument->CharacterStyleAt(offset));
474}
475
476
477