1 /* 2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>. 3 * All rights reserved. Distributed under the terms of the MIT License. 4 */ 5 6 #include "TextDocument.h" 7 8 #include <algorithm> 9 #include <stdio.h> 10 11 12 TextDocument::TextDocument() 13 : 14 fParagraphs(), 15 fEmptyLastParagraph(), 16 fDefaultCharacterStyle() 17 { 18 } 19 20 21 TextDocument::TextDocument(CharacterStyle characterStyle, 22 ParagraphStyle paragraphStyle) 23 : 24 fParagraphs(), 25 fEmptyLastParagraph(paragraphStyle), 26 fDefaultCharacterStyle(characterStyle) 27 { 28 } 29 30 31 TextDocument::TextDocument(const TextDocument& other) 32 : 33 fParagraphs(other.fParagraphs), 34 fEmptyLastParagraph(other.fEmptyLastParagraph), 35 fDefaultCharacterStyle(other.fDefaultCharacterStyle) 36 { 37 } 38 39 40 TextDocument& 41 TextDocument::operator=(const TextDocument& other) 42 { 43 fParagraphs = other.fParagraphs; 44 fEmptyLastParagraph = other.fEmptyLastParagraph; 45 fDefaultCharacterStyle = other.fDefaultCharacterStyle; 46 47 return *this; 48 } 49 50 51 bool 52 TextDocument::operator==(const TextDocument& other) const 53 { 54 if (this == &other) 55 return true; 56 57 return fEmptyLastParagraph == other.fEmptyLastParagraph 58 && fDefaultCharacterStyle == other.fDefaultCharacterStyle 59 && fParagraphs == other.fParagraphs; 60 } 61 62 63 bool 64 TextDocument::operator!=(const TextDocument& other) const 65 { 66 return !(*this == other); 67 } 68 69 70 // #pragma mark - 71 72 73 status_t 74 TextDocument::Insert(int32 textOffset, const BString& text) 75 { 76 return Replace(textOffset, 0, text); 77 } 78 79 80 status_t 81 TextDocument::Insert(int32 textOffset, const BString& text, 82 CharacterStyle style) 83 { 84 return Replace(textOffset, 0, text, style); 85 } 86 87 88 status_t 89 TextDocument::Insert(int32 textOffset, const BString& text, 90 CharacterStyle characterStyle, ParagraphStyle paragraphStyle) 91 { 92 return Replace(textOffset, 0, text, characterStyle, paragraphStyle); 93 } 94 95 96 // #pragma mark - 97 98 99 status_t 100 TextDocument::Remove(int32 textOffset, int32 length) 101 { 102 return Replace(textOffset, length, BString()); 103 } 104 105 106 // #pragma mark - 107 108 109 status_t 110 TextDocument::Replace(int32 textOffset, int32 length, const BString& text) 111 { 112 return Replace(textOffset, length, text, CharacterStyleAt(textOffset)); 113 } 114 115 116 status_t 117 TextDocument::Replace(int32 textOffset, int32 length, const BString& text, 118 CharacterStyle style) 119 { 120 return Replace(textOffset, length, text, style, 121 ParagraphStyleAt(textOffset)); 122 } 123 124 125 status_t 126 TextDocument::Replace(int32 textOffset, int32 length, const BString& text, 127 CharacterStyle characterStyle, ParagraphStyle paragraphStyle) 128 { 129 TextDocumentRef document = NormalizeText(text, characterStyle, 130 paragraphStyle); 131 if (document.Get() == NULL || document->Length() != text.CountChars()) 132 return B_NO_MEMORY; 133 return Replace(textOffset, length, document); 134 } 135 136 137 status_t 138 TextDocument::Replace(int32 textOffset, int32 length, TextDocumentRef document) 139 { 140 int32 firstParagraph = 0; 141 int32 paragraphCount = 0; 142 143 // TODO: Call _NotifyTextChanging() before any change happened 144 145 status_t ret = _Remove(textOffset, length, firstParagraph, paragraphCount); 146 if (ret != B_OK) 147 return ret; 148 149 ret = _Insert(textOffset, document, firstParagraph, paragraphCount); 150 151 _NotifyTextChanged(TextChangedEvent(firstParagraph, paragraphCount)); 152 153 return ret; 154 } 155 156 157 // #pragma mark - 158 159 160 const CharacterStyle& 161 TextDocument::CharacterStyleAt(int32 textOffset) const 162 { 163 int32 paragraphOffset; 164 const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); 165 166 textOffset -= paragraphOffset; 167 const TextSpanList& spans = paragraph.TextSpans(); 168 169 int32 index = 0; 170 while (index < spans.CountItems()) { 171 const TextSpan& span = spans.ItemAtFast(index); 172 if (textOffset - span.CountChars() < 0) 173 return span.Style(); 174 textOffset -= span.CountChars(); 175 index++; 176 } 177 178 return fDefaultCharacterStyle; 179 } 180 181 182 const ParagraphStyle& 183 TextDocument::ParagraphStyleAt(int32 textOffset) const 184 { 185 int32 paragraphOffset; 186 return ParagraphAt(textOffset, paragraphOffset).Style(); 187 } 188 189 190 // #pragma mark - 191 192 193 int32 194 TextDocument::CountParagraphs() const 195 { 196 return fParagraphs.CountItems(); 197 } 198 199 200 int32 201 TextDocument::ParagraphIndexFor(int32 textOffset, int32& paragraphOffset) const 202 { 203 // TODO: Could binary search the Paragraphs if they were wrapped in classes 204 // that knew there text offset in the document. 205 int32 textLength = 0; 206 paragraphOffset = 0; 207 int32 count = fParagraphs.CountItems(); 208 for (int32 i = 0; i < count; i++) { 209 const Paragraph& paragraph = fParagraphs.ItemAtFast(i); 210 int32 paragraphLength = paragraph.Length(); 211 textLength += paragraphLength; 212 if (textLength > textOffset 213 || (i == count - 1 && textLength == textOffset)) { 214 return i; 215 } 216 paragraphOffset += paragraphLength; 217 } 218 return -1; 219 } 220 221 222 const Paragraph& 223 TextDocument::ParagraphAt(int32 textOffset, int32& paragraphOffset) const 224 { 225 int32 index = ParagraphIndexFor(textOffset, paragraphOffset); 226 if (index >= 0) 227 return fParagraphs.ItemAtFast(index); 228 229 return fEmptyLastParagraph; 230 } 231 232 233 const Paragraph& 234 TextDocument::ParagraphAt(int32 index) const 235 { 236 if (index >= 0 && index < fParagraphs.CountItems()) 237 return fParagraphs.ItemAtFast(index); 238 return fEmptyLastParagraph; 239 } 240 241 242 bool 243 TextDocument::Append(const Paragraph& paragraph) 244 { 245 return fParagraphs.Add(paragraph); 246 } 247 248 249 int32 250 TextDocument::Length() const 251 { 252 // TODO: Could be O(1) if the Paragraphs were wrapped in classes that 253 // knew their text offset in the document. 254 int32 textLength = 0; 255 int32 count = fParagraphs.CountItems(); 256 for (int32 i = 0; i < count; i++) { 257 const Paragraph& paragraph = fParagraphs.ItemAtFast(i); 258 textLength += paragraph.Length(); 259 } 260 return textLength; 261 } 262 263 264 BString 265 TextDocument::Text() const 266 { 267 return Text(0, Length()); 268 } 269 270 271 BString 272 TextDocument::Text(int32 start, int32 length) const 273 { 274 if (start < 0) 275 start = 0; 276 277 BString text; 278 279 int32 count = fParagraphs.CountItems(); 280 for (int32 i = 0; i < count; i++) { 281 const Paragraph& paragraph = fParagraphs.ItemAtFast(i); 282 int32 paragraphLength = paragraph.Length(); 283 if (paragraphLength == 0) 284 continue; 285 if (start > paragraphLength) { 286 // Skip paragraph if its before start 287 start -= paragraphLength; 288 continue; 289 } 290 291 // Remaining paragraph length after start 292 paragraphLength -= start; 293 int32 copyLength = std::min(paragraphLength, length); 294 295 text << paragraph.Text(start, copyLength); 296 297 length -= copyLength; 298 if (length == 0) 299 break; 300 301 // Next paragraph is copied from its beginning 302 start = 0; 303 } 304 305 return text; 306 } 307 308 309 TextDocumentRef 310 TextDocument::SubDocument(int32 start, int32 length) const 311 { 312 TextDocumentRef result(new(std::nothrow) TextDocument( 313 fDefaultCharacterStyle, fEmptyLastParagraph.Style()), true); 314 315 if (result.Get() == NULL) 316 return result; 317 318 if (start < 0) 319 start = 0; 320 321 int32 count = fParagraphs.CountItems(); 322 for (int32 i = 0; i < count; i++) { 323 const Paragraph& paragraph = fParagraphs.ItemAtFast(i); 324 int32 paragraphLength = paragraph.Length(); 325 if (paragraphLength == 0) 326 continue; 327 if (start > paragraphLength) { 328 // Skip paragraph if its before start 329 start -= paragraphLength; 330 continue; 331 } 332 333 // Remaining paragraph length after start 334 paragraphLength -= start; 335 int32 copyLength = std::min(paragraphLength, length); 336 337 result->Append(paragraph.SubParagraph(start, copyLength)); 338 339 length -= copyLength; 340 if (length == 0) 341 break; 342 343 // Next paragraph is copied from its beginning 344 start = 0; 345 } 346 347 return result; 348 } 349 350 351 // #pragma mark - 352 353 354 void 355 TextDocument::PrintToStream() const 356 { 357 int32 paragraphCount = fParagraphs.CountItems(); 358 if (paragraphCount == 0) { 359 printf("<document/>\n"); 360 return; 361 } 362 printf("<document>\n"); 363 for (int32 i = 0; i < paragraphCount; i++) { 364 fParagraphs.ItemAtFast(i).PrintToStream(); 365 } 366 printf("</document>\n"); 367 } 368 369 370 /*static*/ TextDocumentRef 371 TextDocument::NormalizeText(const BString& text, 372 CharacterStyle characterStyle, ParagraphStyle paragraphStyle) 373 { 374 TextDocumentRef document(new(std::nothrow) TextDocument(characterStyle, 375 paragraphStyle), true); 376 if (document.Get() == NULL) 377 throw B_NO_MEMORY; 378 379 Paragraph paragraph(paragraphStyle); 380 381 // Append TextSpans, splitting 'text' into Paragraphs at line breaks. 382 int32 length = text.CountChars(); 383 int32 chunkStart = 0; 384 while (chunkStart < length) { 385 int32 chunkEnd = text.FindFirst('\n', chunkStart); 386 bool foundLineBreak = chunkEnd >= chunkStart; 387 if (foundLineBreak) 388 chunkEnd++; 389 else 390 chunkEnd = length; 391 392 BString chunk; 393 text.CopyCharsInto(chunk, chunkStart, chunkEnd - chunkStart); 394 TextSpan span(chunk, characterStyle); 395 396 if (!paragraph.Append(span)) 397 throw B_NO_MEMORY; 398 if (paragraph.Length() > 0 && !document->Append(paragraph)) 399 throw B_NO_MEMORY; 400 401 paragraph = Paragraph(paragraphStyle); 402 chunkStart = chunkEnd + 1; 403 } 404 405 return document; 406 } 407 408 409 // #pragma mark - 410 411 412 bool 413 TextDocument::AddListener(TextListenerRef listener) 414 { 415 return fTextListeners.Add(listener); 416 } 417 418 419 bool 420 TextDocument::RemoveListener(TextListenerRef listener) 421 { 422 return fTextListeners.Remove(listener); 423 } 424 425 426 bool 427 TextDocument::AddUndoListener(UndoableEditListenerRef listener) 428 { 429 return fUndoListeners.Add(listener); 430 } 431 432 433 bool 434 TextDocument::RemoveUndoListener(UndoableEditListenerRef listener) 435 { 436 return fUndoListeners.Remove(listener); 437 } 438 439 440 // #pragma mark - private 441 442 443 status_t 444 TextDocument::_Insert(int32 textOffset, TextDocumentRef document, 445 int32& index, int32& paragraphCount) 446 { 447 int32 paragraphOffset; 448 index = ParagraphIndexFor(textOffset, paragraphOffset); 449 if (index < 0) 450 return B_BAD_VALUE; 451 452 if (document->Length() == 0) 453 return B_OK; 454 455 textOffset -= paragraphOffset; 456 457 bool hasLineBreaks; 458 if (document->CountParagraphs() > 1) { 459 hasLineBreaks = true; 460 } else { 461 const Paragraph& paragraph = document->ParagraphAt(0); 462 hasLineBreaks = paragraph.EndsWith("\n"); 463 } 464 465 if (hasLineBreaks) { 466 // Split paragraph at textOffset 467 Paragraph paragraph1(ParagraphAt(index).Style()); 468 Paragraph paragraph2(document->ParagraphAt( 469 document->CountParagraphs() - 1).Style()); 470 { 471 const TextSpanList& textSpans = ParagraphAt(index).TextSpans(); 472 int32 spanCount = textSpans.CountItems(); 473 for (int32 i = 0; i < spanCount; i++) { 474 const TextSpan& span = textSpans.ItemAtFast(i); 475 int32 spanLength = span.CountChars(); 476 if (textOffset >= spanLength) { 477 if (!paragraph1.Append(span)) 478 return B_NO_MEMORY; 479 textOffset -= spanLength; 480 } else if (textOffset > 0) { 481 if (!paragraph1.Append( 482 span.SubSpan(0, textOffset)) 483 || !paragraph2.Append( 484 span.SubSpan(textOffset, 485 spanLength - textOffset))) { 486 return B_NO_MEMORY; 487 } 488 textOffset = 0; 489 } else { 490 if (!paragraph2.Append(span)) 491 return B_NO_MEMORY; 492 } 493 } 494 } 495 496 fParagraphs.Remove(index); 497 498 // Append first paragraph in other document to first part of 499 // paragraph at insert position 500 { 501 const Paragraph& otherParagraph = document->ParagraphAt(0); 502 const TextSpanList& textSpans = otherParagraph.TextSpans(); 503 int32 spanCount = textSpans.CountItems(); 504 for (int32 i = 0; i < spanCount; i++) { 505 const TextSpan& span = textSpans.ItemAtFast(i); 506 // TODO: Import/map CharacterStyles 507 if (!paragraph1.Append(span)) 508 return B_NO_MEMORY; 509 } 510 } 511 512 // Insert the first paragraph-part again to the document 513 if (!fParagraphs.Add(paragraph1, index)) 514 return B_NO_MEMORY; 515 paragraphCount++; 516 517 // Insert the other document's paragraph save for the last one 518 for (int32 i = 1; i < document->CountParagraphs() - 1; i++) { 519 const Paragraph& otherParagraph = document->ParagraphAt(i); 520 // TODO: Import/map CharacterStyles and ParagraphStyle 521 if (!fParagraphs.Add(otherParagraph, ++index)) 522 return B_NO_MEMORY; 523 paragraphCount++; 524 } 525 526 int32 lastIndex = document->CountParagraphs() - 1; 527 if (lastIndex > 0) { 528 const Paragraph& otherParagraph = document->ParagraphAt(lastIndex); 529 if (otherParagraph.EndsWith("\n")) { 530 // TODO: Import/map CharacterStyles and ParagraphStyle 531 if (!fParagraphs.Add(otherParagraph, ++index)) 532 return B_NO_MEMORY; 533 } else { 534 const TextSpanList& textSpans = otherParagraph.TextSpans(); 535 int32 spanCount = textSpans.CountItems(); 536 for (int32 i = 0; i < spanCount; i++) { 537 const TextSpan& span = textSpans.ItemAtFast(i); 538 // TODO: Import/map CharacterStyles 539 if (!paragraph2.Prepend(span)) 540 return B_NO_MEMORY; 541 } 542 } 543 } 544 545 // Insert back the second paragraph-part 546 if (paragraph2.IsEmpty()) { 547 // Make sure Paragraph has at least one TextSpan, even 548 // if its empty. This handles the case of inserting a 549 // line-break at the end of the document. It than needs to 550 // have a new, empty paragraph at the end. 551 const TextSpanList& spans = paragraph1.TextSpans(); 552 const TextSpan& span = spans.LastItem(); 553 if (!paragraph2.Append(TextSpan("", span.Style()))) 554 return B_NO_MEMORY; 555 } 556 557 if (!fParagraphs.Add(paragraph2, ++index)) 558 return B_NO_MEMORY; 559 560 paragraphCount++; 561 } else { 562 Paragraph paragraph(ParagraphAt(index)); 563 const Paragraph& otherParagraph = document->ParagraphAt(0); 564 565 const TextSpanList& textSpans = otherParagraph.TextSpans(); 566 int32 spanCount = textSpans.CountItems(); 567 for (int32 i = 0; i < spanCount; i++) { 568 const TextSpan& span = textSpans.ItemAtFast(i); 569 paragraph.Insert(textOffset, span); 570 textOffset += span.CountChars(); 571 } 572 573 if (!fParagraphs.Replace(index, paragraph)) 574 return B_NO_MEMORY; 575 576 paragraphCount++; 577 } 578 579 return B_OK; 580 } 581 582 583 status_t 584 TextDocument::_Remove(int32 textOffset, int32 length, int32& index, 585 int32& paragraphCount) 586 { 587 if (length == 0) 588 return B_OK; 589 590 int32 paragraphOffset; 591 index = ParagraphIndexFor(textOffset, paragraphOffset); 592 if (index < 0) 593 return B_BAD_VALUE; 594 595 textOffset -= paragraphOffset; 596 paragraphCount++; 597 598 // The paragraph at the text offset remains, even if the offset is at 599 // the beginning of that paragraph. The idea is that the selection start 600 // stays visually in the same place. Therefore, the paragraph at that 601 // offset has to keep the paragraph style from that paragraph. 602 603 Paragraph resultParagraph(ParagraphAt(index)); 604 int32 paragraphLength = resultParagraph.Length(); 605 if (textOffset == 0 && length > paragraphLength) { 606 length -= paragraphLength; 607 paragraphLength = 0; 608 resultParagraph.Clear(); 609 } else { 610 int32 removeLength = std::min(length, paragraphLength - textOffset); 611 resultParagraph.Remove(textOffset, removeLength); 612 paragraphLength -= removeLength; 613 length -= removeLength; 614 } 615 616 if (textOffset == paragraphLength && length == 0 617 && index + 1 < fParagraphs.CountItems()) { 618 // Line break between paragraphs got removed. Shift the next 619 // paragraph's text spans into the resulting one. 620 621 const TextSpanList& textSpans = ParagraphAt(index + 1).TextSpans(); 622 int32 spanCount = textSpans.CountItems(); 623 for (int32 i = 0; i < spanCount; i++) { 624 const TextSpan& span = textSpans.ItemAtFast(i); 625 resultParagraph.Append(span); 626 } 627 fParagraphs.Remove(index + 1); 628 paragraphCount++; 629 } 630 631 textOffset = 0; 632 633 while (length > 0 && index + 1 < fParagraphs.CountItems()) { 634 paragraphCount++; 635 const Paragraph& paragraph = ParagraphAt(index + 1); 636 paragraphLength = paragraph.Length(); 637 // Remove paragraph in any case. If some of it remains, the last 638 // paragraph to remove is reached, and the remaining spans are 639 // transfered to the result parahraph. 640 if (length >= paragraphLength) { 641 length -= paragraphLength; 642 fParagraphs.Remove(index); 643 } else { 644 // Last paragraph reached 645 int32 removedLength = std::min(length, paragraphLength); 646 Paragraph newParagraph(paragraph); 647 fParagraphs.Remove(index + 1); 648 649 if (!newParagraph.Remove(0, removedLength)) 650 return B_NO_MEMORY; 651 652 // Transfer remaining spans to resultParagraph 653 const TextSpanList& textSpans = newParagraph.TextSpans(); 654 int32 spanCount = textSpans.CountItems(); 655 for (int32 i = 0; i < spanCount; i++) { 656 const TextSpan& span = textSpans.ItemAtFast(i); 657 resultParagraph.Append(span); 658 } 659 660 break; 661 } 662 } 663 664 fParagraphs.Replace(index, resultParagraph); 665 666 return B_OK; 667 } 668 669 670 // #pragma mark - notifications 671 672 673 void 674 TextDocument::_NotifyTextChanging(TextChangingEvent& event) const 675 { 676 // Copy listener list to have a stable list in case listeners 677 // are added/removed from within the notification hook. 678 TextListenerList listeners(fTextListeners); 679 int32 count = listeners.CountItems(); 680 for (int32 i = 0; i < count; i++) { 681 const TextListenerRef& listener = listeners.ItemAtFast(i); 682 if (listener.Get() == NULL) 683 continue; 684 listener->TextChanging(event); 685 if (event.IsCanceled()) 686 break; 687 } 688 } 689 690 691 void 692 TextDocument::_NotifyTextChanged(const TextChangedEvent& event) const 693 { 694 // Copy listener list to have a stable list in case listeners 695 // are added/removed from within the notification hook. 696 TextListenerList listeners(fTextListeners); 697 int32 count = listeners.CountItems(); 698 for (int32 i = 0; i < count; i++) { 699 const TextListenerRef& listener = listeners.ItemAtFast(i); 700 if (listener.Get() == NULL) 701 continue; 702 listener->TextChanged(event); 703 } 704 } 705 706 707 void 708 TextDocument::_NotifyUndoableEditHappened(const UndoableEditRef& edit) const 709 { 710 // Copy listener list to have a stable list in case listeners 711 // are added/removed from within the notification hook. 712 UndoListenerList listeners(fUndoListeners); 713 int32 count = listeners.CountItems(); 714 for (int32 i = 0; i < count; i++) { 715 const UndoableEditListenerRef& listener = listeners.ItemAtFast(i); 716 if (listener.Get() == NULL) 717 continue; 718 listener->UndoableEditHappened(this, edit); 719 } 720 } 721