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