2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3 * All rights reserved. Distributed under the terms of the MIT License.
6 #include "TextDocument.h"
12 TextDocument::TextDocument()
15 fEmptyLastParagraph(),
16 fDefaultCharacterStyle()
21 TextDocument::TextDocument(CharacterStyle characterStyle
,
22 ParagraphStyle paragraphStyle
)
25 fEmptyLastParagraph(paragraphStyle
),
26 fDefaultCharacterStyle(characterStyle
)
31 TextDocument::TextDocument(const TextDocument
& other
)
33 fParagraphs(other
.fParagraphs
),
34 fEmptyLastParagraph(other
.fEmptyLastParagraph
),
35 fDefaultCharacterStyle(other
.fDefaultCharacterStyle
)
41 TextDocument::operator=(const TextDocument
& other
)
43 fParagraphs
= other
.fParagraphs
;
44 fEmptyLastParagraph
= other
.fEmptyLastParagraph
;
45 fDefaultCharacterStyle
= other
.fDefaultCharacterStyle
;
52 TextDocument::operator==(const TextDocument
& other
) const
57 return fEmptyLastParagraph
== other
.fEmptyLastParagraph
58 && fDefaultCharacterStyle
== other
.fDefaultCharacterStyle
59 && fParagraphs
== other
.fParagraphs
;
64 TextDocument::operator!=(const TextDocument
& other
) const
66 return !(*this == other
);
74 TextDocument::Insert(int32 textOffset
, const BString
& text
)
76 return Replace(textOffset
, 0, text
);
81 TextDocument::Insert(int32 textOffset
, const BString
& text
,
84 return Replace(textOffset
, 0, text
, style
);
89 TextDocument::Insert(int32 textOffset
, const BString
& text
,
90 CharacterStyle characterStyle
, ParagraphStyle paragraphStyle
)
92 return Replace(textOffset
, 0, text
, characterStyle
, paragraphStyle
);
100 TextDocument::Remove(int32 textOffset
, int32 length
)
102 return Replace(textOffset
, length
, BString());
110 TextDocument::Replace(int32 textOffset
, int32 length
, const BString
& text
)
112 return Replace(textOffset
, length
, text
, CharacterStyleAt(textOffset
));
117 TextDocument::Replace(int32 textOffset
, int32 length
, const BString
& text
,
118 CharacterStyle style
)
120 return Replace(textOffset
, length
, text
, style
,
121 ParagraphStyleAt(textOffset
));
126 TextDocument::Replace(int32 textOffset
, int32 length
, const BString
& text
,
127 CharacterStyle characterStyle
, ParagraphStyle paragraphStyle
)
129 TextDocumentRef document
= NormalizeText(text
, characterStyle
,
131 if (document
.Get() == NULL
|| document
->Length() != text
.CountChars())
133 return Replace(textOffset
, length
, document
);
138 TextDocument::Replace(int32 textOffset
, int32 length
, TextDocumentRef document
)
140 int32 firstParagraph
= 0;
141 int32 paragraphCount
= 0;
143 // TODO: Call _NotifyTextChanging() before any change happened
145 status_t ret
= _Remove(textOffset
, length
, firstParagraph
, paragraphCount
);
149 ret
= _Insert(textOffset
, document
, firstParagraph
, paragraphCount
);
151 _NotifyTextChanged(TextChangedEvent(firstParagraph
, paragraphCount
));
160 const CharacterStyle
&
161 TextDocument::CharacterStyleAt(int32 textOffset
) const
163 int32 paragraphOffset
;
164 const Paragraph
& paragraph
= ParagraphAt(textOffset
, paragraphOffset
);
166 textOffset
-= paragraphOffset
;
167 const TextSpanList
& spans
= paragraph
.TextSpans();
170 while (index
< spans
.CountItems()) {
171 const TextSpan
& span
= spans
.ItemAtFast(index
);
172 if (textOffset
- span
.CountChars() < 0)
174 textOffset
-= span
.CountChars();
178 return fDefaultCharacterStyle
;
182 const ParagraphStyle
&
183 TextDocument::ParagraphStyleAt(int32 textOffset
) const
185 int32 paragraphOffset
;
186 return ParagraphAt(textOffset
, paragraphOffset
).Style();
194 TextDocument::CountParagraphs() const
196 return fParagraphs
.CountItems();
201 TextDocument::ParagraphIndexFor(int32 textOffset
, int32
& paragraphOffset
) const
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;
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
)) {
216 paragraphOffset
+= paragraphLength
;
223 TextDocument::ParagraphAt(int32 textOffset
, int32
& paragraphOffset
) const
225 int32 index
= ParagraphIndexFor(textOffset
, paragraphOffset
);
227 return fParagraphs
.ItemAtFast(index
);
229 return fEmptyLastParagraph
;
234 TextDocument::ParagraphAt(int32 index
) const
236 if (index
>= 0 && index
< fParagraphs
.CountItems())
237 return fParagraphs
.ItemAtFast(index
);
238 return fEmptyLastParagraph
;
243 TextDocument::Append(const Paragraph
& paragraph
)
245 return fParagraphs
.Add(paragraph
);
250 TextDocument::Length() const
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();
265 TextDocument::Text() const
267 return Text(0, Length());
272 TextDocument::Text(int32 start
, int32 length
) const
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)
285 if (start
> paragraphLength
) {
286 // Skip paragraph if its before start
287 start
-= paragraphLength
;
291 // Remaining paragraph length after start
292 paragraphLength
-= start
;
293 int32 copyLength
= std::min(paragraphLength
, length
);
295 text
<< paragraph
.Text(start
, copyLength
);
297 length
-= copyLength
;
301 // Next paragraph is copied from its beginning
310 TextDocument::SubDocument(int32 start
, int32 length
) const
312 TextDocumentRef
result(new(std::nothrow
) TextDocument(
313 fDefaultCharacterStyle
, fEmptyLastParagraph
.Style()), true);
315 if (result
.Get() == NULL
)
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)
327 if (start
> paragraphLength
) {
328 // Skip paragraph if its before start
329 start
-= paragraphLength
;
333 // Remaining paragraph length after start
334 paragraphLength
-= start
;
335 int32 copyLength
= std::min(paragraphLength
, length
);
337 result
->Append(paragraph
.SubParagraph(start
, copyLength
));
339 length
-= copyLength
;
343 // Next paragraph is copied from its beginning
355 TextDocument::PrintToStream() const
357 int32 paragraphCount
= fParagraphs
.CountItems();
358 if (paragraphCount
== 0) {
359 printf("<document/>\n");
362 printf("<document>\n");
363 for (int32 i
= 0; i
< paragraphCount
; i
++) {
364 fParagraphs
.ItemAtFast(i
).PrintToStream();
366 printf("</document>\n");
370 /*static*/ TextDocumentRef
371 TextDocument::NormalizeText(const BString
& text
,
372 CharacterStyle characterStyle
, ParagraphStyle paragraphStyle
)
374 TextDocumentRef
document(new(std::nothrow
) TextDocument(characterStyle
,
375 paragraphStyle
), true);
376 if (document
.Get() == NULL
)
379 Paragraph
paragraph(paragraphStyle
);
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
;
393 text
.CopyCharsInto(chunk
, chunkStart
, chunkEnd
- chunkStart
);
394 TextSpan
span(chunk
, characterStyle
);
396 if (!paragraph
.Append(span
))
398 if (paragraph
.Length() > 0 && !document
->Append(paragraph
))
401 paragraph
= Paragraph(paragraphStyle
);
402 chunkStart
= chunkEnd
+ 1;
413 TextDocument::AddListener(TextListenerRef listener
)
415 return fTextListeners
.Add(listener
);
420 TextDocument::RemoveListener(TextListenerRef listener
)
422 return fTextListeners
.Remove(listener
);
427 TextDocument::AddUndoListener(UndoableEditListenerRef listener
)
429 return fUndoListeners
.Add(listener
);
434 TextDocument::RemoveUndoListener(UndoableEditListenerRef listener
)
436 return fUndoListeners
.Remove(listener
);
440 // #pragma mark - private
444 TextDocument::_Insert(int32 textOffset
, TextDocumentRef document
,
445 int32
& index
, int32
& paragraphCount
)
447 int32 paragraphOffset
;
448 index
= ParagraphIndexFor(textOffset
, paragraphOffset
);
452 if (document
->Length() == 0)
455 textOffset
-= paragraphOffset
;
458 if (document
->CountParagraphs() > 1) {
459 hasLineBreaks
= true;
461 const Paragraph
& paragraph
= document
->ParagraphAt(0);
462 hasLineBreaks
= paragraph
.EndsWith("\n");
466 // Split paragraph at textOffset
467 Paragraph
paragraph1(ParagraphAt(index
).Style());
468 Paragraph
paragraph2(document
->ParagraphAt(
469 document
->CountParagraphs() - 1).Style());
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
))
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
))) {
490 if (!paragraph2
.Append(span
))
496 fParagraphs
.Remove(index
);
498 // Append first paragraph in other document to first part of
499 // paragraph at insert position
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
))
512 // Insert the first paragraph-part again to the document
513 if (!fParagraphs
.Add(paragraph1
, index
))
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
))
526 int32 lastIndex
= document
->CountParagraphs() - 1;
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
))
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
))
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())))
557 if (!fParagraphs
.Add(paragraph2
, ++index
))
562 Paragraph
paragraph(ParagraphAt(index
));
563 const Paragraph
& otherParagraph
= document
->ParagraphAt(0);
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();
573 if (!fParagraphs
.Replace(index
, paragraph
))
584 TextDocument::_Remove(int32 textOffset
, int32 length
, int32
& index
,
585 int32
& paragraphCount
)
590 int32 paragraphOffset
;
591 index
= ParagraphIndexFor(textOffset
, paragraphOffset
);
595 textOffset
-= paragraphOffset
;
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.
603 Paragraph
resultParagraph(ParagraphAt(index
));
604 int32 paragraphLength
= resultParagraph
.Length();
605 if (textOffset
== 0 && length
> paragraphLength
) {
606 length
-= paragraphLength
;
608 resultParagraph
.Clear();
610 int32 removeLength
= std::min(length
, paragraphLength
- textOffset
);
611 resultParagraph
.Remove(textOffset
, removeLength
);
612 paragraphLength
-= removeLength
;
613 length
-= removeLength
;
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.
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
);
627 fParagraphs
.Remove(index
+ 1);
633 while (length
> 0 && index
+ 1 < fParagraphs
.CountItems()) {
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
);
644 // Last paragraph reached
645 int32 removedLength
= std::min(length
, paragraphLength
);
646 Paragraph
newParagraph(paragraph
);
647 fParagraphs
.Remove(index
+ 1);
649 if (!newParagraph
.Remove(0, removedLength
))
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
);
664 fParagraphs
.Replace(index
, resultParagraph
);
670 // #pragma mark - notifications
674 TextDocument::_NotifyTextChanging(TextChangingEvent
& event
) const
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
)
684 listener
->TextChanging(event
);
685 if (event
.IsCanceled())
692 TextDocument::_NotifyTextChanged(const TextChangedEvent
& event
) const
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
)
702 listener
->TextChanged(event
);
708 TextDocument::_NotifyUndoableEditHappened(const UndoableEditRef
& edit
) const
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
)
718 listener
->UndoableEditHappened(this, edit
);