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