xref: /haiku/src/apps/haikudepot/textview/TextEditor.cpp (revision 0754c319592cd8a523959d85fb06ab23c64a98a6)
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::SetSelection(TextSelection selection)
124 {
125 	_SetSelection(selection.Caret(), selection.Anchor(), true, true);
126 }
127 
128 
129 void
130 TextEditor::SetCharacterStyle(::CharacterStyle style)
131 {
132 	if (fStyleAtCaret == style)
133 		return;
134 
135 	fStyleAtCaret = style;
136 
137 	if (HasSelection()) {
138 		// TODO: Apply style to selection range
139 	}
140 }
141 
142 
143 void
144 TextEditor::KeyDown(KeyEvent event)
145 {
146 	if (fDocument.Get() == NULL)
147 		return;
148 
149 	bool select = (event.modifiers & B_SHIFT_KEY) != 0;
150 
151 	switch (event.key) {
152 		case B_UP_ARROW:
153 			LineUp(select);
154 			break;
155 
156 		case B_DOWN_ARROW:
157 			LineDown(select);
158 			break;
159 
160 		case B_LEFT_ARROW:
161 			if (HasSelection() && !select) {
162 				_SetCaretOffset(
163 					std::min(fSelection.Caret(), fSelection.Anchor()),
164 					true, false, true);
165 			} else
166 				_SetCaretOffset(fSelection.Caret() - 1, true, select, true);
167 			break;
168 
169 		case B_RIGHT_ARROW:
170 			if (HasSelection() && !select) {
171 				_SetCaretOffset(
172 					std::max(fSelection.Caret(), fSelection.Anchor()),
173 					true, false, true);
174 			} else
175 				_SetCaretOffset(fSelection.Caret() + 1, true, select, true);
176 			break;
177 
178 		case B_HOME:
179 			LineStart(select);
180 			break;
181 
182 		case B_END:
183 			LineEnd(select);
184 			break;
185 
186 		case B_ENTER:
187 			Insert(fSelection.Caret(), "\n");
188 			break;
189 
190 		case B_TAB:
191 			// TODO: Tab support in TextLayout
192 			Insert(fSelection.Caret(), " ");
193 			break;
194 
195 		case B_ESCAPE:
196 			break;
197 
198 		case B_BACKSPACE:
199 			if (HasSelection()) {
200 				Remove(SelectionStart(), SelectionLength());
201 			} else {
202 				if (fSelection.Caret() > 0)
203 					Remove(fSelection.Caret() - 1, 1);
204 			}
205 			break;
206 
207 		case B_DELETE:
208 			if (HasSelection()) {
209 				Remove(SelectionStart(), SelectionLength());
210 			} else {
211 				if (fSelection.Caret() < fDocument->Length())
212 					Remove(fSelection.Caret(), 1);
213 			}
214 			break;
215 
216 		case B_INSERT:
217 			// TODO: Toggle insert mode (or maybe just don't support it)
218 			break;
219 
220 		case B_PAGE_UP:
221 		case B_PAGE_DOWN:
222 		case B_SUBSTITUTE:
223 		case B_FUNCTION_KEY:
224 		case B_KATAKANA_HIRAGANA:
225 		case B_HANKAKU_ZENKAKU:
226 			break;
227 
228 		default:
229 			if (event.bytes != NULL && event.length > 0) {
230 				// Handle null-termintating the string
231 				BString text(event.bytes, event.length);
232 
233 				// Remove selection, if any
234 				if (HasSelection())
235 					Remove(SelectionStart(), SelectionLength());
236 
237 				Insert(fSelection.Caret(), text);
238 			}
239 			break;
240 	}
241 }
242 
243 
244 status_t
245 TextEditor::Insert(int32 offset, const BString& string)
246 {
247 	if (!fEditingEnabled || fDocument.Get() == NULL)
248 		return B_ERROR;
249 
250 	status_t ret = fDocument->Insert(offset, string, fStyleAtCaret);
251 
252 	if (ret == B_OK) {
253 		// TODO: Via listener, and only affected paragraphs
254 		fLayout->Invalidate();
255 
256 		_SetCaretOffset(offset + string.CountChars(), true, false, true);
257 
258 		fDocument->PrintToStream();
259 	}
260 
261 	return ret;
262 }
263 
264 
265 status_t
266 TextEditor::Remove(int32 offset, int32 length)
267 {
268 	if (!fEditingEnabled || fDocument.Get() == NULL)
269 		return B_ERROR;
270 
271 	status_t ret = fDocument->Remove(offset, length);
272 
273 	if (ret == B_OK) {
274 		// TODO: Via listener, and only affected paragraphs
275 		fLayout->Invalidate();
276 
277 		_SetCaretOffset(offset, true, false, true);
278 
279 		fDocument->PrintToStream();
280 	}
281 
282 	return ret;
283 }
284 
285 
286 // #pragma mark -
287 
288 
289 void
290 TextEditor::LineUp(bool select)
291 {
292 	if (fLayout.Get() == NULL)
293 		return;
294 
295 	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
296 	_MoveToLine(lineIndex - 1, select);
297 }
298 
299 
300 void
301 TextEditor::LineDown(bool select)
302 {
303 	if (fLayout.Get() == NULL)
304 		return;
305 
306 	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
307 	_MoveToLine(lineIndex + 1, select);
308 }
309 
310 
311 void
312 TextEditor::LineStart(bool select)
313 {
314 	if (fLayout.Get() == NULL)
315 		return;
316 
317 	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
318 	_SetCaretOffset(fLayout->FirstOffsetOnLine(lineIndex), true, select,
319 		true);
320 }
321 
322 
323 void
324 TextEditor::LineEnd(bool select)
325 {
326 	if (fLayout.Get() == NULL)
327 		return;
328 
329 	int32 lineIndex = fLayout->LineIndexForOffset(fSelection.Caret());
330 	_SetCaretOffset(fLayout->LastOffsetOnLine(lineIndex), true, select,
331 		true);
332 }
333 
334 
335 // #pragma mark -
336 
337 
338 bool
339 TextEditor::HasSelection() const
340 {
341 	return SelectionLength() > 0;
342 }
343 
344 
345 int32
346 TextEditor::SelectionStart() const
347 {
348 	return std::min(fSelection.Caret(), fSelection.Anchor());
349 }
350 
351 
352 int32
353 TextEditor::SelectionEnd() const
354 {
355 	return std::max(fSelection.Caret(), fSelection.Anchor());
356 }
357 
358 
359 int32
360 TextEditor::SelectionLength() const
361 {
362 	return SelectionEnd() - SelectionStart();
363 }
364 
365 
366 // #pragma mark - private
367 
368 
369 // _MoveToLine
370 void
371 TextEditor::_MoveToLine(int32 lineIndex, bool select)
372 {
373 	if (lineIndex < 0) {
374 		// Move to beginning of line instead. Most editors do. Some only when
375 		// selecting. Note that we are not updating the horizontal anchor here,
376 		// even though the horizontal caret position changes. Most editors
377 		// return to the previous horizonal offset when moving back down from
378 		// the beginning of the line.
379 		_SetCaretOffset(0, false, select, true);
380 		return;
381 	}
382 	if (lineIndex >= fLayout->CountLines()) {
383 		// Move to end of line instead, see above for why we do not update the
384 		// horizontal anchor.
385 		_SetCaretOffset(fDocument->Length(), false, select, true);
386 		return;
387 	}
388 
389 	float x1;
390 	float y1;
391 	float x2;
392 	float y2;
393 	fLayout->GetLineBounds(lineIndex , x1, y1, x2, y2);
394 
395 	bool rightOfCenter;
396 	int32 textOffset = fLayout->TextOffsetAt(fCaretAnchorX, (y1 + y2) / 2,
397 		rightOfCenter);
398 
399 	if (rightOfCenter)
400 		textOffset++;
401 
402 	_SetCaretOffset(textOffset, false, select, true);
403 }
404 
405 void
406 TextEditor::_SetCaretOffset(int32 offset, bool updateAnchor,
407 	bool lockSelectionAnchor, bool updateSelectionStyle)
408 {
409 	if (fDocument.Get() == NULL)
410 		return;
411 
412 	if (offset < 0)
413 		offset = 0;
414 	int32 textLength = fDocument->Length();
415 	if (offset > textLength)
416 		offset = textLength;
417 
418 	int32 caret = offset;
419 	int32 anchor = lockSelectionAnchor ? fSelection.Anchor() : offset;
420 	_SetSelection(caret, anchor, updateAnchor, updateSelectionStyle);
421 }
422 
423 
424 void
425 TextEditor::_SetSelection(int32 caret, int32 anchor, bool updateAnchor,
426 	bool updateSelectionStyle)
427 {
428 	if (fLayout.Get() == NULL)
429 		return;
430 
431 	if (caret == fSelection.Caret() && anchor == fSelection.Anchor())
432 		return;
433 
434 	fSelection.SetCaret(caret);
435 	fSelection.SetAnchor(anchor);
436 
437 	if (updateAnchor) {
438 		float x1;
439 		float y1;
440 		float x2;
441 		float y2;
442 
443 		fLayout->GetTextBounds(caret, x1, y1, x2, y2);
444 		fCaretAnchorX = x1;
445 	}
446 
447 	if (updateSelectionStyle)
448 		_UpdateStyleAtCaret();
449 }
450 
451 
452 void
453 TextEditor::_UpdateStyleAtCaret()
454 {
455 	if (fDocument.Get() == NULL)
456 		return;
457 
458 	int32 offset = fSelection.Caret() - 1;
459 	if (offset < 0)
460 		offset = 0;
461 	SetCharacterStyle(fDocument->CharacterStyleAt(offset));
462 }
463 
464 
465