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.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 123 TextEditor::SelectAll() 124 { 125 if (!fDocument.IsSet()) 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.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 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 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 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 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 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 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 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 351 TextEditor::HasSelection() const 352 { 353 return SelectionLength() > 0; 354 } 355 356 357 int32 358 TextEditor::SelectionStart() const 359 { 360 return std::min(fSelection.Caret(), fSelection.Anchor()); 361 } 362 363 364 int32 365 TextEditor::SelectionEnd() const 366 { 367 return std::max(fSelection.Caret(), fSelection.Anchor()); 368 } 369 370 371 int32 372 TextEditor::SelectionLength() const 373 { 374 return SelectionEnd() - SelectionStart(); 375 } 376 377 378 // #pragma mark - private 379 380 381 // _MoveToLine 382 void 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 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 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 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