xref: /haiku/src/apps/haikudepot/textview/TextEditor.cpp (revision 37e5a036605931f55d82e971f8ab99c48023a5c4)
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.Get() == NULL || fLayout.Get() == NULL)
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.Get() == NULL)
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.Get() == NULL)
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.Get() == NULL)
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 		fDocument->PrintToStream();
262 	}
263 
264 	return ret;
265 }
266 
267 
268 status_t
269 TextEditor::Remove(int32 offset, int32 length)
270 {
271 	if (!fEditingEnabled || fDocument.Get() == NULL)
272 		return B_ERROR;
273 
274 	status_t ret = fDocument->Remove(offset, length);
275 
276 	if (ret == B_OK) {
277 		_SetCaretOffset(offset, true, false, true);
278 
279 		fDocument->PrintToStream();
280 	}
281 
282 	return ret;
283 }
284 
285 
286 status_t
287 TextEditor::Replace(int32 offset, int32 length, const BString& string)
288 {
289 	if (!fEditingEnabled || fDocument.Get() == NULL)
290 		return B_ERROR;
291 
292 	status_t ret = fDocument->Replace(offset, length, string);
293 
294 	if (ret == B_OK) {
295 		_SetCaretOffset(offset + string.CountChars(), true, false, true);
296 
297 		fDocument->PrintToStream();
298 	}
299 
300 	return ret;
301 }
302 
303 
304 // #pragma mark -
305 
306 
307 void
308 TextEditor::LineUp(bool select)
309 {
310 	if (fLayout.Get() == NULL)
311 		return;
312 
313 	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
314 	_MoveToLine(lineIndex - 1, select);
315 }
316 
317 
318 void
319 TextEditor::LineDown(bool select)
320 {
321 	if (fLayout.Get() == NULL)
322 		return;
323 
324 	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
325 	_MoveToLine(lineIndex + 1, select);
326 }
327 
328 
329 void
330 TextEditor::LineStart(bool select)
331 {
332 	if (fLayout.Get() == NULL)
333 		return;
334 
335 	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
336 	_SetCaretOffset(fLayout->FirstOffsetOnLine(lineIndex), true, select,
337 		true);
338 }
339 
340 
341 void
342 TextEditor::LineEnd(bool select)
343 {
344 	if (fLayout.Get() == NULL)
345 		return;
346 
347 	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
348 	_SetCaretOffset(fLayout->LastOffsetOnLine(lineIndex), true, select,
349 		true);
350 }
351 
352 
353 // #pragma mark -
354 
355 
356 bool
357 TextEditor::HasSelection() const
358 {
359 	return SelectionLength() > 0;
360 }
361 
362 
363 int32
364 TextEditor::SelectionStart() const
365 {
366 	return std::min(fSelection.Caret(), fSelection.Anchor());
367 }
368 
369 
370 int32
371 TextEditor::SelectionEnd() const
372 {
373 	return std::max(fSelection.Caret(), fSelection.Anchor());
374 }
375 
376 
377 int32
378 TextEditor::SelectionLength() const
379 {
380 	return SelectionEnd() - SelectionStart();
381 }
382 
383 
384 // #pragma mark - private
385 
386 
387 // _MoveToLine
388 void
389 TextEditor::_MoveToLine(int32 lineIndex, bool select)
390 {
391 	if (lineIndex < 0) {
392 		// Move to beginning of line instead. Most editors do. Some only when
393 		// selecting. Note that we are not updating the horizontal anchor here,
394 		// even though the horizontal caret position changes. Most editors
395 		// return to the previous horizonal offset when moving back down from
396 		// the beginning of the line.
397 		_SetCaretOffset(0, false, select, true);
398 		return;
399 	}
400 	if (lineIndex >= fLayout->CountLines()) {
401 		// Move to end of line instead, see above for why we do not update the
402 		// horizontal anchor.
403 		_SetCaretOffset(fDocument->Length(), false, select, true);
404 		return;
405 	}
406 
407 	float x1;
408 	float y1;
409 	float x2;
410 	float y2;
411 	fLayout->GetLineBounds(lineIndex , x1, y1, x2, y2);
412 
413 	bool rightOfCenter;
414 	int32 textOffset = fLayout->TextOffsetAt(fCaretAnchorX, (y1 + y2) / 2,
415 		rightOfCenter);
416 
417 	if (rightOfCenter)
418 		textOffset++;
419 
420 	_SetCaretOffset(textOffset, false, select, true);
421 }
422 
423 void
424 TextEditor::_SetCaretOffset(int32 offset, bool updateAnchor,
425 	bool lockSelectionAnchor, bool updateSelectionStyle)
426 {
427 	if (fDocument.Get() == NULL)
428 		return;
429 
430 	if (offset < 0)
431 		offset = 0;
432 	int32 textLength = fDocument->Length();
433 	if (offset > textLength)
434 		offset = textLength;
435 
436 	int32 caret = offset;
437 	int32 anchor = lockSelectionAnchor ? fSelection.Anchor() : offset;
438 	_SetSelection(caret, anchor, updateAnchor, updateSelectionStyle);
439 }
440 
441 
442 void
443 TextEditor::_SetSelection(int32 caret, int32 anchor, bool updateAnchor,
444 	bool updateSelectionStyle)
445 {
446 	if (fLayout.Get() == NULL)
447 		return;
448 
449 	if (caret == fSelection.Caret() && anchor == fSelection.Anchor())
450 		return;
451 
452 	fSelection.SetCaret(caret);
453 	fSelection.SetAnchor(anchor);
454 
455 	if (updateAnchor) {
456 		float x1;
457 		float y1;
458 		float x2;
459 		float y2;
460 
461 		fLayout->GetTextBounds(caret, x1, y1, x2, y2);
462 		fCaretAnchorX = x1;
463 	}
464 
465 	if (updateSelectionStyle)
466 		_UpdateStyleAtCaret();
467 }
468 
469 
470 void
471 TextEditor::_UpdateStyleAtCaret()
472 {
473 	if (fDocument.Get() == NULL)
474 		return;
475 
476 	int32 offset = fSelection.Caret() - 1;
477 	if (offset < 0)
478 		offset = 0;
479 	SetCharacterStyle(fDocument->CharacterStyleAt(offset));
480 }
481 
482 
483