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