xref: /haiku/src/apps/haikudepot/textview/TextDocument.cpp (revision 1e60bdeab63fa7a57bc9a55b032052e95a18bd2c)
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