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
TextEditor()12 TextEditor::TextEditor()
13 :
14 fDocument(),
15 fLayout(),
16 fSelection(),
17 fCaretAnchorX(0.0f),
18 fStyleAtCaret(),
19 fEditingEnabled(true)
20 {
21 }
22
23
TextEditor(const TextEditor & other)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
~TextEditor()36 TextEditor::~TextEditor()
37 {
38 }
39
40
41 TextEditor&
operator =(const TextEditor & other)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
operator ==(const TextEditor & other) const58 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
operator !=(const TextEditor & other) const73 TextEditor::operator!=(const TextEditor& other) const
74 {
75 return !(*this == other);
76 }
77
78
79 // #pragma mark -
80
81
82 void
SetDocument(const TextDocumentRef & ref)83 TextEditor::SetDocument(const TextDocumentRef& ref)
84 {
85 fDocument = ref;
86 SetSelection(TextSelection());
87 }
88
89
90 void
SetLayout(const TextDocumentLayoutRef & ref)91 TextEditor::SetLayout(const TextDocumentLayoutRef& ref)
92 {
93 fLayout = ref;
94 SetSelection(TextSelection());
95 }
96
97
98 void
SetEditingEnabled(bool enabled)99 TextEditor::SetEditingEnabled(bool enabled)
100 {
101 fEditingEnabled = enabled;
102 }
103
104
105 void
SetCaret(BPoint location,bool extendSelection)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
SelectAll()123 TextEditor::SelectAll()
124 {
125 if (!fDocument.IsSet())
126 return;
127
128 SetSelection(TextSelection(0, fDocument->Length()));
129 }
130
131
132 void
SetSelection(TextSelection selection)133 TextEditor::SetSelection(TextSelection selection)
134 {
135 _SetSelection(selection.Caret(), selection.Anchor(), true, true);
136 }
137
138
139 void
SetCharacterStyle(::CharacterStyle style)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
KeyDown(KeyEvent event)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
Insert(int32 offset,const BString & string)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
Remove(int32 offset,int32 length)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
Replace(int32 offset,int32 length,const BString & string)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
LineUp(bool select)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
LineDown(bool select)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
LineStart(bool select)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
LineEnd(bool select)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
HasSelection() const351 TextEditor::HasSelection() const
352 {
353 return SelectionLength() > 0;
354 }
355
356
357 int32
SelectionStart() const358 TextEditor::SelectionStart() const
359 {
360 return std::min(fSelection.Caret(), fSelection.Anchor());
361 }
362
363
364 int32
SelectionEnd() const365 TextEditor::SelectionEnd() const
366 {
367 return std::max(fSelection.Caret(), fSelection.Anchor());
368 }
369
370
371 int32
SelectionLength() const372 TextEditor::SelectionLength() const
373 {
374 return SelectionEnd() - SelectionStart();
375 }
376
377
378 // #pragma mark - private
379
380
381 // _MoveToLine
382 void
_MoveToLine(int32 lineIndex,bool select)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
_SetCaretOffset(int32 offset,bool updateAnchor,bool lockSelectionAnchor,bool updateSelectionStyle)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
_SetSelection(int32 caret,int32 anchor,bool updateAnchor,bool updateSelectionStyle)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
_UpdateStyleAtCaret()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