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