xref: /haiku/src/apps/haikudepot/textview/TextEditor.cpp (revision ed24eb5ff12640d052171c6a7feba37fab8a75d1)
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 
12 TextEditor::TextEditor()
13 	:
14 	fDocument(),
15 	fLayout(),
16 	fSelection(),
17 	fCaretAnchorX(0.0f),
18 	fStyleAtCaret(),
19 	fEditingEnabled(true)
20 {
21 }
22 
23 
24 TextEditor::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 
36 TextEditor::~TextEditor()
37 {
38 }
39 
40 
41 TextEditor&
42 TextEditor::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 
57 bool
58 TextEditor::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 
72 bool
73 TextEditor::operator!=(const TextEditor& other) const
74 {
75 	return !(*this == other);
76 }
77 
78 
79 // #pragma mark -
80 
81 
82 void
83 TextEditor::SetDocument(const TextDocumentRef& ref)
84 {
85 	fDocument = ref;
86 	SetSelection(TextSelection());
87 }
88 
89 
90 void
91 TextEditor::SetLayout(const TextDocumentLayoutRef& ref)
92 {
93 	fLayout = ref;
94 	SetSelection(TextSelection());
95 }
96 
97 
98 void
99 TextEditor::SetEditingEnabled(bool enabled)
100 {
101 	fEditingEnabled = enabled;
102 }
103 
104 
105 void
106 TextEditor::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 
122 void
123 TextEditor::SelectAll()
124 {
125 	if (!fDocument.IsSet())
126 		return;
127 
128 	SetSelection(TextSelection(0, fDocument->Length()));
129 }
130 
131 
132 void
133 TextEditor::SetSelection(TextSelection selection)
134 {
135 	_SetSelection(selection.Caret(), selection.Anchor(), true, true);
136 }
137 
138 
139 void
140 TextEditor::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 
153 void
154 TextEditor::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 
250 status_t
251 TextEditor::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 
266 status_t
267 TextEditor::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 
282 status_t
283 TextEditor::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 
301 void
302 TextEditor::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 
312 void
313 TextEditor::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 
323 void
324 TextEditor::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 
335 void
336 TextEditor::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 
350 bool
351 TextEditor::HasSelection() const
352 {
353 	return SelectionLength() > 0;
354 }
355 
356 
357 int32
358 TextEditor::SelectionStart() const
359 {
360 	return std::min(fSelection.Caret(), fSelection.Anchor());
361 }
362 
363 
364 int32
365 TextEditor::SelectionEnd() const
366 {
367 	return std::max(fSelection.Caret(), fSelection.Anchor());
368 }
369 
370 
371 int32
372 TextEditor::SelectionLength() const
373 {
374 	return SelectionEnd() - SelectionStart();
375 }
376 
377 
378 // #pragma mark - private
379 
380 
381 // _MoveToLine
382 void
383 TextEditor::_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 
417 void
418 TextEditor::_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 
436 void
437 TextEditor::_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 
464 void
465 TextEditor::_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