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(const CharacterStyle& characterStyle, 22 const 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 Insert(textOffset, text, CharacterStyleAt(textOffset)); 77 } 78 79 80 status_t 81 TextDocument::Insert(int32 textOffset, const BString& text, 82 const CharacterStyle& style) 83 { 84 return Insert(textOffset, text, style, ParagraphStyleAt(textOffset)); 85 } 86 87 88 status_t 89 TextDocument::Insert(int32 textOffset, const BString& text, 90 const CharacterStyle& characterStyle, const ParagraphStyle& paragraphStyle) 91 { 92 int32 paragraphOffset; 93 int32 index = ParagraphIndexFor(textOffset, paragraphOffset); 94 if (index < 0) 95 return B_BAD_VALUE; 96 97 textOffset -= paragraphOffset; 98 99 bool hasLineBreaks = text.FindFirst('\n', 0) >= 0; 100 101 if (hasLineBreaks) { 102 // Split paragraph at textOffset 103 Paragraph paragraph1(ParagraphAt(index).Style()); 104 Paragraph paragraph2(paragraphStyle); 105 const TextSpanList& textSpans = ParagraphAt(index).TextSpans(); 106 int32 spanCount = textSpans.CountItems(); 107 for (int32 i = 0; i < spanCount; i++) { 108 const TextSpan& span = textSpans.ItemAtFast(i); 109 int32 spanLength = span.CountChars(); 110 if (textOffset >= spanLength) { 111 paragraph1.Append(span); 112 textOffset -= spanLength; 113 } else if (textOffset > 0) { 114 paragraph1.Append( 115 span.SubSpan(0, textOffset)); 116 paragraph2.Append( 117 span.SubSpan(textOffset, spanLength - textOffset)); 118 textOffset = 0; 119 } else { 120 paragraph2.Append(span); 121 } 122 } 123 124 fParagraphs.Remove(index); 125 126 // Insert TextSpans, splitting 'text' into Paragraphs at line breaks. 127 int32 length = text.CountChars(); 128 int32 chunkStart = 0; 129 while (chunkStart < length) { 130 int32 chunkEnd = text.FindFirst('\n', chunkStart); 131 bool foundLineBreak = chunkEnd >= chunkStart; 132 if (foundLineBreak) 133 chunkEnd++; 134 else 135 chunkEnd = length; 136 137 BString chunk; 138 text.CopyCharsInto(chunk, chunkStart, chunkEnd - chunkStart); 139 TextSpan span(chunk, characterStyle); 140 141 if (foundLineBreak) { 142 if (!paragraph1.Append(span)) 143 return B_NO_MEMORY; 144 if (paragraph1.Length() > 0) { 145 if (!fParagraphs.Add(paragraph1, index)) 146 return B_NO_MEMORY; 147 index++; 148 } 149 paragraph1 = Paragraph(paragraphStyle); 150 } else { 151 if (!paragraph2.Prepend(span)) 152 return B_NO_MEMORY; 153 } 154 155 chunkStart = chunkEnd + 1; 156 } 157 158 if (paragraph2.IsEmpty()) { 159 // Make sure Paragraph has at least one TextSpan, even 160 // if its empty. 161 const TextSpanList& spans = paragraph1.TextSpans(); 162 const TextSpan& span = spans.LastItem(); 163 paragraph2.Append(TextSpan("", span.Style())); 164 } 165 166 if (!fParagraphs.Add(paragraph2, index)) 167 return B_NO_MEMORY; 168 } else { 169 Paragraph paragraph(ParagraphAt(index)); 170 paragraph.Insert(textOffset, TextSpan(text, characterStyle)); 171 if (!fParagraphs.Replace(index, paragraph)) 172 return B_NO_MEMORY; 173 } 174 175 return B_OK; 176 } 177 178 179 // #pragma mark - 180 181 182 status_t 183 TextDocument::Remove(int32 textOffset, int32 length) 184 { 185 if (length == 0) 186 return B_OK; 187 188 int32 paragraphOffset; 189 int32 index = ParagraphIndexFor(textOffset, paragraphOffset); 190 if (index < 0) 191 return B_BAD_VALUE; 192 193 textOffset -= paragraphOffset; 194 195 // The paragraph at the text offset remains, even if the offset is at 196 // the beginning of that paragraph. The idea is that the selection start 197 // stays visually in the same place. Therefore, the paragraph at that 198 // offset has to keep the paragraph style from that paragraph. 199 200 Paragraph resultParagraph(ParagraphAt(index)); 201 int32 paragraphLength = resultParagraph.Length(); 202 if (textOffset == 0 && length > paragraphLength) { 203 length -= paragraphLength; 204 paragraphLength = 0; 205 resultParagraph.Clear(); 206 } else { 207 int32 removeLength = std::min(length, paragraphLength - textOffset); 208 resultParagraph.Remove(textOffset, removeLength); 209 paragraphLength -= removeLength; 210 length -= removeLength; 211 } 212 213 if (textOffset == paragraphLength && length == 0 214 && index + 1 < fParagraphs.CountItems()) { 215 // Line break between paragraphs got removed. Shift the next 216 // paragraph's text spans into the resulting one. 217 218 const TextSpanList& textSpans = ParagraphAt(index + 1).TextSpans(); 219 int32 spanCount = textSpans.CountItems(); 220 for (int32 i = 0; i < spanCount; i++) { 221 const TextSpan& span = textSpans.ItemAtFast(i); 222 resultParagraph.Append(span); 223 } 224 fParagraphs.Remove(index + 1); 225 } 226 227 textOffset = 0; 228 229 while (length > 0 && index + 1 < fParagraphs.CountItems()) { 230 const Paragraph& paragraph = ParagraphAt(index + 1); 231 paragraphLength = paragraph.Length(); 232 // Remove paragraph in any case. If some of it remains, the last 233 // paragraph to remove is reached, and the remaining spans are 234 // transfered to the result parahraph. 235 if (length >= paragraphLength) { 236 length -= paragraphLength; 237 fParagraphs.Remove(index); 238 } else { 239 // Last paragraph reached 240 int32 removedLength = std::min(length, paragraphLength); 241 Paragraph newParagraph(paragraph); 242 fParagraphs.Remove(index + 1); 243 244 if (!newParagraph.Remove(0, removedLength)) 245 return B_NO_MEMORY; 246 247 // Transfer remaining spans to resultParagraph 248 const TextSpanList& textSpans = newParagraph.TextSpans(); 249 int32 spanCount = textSpans.CountItems(); 250 for (int32 i = 0; i < spanCount; i++) { 251 const TextSpan& span = textSpans.ItemAtFast(i); 252 resultParagraph.Append(span); 253 } 254 255 break; 256 } 257 } 258 259 fParagraphs.Replace(index, resultParagraph); 260 261 return B_OK; 262 } 263 264 265 // #pragma mark - 266 267 268 status_t 269 TextDocument::Replace(int32 textOffset, int32 length, const BString& text) 270 { 271 return Replace(textOffset, length, text, CharacterStyleAt(textOffset)); 272 } 273 274 275 status_t 276 TextDocument::Replace(int32 textOffset, int32 length, const BString& text, 277 const CharacterStyle& style) 278 { 279 return Replace(textOffset, length, text, style, 280 ParagraphStyleAt(textOffset)); 281 } 282 283 284 status_t 285 TextDocument::Replace(int32 textOffset, int32 length, const BString& text, 286 const CharacterStyle& characterStyle, const ParagraphStyle& paragraphStyle) 287 { 288 status_t ret = Remove(textOffset, length); 289 if (ret != B_OK) 290 return ret; 291 292 return Insert(textOffset, text, characterStyle, paragraphStyle); 293 } 294 295 296 // #pragma mark - 297 298 299 const CharacterStyle& 300 TextDocument::CharacterStyleAt(int32 textOffset) const 301 { 302 int32 paragraphOffset; 303 const Paragraph& paragraph = ParagraphAt(textOffset, paragraphOffset); 304 305 textOffset -= paragraphOffset; 306 const TextSpanList& spans = paragraph.TextSpans(); 307 308 int32 index = 0; 309 while (index < spans.CountItems()) { 310 const TextSpan& span = spans.ItemAtFast(index); 311 if (textOffset - span.CountChars() < 0) 312 return span.Style(); 313 textOffset -= span.CountChars(); 314 index++; 315 } 316 317 return fDefaultCharacterStyle; 318 } 319 320 321 const ParagraphStyle& 322 TextDocument::ParagraphStyleAt(int32 textOffset) const 323 { 324 int32 paragraphOffset; 325 return ParagraphAt(textOffset, paragraphOffset).Style(); 326 } 327 328 329 // #pragma mark - 330 331 332 int32 333 TextDocument::ParagraphIndexFor(int32 textOffset, int32& paragraphOffset) const 334 { 335 // TODO: Could binary search the Paragraphs if they were wrapped in classes 336 // that knew there text offset in the document. 337 int32 textLength = 0; 338 paragraphOffset = 0; 339 int32 count = fParagraphs.CountItems(); 340 for (int32 i = 0; i < count; i++) { 341 const Paragraph& paragraph = fParagraphs.ItemAtFast(i); 342 int32 paragraphLength = paragraph.Length(); 343 textLength += paragraphLength; 344 if (textLength > textOffset 345 || (i == count - 1 && textLength == textOffset)) { 346 return i; 347 } 348 paragraphOffset += paragraphLength; 349 } 350 return -1; 351 } 352 353 354 const Paragraph& 355 TextDocument::ParagraphAt(int32 textOffset, int32& paragraphOffset) const 356 { 357 int32 index = ParagraphIndexFor(textOffset, paragraphOffset); 358 if (index >= 0) 359 return fParagraphs.ItemAtFast(index); 360 361 return fEmptyLastParagraph; 362 } 363 364 365 const Paragraph& 366 TextDocument::ParagraphAt(int32 index) const 367 { 368 if (index >= 0 && index < fParagraphs.CountItems()) 369 return fParagraphs.ItemAtFast(index); 370 return fEmptyLastParagraph; 371 } 372 373 374 bool 375 TextDocument::Append(const Paragraph& paragraph) 376 { 377 return fParagraphs.Add(paragraph); 378 } 379 380 381 int32 382 TextDocument::Length() const 383 { 384 // TODO: Could be O(1) if the Paragraphs were wrapped in classes that 385 // knew there text offset in the document. 386 int32 textLength = 0; 387 int32 count = fParagraphs.CountItems(); 388 for (int32 i = 0; i < count; i++) { 389 const Paragraph& paragraph = fParagraphs.ItemAtFast(i); 390 textLength += paragraph.Length(); 391 } 392 return textLength; 393 } 394 395 396 BString 397 TextDocument::Text() const 398 { 399 return Text(0, Length()); 400 } 401 402 403 BString 404 TextDocument::Text(int32 start, int32 length) const 405 { 406 if (start < 0) 407 start = 0; 408 409 BString text; 410 411 int32 count = fParagraphs.CountItems(); 412 for (int32 i = 0; i < count; i++) { 413 const Paragraph& paragraph = fParagraphs.ItemAtFast(i); 414 int32 paragraphLength = paragraph.Length(); 415 if (paragraphLength == 0) 416 continue; 417 if (start > paragraphLength) { 418 // Skip paragraph if its before start 419 start -= paragraphLength; 420 continue; 421 } 422 423 // Remaining paragraph length after start 424 paragraphLength -= start; 425 int32 copyLength = std::min(paragraphLength, length); 426 427 text << paragraph.Text(start, copyLength); 428 429 length -= copyLength; 430 if (length == 0) 431 break; 432 433 // Next paragraph is copied from its beginning 434 start = 0; 435 } 436 437 return text; 438 } 439 440 441 TextDocumentRef 442 TextDocument::SubDocument(int32 start, int32 length) const 443 { 444 TextDocumentRef result(new(std::nothrow) TextDocument( 445 fDefaultCharacterStyle, fEmptyLastParagraph.Style()), true); 446 447 if (result.Get() == NULL) 448 return result; 449 450 if (start < 0) 451 start = 0; 452 453 int32 count = fParagraphs.CountItems(); 454 for (int32 i = 0; i < count; i++) { 455 const Paragraph& paragraph = fParagraphs.ItemAtFast(i); 456 int32 paragraphLength = paragraph.Length(); 457 if (paragraphLength == 0) 458 continue; 459 if (start > paragraphLength) { 460 // Skip paragraph if its before start 461 start -= paragraphLength; 462 continue; 463 } 464 465 // Remaining paragraph length after start 466 paragraphLength -= start; 467 int32 copyLength = std::min(paragraphLength, length); 468 469 result->Append(paragraph.SubParagraph(start, copyLength)); 470 471 length -= copyLength; 472 if (length == 0) 473 break; 474 475 // Next paragraph is copied from its beginning 476 start = 0; 477 } 478 479 return result; 480 } 481 482 483 // #pragma mark - 484 485 486 void 487 TextDocument::PrintToStream() const 488 { 489 int32 paragraphCount = fParagraphs.CountItems(); 490 if (paragraphCount == 0) { 491 printf("<document/>\n"); 492 return; 493 } 494 printf("<document>\n"); 495 for (int32 i = 0; i < paragraphCount; i++) { 496 fParagraphs.ItemAtFast(i).PrintToStream(); 497 } 498 printf("</document>\n"); 499 } 500 501 502 // #pragma mark - 503 504 505 bool 506 TextDocument::AddListener(const TextListenerRef& listener) 507 { 508 return fTextListeners.Add(listener); 509 } 510 511 512 bool 513 TextDocument::RemoveListener(const TextListenerRef& listener) 514 { 515 return fTextListeners.Remove(listener); 516 } 517 518 519 bool 520 TextDocument::AddUndoListener(const UndoableEditListenerRef& listener) 521 { 522 return fUndoListeners.Add(listener); 523 } 524 525 526 bool 527 TextDocument::RemoveUndoListener(const UndoableEditListenerRef& listener) 528 { 529 return fUndoListeners.Remove(listener); 530 } 531 532 533 void 534 TextDocument::_NotifyTextChanging(TextChangingEvent& event) const 535 { 536 // Copy listener list to have a stable list in case listeners 537 // are added/removed from within the notification hook. 538 TextListenerList listeners(fTextListeners); 539 int32 count = listeners.CountItems(); 540 for (int32 i = 0; i < count; i++) { 541 const TextListenerRef& listener = listeners.ItemAtFast(i); 542 if (listener.Get() == NULL) 543 continue; 544 listener->TextChanging(event); 545 if (event.IsCanceled()) 546 break; 547 } 548 } 549 550 551 void 552 TextDocument::_NotifyTextChanged(const TextChangedEvent& event) const 553 { 554 // Copy listener list to have a stable list in case listeners 555 // are added/removed from within the notification hook. 556 TextListenerList listeners(fTextListeners); 557 int32 count = listeners.CountItems(); 558 for (int32 i = 0; i < count; i++) { 559 const TextListenerRef& listener = listeners.ItemAtFast(i); 560 if (listener.Get() == NULL) 561 continue; 562 listener->TextChanged(event); 563 } 564 } 565 566 567 void 568 TextDocument::_NotifyUndoableEditHappened(const UndoableEditRef& edit) const 569 { 570 // Copy listener list to have a stable list in case listeners 571 // are added/removed from within the notification hook. 572 UndoListenerList listeners(fUndoListeners); 573 int32 count = listeners.CountItems(); 574 for (int32 i = 0; i < count; i++) { 575 const UndoableEditListenerRef& listener = listeners.ItemAtFast(i); 576 if (listener.Get() == NULL) 577 continue; 578 listener->UndoableEditHappened(this, edit); 579 } 580 } 581