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