repository_infos: Enable automatic updates on the main Haiku repostiory.
[haiku.git] / src / apps / haikudepot / textview / TextDocument.cpp
blob0cce1b4f5e9f25c9a330cd29f7ee4ed10e46e442
1 /*
2 * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3 * All rights reserved. Distributed under the terms of the MIT License.
4 */
6 #include "TextDocument.h"
8 #include <algorithm>
9 #include <stdio.h>
12 TextDocument::TextDocument()
14 fParagraphs(),
15 fEmptyLastParagraph(),
16 fDefaultCharacterStyle()
21 TextDocument::TextDocument(CharacterStyle characterStyle,
22 ParagraphStyle paragraphStyle)
24 fParagraphs(),
25 fEmptyLastParagraph(paragraphStyle),
26 fDefaultCharacterStyle(characterStyle)
31 TextDocument::TextDocument(const TextDocument& other)
33 fParagraphs(other.fParagraphs),
34 fEmptyLastParagraph(other.fEmptyLastParagraph),
35 fDefaultCharacterStyle(other.fDefaultCharacterStyle)
40 TextDocument&
41 TextDocument::operator=(const TextDocument& other)
43 fParagraphs = other.fParagraphs;
44 fEmptyLastParagraph = other.fEmptyLastParagraph;
45 fDefaultCharacterStyle = other.fDefaultCharacterStyle;
47 return *this;
51 bool
52 TextDocument::operator==(const TextDocument& other) const
54 if (this == &other)
55 return true;
57 return fEmptyLastParagraph == other.fEmptyLastParagraph
58 && fDefaultCharacterStyle == other.fDefaultCharacterStyle
59 && fParagraphs == other.fParagraphs;
63 bool
64 TextDocument::operator!=(const TextDocument& other) const
66 return !(*this == other);
70 // #pragma mark -
73 status_t
74 TextDocument::Insert(int32 textOffset, const BString& text)
76 return Replace(textOffset, 0, text);
80 status_t
81 TextDocument::Insert(int32 textOffset, const BString& text,
82 CharacterStyle style)
84 return Replace(textOffset, 0, text, style);
88 status_t
89 TextDocument::Insert(int32 textOffset, const BString& text,
90 CharacterStyle characterStyle, ParagraphStyle paragraphStyle)
92 return Replace(textOffset, 0, text, characterStyle, paragraphStyle);
96 // #pragma mark -
99 status_t
100 TextDocument::Remove(int32 textOffset, int32 length)
102 return Replace(textOffset, length, BString());
106 // #pragma mark -
109 status_t
110 TextDocument::Replace(int32 textOffset, int32 length, const BString& text)
112 return Replace(textOffset, length, text, CharacterStyleAt(textOffset));
116 status_t
117 TextDocument::Replace(int32 textOffset, int32 length, const BString& text,
118 CharacterStyle style)
120 return Replace(textOffset, length, text, style,
121 ParagraphStyleAt(textOffset));
125 status_t
126 TextDocument::Replace(int32 textOffset, int32 length, const BString& text,
127 CharacterStyle characterStyle, ParagraphStyle paragraphStyle)
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);
137 status_t
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);
146 if (ret != B_OK)
147 return ret;
149 ret = _Insert(textOffset, document, firstParagraph, paragraphCount);
151 _NotifyTextChanged(TextChangedEvent(firstParagraph, paragraphCount));
153 return ret;
157 // #pragma mark -
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();
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++;
178 return fDefaultCharacterStyle;
182 const ParagraphStyle&
183 TextDocument::ParagraphStyleAt(int32 textOffset) const
185 int32 paragraphOffset;
186 return ParagraphAt(textOffset, paragraphOffset).Style();
190 // #pragma mark -
193 int32
194 TextDocument::CountParagraphs() const
196 return fParagraphs.CountItems();
200 int32
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;
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;
216 paragraphOffset += paragraphLength;
218 return -1;
222 const Paragraph&
223 TextDocument::ParagraphAt(int32 textOffset, int32& paragraphOffset) const
225 int32 index = ParagraphIndexFor(textOffset, paragraphOffset);
226 if (index >= 0)
227 return fParagraphs.ItemAtFast(index);
229 return fEmptyLastParagraph;
233 const Paragraph&
234 TextDocument::ParagraphAt(int32 index) const
236 if (index >= 0 && index < fParagraphs.CountItems())
237 return fParagraphs.ItemAtFast(index);
238 return fEmptyLastParagraph;
242 bool
243 TextDocument::Append(const Paragraph& paragraph)
245 return fParagraphs.Add(paragraph);
249 int32
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();
260 return textLength;
264 BString
265 TextDocument::Text() const
267 return Text(0, Length());
271 BString
272 TextDocument::Text(int32 start, int32 length) const
274 if (start < 0)
275 start = 0;
277 BString text;
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;
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;
298 if (length == 0)
299 break;
301 // Next paragraph is copied from its beginning
302 start = 0;
305 return text;
309 TextDocumentRef
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)
316 return result;
318 if (start < 0)
319 start = 0;
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;
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;
340 if (length == 0)
341 break;
343 // Next paragraph is copied from its beginning
344 start = 0;
347 return result;
351 // #pragma mark -
354 void
355 TextDocument::PrintToStream() const
357 int32 paragraphCount = fParagraphs.CountItems();
358 if (paragraphCount == 0) {
359 printf("<document/>\n");
360 return;
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)
377 throw B_NO_MEMORY;
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;
387 if (foundLineBreak)
388 chunkEnd++;
389 else
390 chunkEnd = length;
392 BString chunk;
393 text.CopyCharsInto(chunk, chunkStart, chunkEnd - chunkStart);
394 TextSpan span(chunk, characterStyle);
396 if (!paragraph.Append(span))
397 throw B_NO_MEMORY;
398 if (paragraph.Length() > 0 && !document->Append(paragraph))
399 throw B_NO_MEMORY;
401 paragraph = Paragraph(paragraphStyle);
402 chunkStart = chunkEnd + 1;
405 return document;
409 // #pragma mark -
412 bool
413 TextDocument::AddListener(TextListenerRef listener)
415 return fTextListeners.Add(listener);
419 bool
420 TextDocument::RemoveListener(TextListenerRef listener)
422 return fTextListeners.Remove(listener);
426 bool
427 TextDocument::AddUndoListener(UndoableEditListenerRef listener)
429 return fUndoListeners.Add(listener);
433 bool
434 TextDocument::RemoveUndoListener(UndoableEditListenerRef listener)
436 return fUndoListeners.Remove(listener);
440 // #pragma mark - private
443 status_t
444 TextDocument::_Insert(int32 textOffset, TextDocumentRef document,
445 int32& index, int32& paragraphCount)
447 int32 paragraphOffset;
448 index = ParagraphIndexFor(textOffset, paragraphOffset);
449 if (index < 0)
450 return B_BAD_VALUE;
452 if (document->Length() == 0)
453 return B_OK;
455 textOffset -= paragraphOffset;
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");
465 if (hasLineBreaks) {
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))
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;
488 textOffset = 0;
489 } else {
490 if (!paragraph2.Append(span))
491 return B_NO_MEMORY;
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))
508 return B_NO_MEMORY;
512 // Insert the first paragraph-part again to the document
513 if (!fParagraphs.Add(paragraph1, index))
514 return B_NO_MEMORY;
515 paragraphCount++;
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++;
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;
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;
557 if (!fParagraphs.Add(paragraph2, ++index))
558 return B_NO_MEMORY;
560 paragraphCount++;
561 } else {
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))
574 return B_NO_MEMORY;
576 paragraphCount++;
579 return B_OK;
583 status_t
584 TextDocument::_Remove(int32 textOffset, int32 length, int32& index,
585 int32& paragraphCount)
587 if (length == 0)
588 return B_OK;
590 int32 paragraphOffset;
591 index = ParagraphIndexFor(textOffset, paragraphOffset);
592 if (index < 0)
593 return B_BAD_VALUE;
595 textOffset -= paragraphOffset;
596 paragraphCount++;
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;
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;
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);
628 paragraphCount++;
631 textOffset = 0;
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);
649 if (!newParagraph.Remove(0, removedLength))
650 return B_NO_MEMORY;
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);
660 break;
664 fParagraphs.Replace(index, resultParagraph);
666 return B_OK;
670 // #pragma mark - notifications
673 void
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)
683 continue;
684 listener->TextChanging(event);
685 if (event.IsCanceled())
686 break;
691 void
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)
701 continue;
702 listener->TextChanged(event);
707 void
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)
717 continue;
718 listener->UndoableEditHappened(this, edit);