org.fxmisc.richtext.MultiChangeBuilder Maven / Gradle / Ivy
Show all versions of richtextfx Show documentation
package org.fxmisc.richtext;
import javafx.scene.control.IndexRange;
import org.fxmisc.richtext.model.ReadOnlyStyledDocument;
import org.fxmisc.richtext.model.Replacement;
import org.fxmisc.richtext.model.StyledDocument;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Constructs a list of {@link Replacement}s that are used to update an
* {@link org.fxmisc.richtext.model.EditableStyledDocument} in one call via {@link #commit()}. Note: this builder
* cannot be reused and a new one must be created for each multi-change.
*
* Relative vs Absolute Changes
*
* Let's say that a document has the text {@code |(|t|e|x|t||)|} where each {@code |} represents a position
* in-between the characters of the text . If one wants to remove the opening
* and closing parenthesis in two update calls, they would write:
*
*
* // Text: |(|t|e|x|t|)|
* // Position: 0 1 2 3 4 5 6
* area.deleteText(0, 1); // delete the first parenthesis
*
* // now the second parenthesis is positioned in a different spot than before
* // Text: |t|e|x|t|)|
* // Position: 0 1 2 3 4 5
* area.deleteText(4, 5); // delete the second parenthesis
*
*
* {@code MultiChangeBuilder} can do the same thing in one call and works by applying earlier changes before
* later ones. However, this creates a problem. Later changes' start and end positions must be updated to still
* point to the correct spot in the text. Wouldn't it be easier to be able to define both changes in the original
* coordinate system of the document before any changes were applied? Returning to the previous example,
* wouldn't it be better if one could write:
*
*
* // Text: |(|t|e|x|t|)|
* // Position: 0 1 2 3 4 5 6
*
* // start multi change
* area.deleteText(0, 1); // delete the 1st parenthesis
* area.deleteText(5, 6); // delete the 2nd parenthesis
* // end multi change
*
*
* Fortunately, that is already handled for you. Such changes are known as "relative" changes: their
* start and end positions are updated to point to where they should point once all the earlier changes before
* them have been applied. To execute the same code as before, we would write:
*
*
* area.createMultiChange()
* // deletes the 1st parenthesis
* .deleteText(0, 1)
*
* // internally updates this to delete(4, 5), so that it deletes the 2nd parenthesis
* .deleteText(5, 6)
*
* // executes the call and updates the document
* .commit();
*
*
* However, a developer may sometimes want an "absolute" change: their start and end positions
* are exactly what the developer specifies regardless of how changes before them modify the underlying document.
* For example, the following code would still delete both parenthesis:
*
*
* area.createMultiChange()
* .deleteText(0, 1) // deletes the 1st parenthesis
* .deleteTextAbsolutely(4, 5) // deletes the 2nd parenthesis
* .commit();
*
*
* Thus, absolute changes (e.g. {@link #replaceAbsolutely(int, int, StyledDocument)}) are all the methods
* which have "absolute" as a suffix and relative changes (e.g. {@link #replace(int, int, StyledDocument)})
* do not. To make things easier for the developer, the methods declared here are similar to those declared
* in {@link EditActions}, minus a few (e.g. {@link EditActions#append(StyledDocument)}).
*
* Other Considerations
*
* -
* Warning: relative changes will not be updated if one starts a multi-change and fails
* to commit those changes before updating the area's underlying document in a
* {@link GenericStyledArea#replace(int, int, StyledDocument)} call.
*
* -
* The builder does not optimize performance in cases where the developer adds a later change that makes
* an earlier change obsolete. For example,
*
* // This code...
* area.createMultiChange(4)
* .replaceText(0, 1, "a")
* .replaceTextAbsolutely(0, 1, "b")
* .replaceTextAbsolutely(0, 1, "c")
* .replaceTextAbsolutely(0, 1, "d")
* .commit();
*
* // ...could be optimized to...
* area.createMultiChange(2)
* .replaceText(0, 1, "a")
* // .replaceTextAbsolutely(0, 1, "b") // superseded by next change
* // .replaceTextAbsolutely(0, 1, "c") // superseded by next change
* .replaceTextAbsolutely(0, 1, "d")
* .commit();
* // ...as it would reduce the number of updates by two and not create all the other objects
* // associated with an update (e.g. RichTextChange, PlainTextChange, etc.).
*
*
*
*/
public class MultiChangeBuilder {
private final GenericStyledArea area;
private final List> list;
private boolean alreadyCreated = false;
MultiChangeBuilder(GenericStyledArea area) {
this(area, new ArrayList<>());
}
MultiChangeBuilder(GenericStyledArea area, int initialListSize) {
this(area, new ArrayList<>(initialListSize));
}
private MultiChangeBuilder(GenericStyledArea area, List> list) {
this.area = area;
this.list = list;
}
/**
* Inserts the given text at the given position.
*
* @param position The position to insert the text.
* @param text The text to insert.
*/
public MultiChangeBuilder insertText(int position, String text) {
return replaceText(position, position, text);
}
/**
* Inserts the given text at the given position.
*
* @param position The position to insert the text.
* @param text The text to insert.
*/
public MultiChangeBuilder insertTextAbsolutely(int position, String text) {
return replaceTextAbsolutely(position, position, text);
}
/**
* Inserts the given text at the position returned from
* {@code getAbsolutePosition(paragraphIndex, columnPosition)}.
*
* Caution: see {@link StyledDocument#getAbsolutePosition(int, int)} to know how the column index argument
* can affect the returned position.
*
* @param text The text to insert
*/
public MultiChangeBuilder insertText(int paragraphIndex, int columnPosition, String text) {
int index = area.getAbsolutePosition(paragraphIndex, columnPosition);
return replaceText(index, index, text);
}
/**
* Inserts the given text at the position returned from
* {@code getAbsolutePosition(paragraphIndex, columnPosition)}.
*
* Caution: see {@link StyledDocument#getAbsolutePosition(int, int)} to know how the column index argument
* can affect the returned position.
*
* @param text The text to insert
*/
public MultiChangeBuilder insertTextAbsolutely(int paragraphIndex, int columnPosition, String text) {
int index = area.getAbsolutePosition(paragraphIndex, columnPosition);
return replaceTextAbsolutely(index, index, text);
}
/**
* Inserts the given rich-text content at the given position.
*
* @param position The position to insert the text.
* @param document The rich-text content to insert.
*/
public MultiChangeBuilder insert(int position, StyledDocument document) {
return replace(position, position, document);
}
/**
* Inserts the given rich-text content at the given position.
*
* @param position The position to insert the text.
* @param document The rich-text content to insert.
*/
public MultiChangeBuilder insertAbsolutely(int position, StyledDocument document) {
return replaceAbsolutely(position, position, document);
}
/**
* Inserts the given rich-text content at the position returned from
* {@code getAbsolutePosition(paragraphIndex, columnPosition)}.
*
* Caution: see {@link StyledDocument#getAbsolutePosition(int, int)} to know how the column index argument
* can affect the returned position.
*
* @param document The rich-text content to insert.
*/
public MultiChangeBuilder insert(int paragraphIndex, int columnPosition, StyledDocument document) {
int pos = area.getAbsolutePosition(paragraphIndex, columnPosition);
return replace(pos, pos, document);
}
/**
* Inserts the given rich-text content at the position returned from
* {@code getAbsolutePosition(paragraphIndex, columnPosition)}.
*
* Caution: see {@link StyledDocument#getAbsolutePosition(int, int)} to know how the column index argument
* can affect the returned position.
*
* @param document The rich-text content to insert.
*/
public MultiChangeBuilder insertAbsolutely(int paragraphIndex, int columnPosition, StyledDocument document) {
int pos = area.getAbsolutePosition(paragraphIndex, columnPosition);
return replaceAbsolutely(pos, pos, document);
}
/**
* Removes a range of text.
*
* @param range The range of text to delete. It must not be null. Its start and end values specify the start
* and end positions within the area.
*
* @see #deleteText(int, int)
*/
public MultiChangeBuilder deleteText(IndexRange range) {
return deleteText(range.getStart(), range.getEnd());
}
/**
* Removes a range of text.
*
* @param range The range of text to delete. It must not be null. Its start and end values specify the start
* and end positions within the area.
*
* @see #deleteText(int, int)
*/
public MultiChangeBuilder deleteTextAbsolutely(IndexRange range) {
return deleteTextAbsolutely(range.getStart(), range.getEnd());
}
/**
* Removes a range of text.
*
* It must hold {@code 0 <= start <= end <= getLength()}.
*
* @param start Start position of the range to remove
* @param end End position of the range to remove
*/
public MultiChangeBuilder deleteText(int start, int end) {
return replaceText(start, end, "");
}
/**
* Removes a range of text.
*
* It must hold {@code 0 <= start <= end <= getLength()}.
*
* @param start Start position of the range to remove
* @param end End position of the range to remove
*/
public MultiChangeBuilder deleteTextAbsolutely(int start, int end) {
return replaceTextAbsolutely(start, end, "");
}
/**
* Removes a range of text.
*
* It must hold {@code 0 <= start <= end <= getLength()} where
* {@code start = getAbsolutePosition(startParagraph, startColumn);} and is inclusive, and
* {@code int end = getAbsolutePosition(endParagraph, endColumn);} and is exclusive.
*
* Caution: see {@link StyledDocument#getAbsolutePosition(int, int)} to know how the column index argument
* can affect the returned position.
*/
public MultiChangeBuilder deleteText(int startParagraph, int startColumn, int endParagraph, int endColumn) {
int start = area.getAbsolutePosition(startParagraph, startColumn);
int end = area.getAbsolutePosition(endParagraph, endColumn);
return replaceText(start, end, "");
}
/**
* Removes a range of text.
*
* It must hold {@code 0 <= start <= end <= getLength()} where
* {@code start = getAbsolutePosition(startParagraph, startColumn);} and is inclusive, and
* {@code int end = getAbsolutePosition(endParagraph, endColumn);} and is exclusive.
*
* Caution: see {@link StyledDocument#getAbsolutePosition(int, int)} to know how the column index argument
* can affect the returned position.
*/
public MultiChangeBuilder deleteTextAbsolutely(int startParagraph, int startColumn, int endParagraph, int endColumn) {
int start = area.getAbsolutePosition(startParagraph, startColumn);
int end = area.getAbsolutePosition(endParagraph, endColumn);
return replaceTextAbsolutely(start, end, "");
}
/**
* Replaces a range of characters with the given text.
*
* It must hold {@code 0 <= start <= end <= getLength()}.
*
* @param start Start index of the range to replace, inclusive.
* @param end End index of the range to replace, exclusive.
* @param text The text to put in place of the deleted range.
* It must not be null.
*/
public MultiChangeBuilder replaceText(int start, int end, String text) {
return relativeReplace(start, end, ReadOnlyStyledDocument.fromString(
text, area.getInitialParagraphStyle(), area.getInitialTextStyle(), area.getSegOps())
);
}
/**
* Replaces a range of characters with the given text.
*
* It must hold {@code 0 <= start <= end <= getLength()}.
*
* @param start Start index of the range to replace, inclusive.
* @param end End index of the range to replace, exclusive.
* @param text The text to put in place of the deleted range.
* It must not be null.
*/
public MultiChangeBuilder replaceTextAbsolutely(int start, int end, String text) {
return absoluteReplace(start, end, ReadOnlyStyledDocument.fromString(
text, area.getInitialParagraphStyle(), area.getInitialTextStyle(), area.getSegOps())
);
}
/**
* Replaces a range of characters with the given rich-text document.
*/
public MultiChangeBuilder replace(int start, int end, StyledDocument replacement) {
return relativeReplace(start, end, ReadOnlyStyledDocument.from(replacement));
}
/**
* Replaces a range of characters with the given rich-text document.
*/
public MultiChangeBuilder replaceAbsolutely(int start, int end, StyledDocument replacement) {
return absoluteReplace(start, end, ReadOnlyStyledDocument.from(replacement));
}
/**
* Applies all the changes stored in this object to the underlying document of the area. Note: this builder
* cannot be reused and a new one must be created for each multi-change.
*/
public final void commit() {
ensureNotYetCreated();
ensureHasChanges();
alreadyCreated = true;
area.replaceMulti(Collections.unmodifiableList(list));
}
private MultiChangeBuilder relativeReplace(int start, int end, ReadOnlyStyledDocument replacement) {
if (list.isEmpty()) {
return absoluteReplace(start, end, replacement);
} else {
int realStart = start;
int realEnd = end;
for (Replacement r : list) {
if (r.getStart() <= realStart) {
realStart += r.getNetLength();
if (r.getEnd() <= realEnd) {
realEnd += r.getNetLength();
}
} else if (r.getEnd() <= realEnd) {
realEnd += r.getNetLength();
}
}
return absoluteReplace(realStart, realEnd, replacement);
}
}
private MultiChangeBuilder absoluteReplace(int start, int end, ReadOnlyStyledDocument replacement) {
// TODO: could be optimized to ignore earlier changes when later one makes them obsolete
// for example:
// builder
// .replaceTextAbsolutely(0, 1, "a")
// .replaceTextAbsolutely(0, 1, "b")
// .replaceTextAbsolutely(0, 1, "c") // makes previous two commits obsolete
// .commit();
list.add(new Replacement<>(start, end, replacement));
return this;
}
private void ensureNotYetCreated() {
if (alreadyCreated) {
throw new IllegalStateException("Cannot reuse a builder multiple times");
}
}
private void ensureHasChanges() {
if (list.isEmpty()) {
throw new IllegalStateException("Cannot commit multiple changes since none have been added");
}
}
public boolean hasChanges() {
return list.size() > 0;
}
}