Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.fxmisc.richtext.EditableStyledDocument Maven / Gradle / Ivy
package org.fxmisc.richtext;
import static org.fxmisc.richtext.ReadOnlyStyledDocument.ParagraphsPolicy.*;
import static org.fxmisc.richtext.TwoDimensional.Bias.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.beans.value.ObservableIntegerValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.fxmisc.richtext.ReadOnlyStyledDocument.ParagraphsPolicy;
import org.reactfx.EventSource;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.Guard;
import org.reactfx.inhibeans.property.ReadOnlyIntegerWrapper;
/**
* Content model for {@link StyledTextArea}. Implements edit operations
* on styled text, but not worrying about additional aspects such as
* caret or selection.
*/
final class EditableStyledDocument
extends StyledDocumentBase>> {
/* ********************************************************************** *
* *
* Observables *
* *
* Observables are "dynamic" (i.e. changing) characteristics of an object.*
* They are not directly settable by the client code, but change in *
* response to user input and/or API actions. *
* *
* ********************************************************************** */
/**
* Content of this {@code StyledDocument}.
*/
private final StringBinding text = Bindings.createStringBinding(() -> getText(0, length()));
@Override
public String getText() { return text.getValue(); }
public ObservableValue textProperty() { return text; }
/**
* Length of this {@code StyledDocument}.
*/
private final ReadOnlyIntegerWrapper length = new ReadOnlyIntegerWrapper();
public int getLength() { return length.get(); }
public ObservableIntegerValue lengthProperty() { return length.getReadOnlyProperty(); }
@Override
public int length() { return length.get(); }
/**
* Unmodifiable observable list of styled paragraphs of this document.
*/
@Override
public ObservableList> getParagraphs() {
return FXCollections.unmodifiableObservableList(paragraphs);
}
/**
* Read-only snapshot of the current state of this document.
*/
public ReadOnlyStyledDocument snapshot() {
return new ReadOnlyStyledDocument<>(paragraphs, ParagraphsPolicy.COPY);
}
/* ********************************************************************** *
* *
* Event streams *
* *
* ********************************************************************** */
// To publish a text change:
// 1. push to textChangePosition,
// 2. push to textRemovalEnd,
// 3. push to insertedText.
//
// To publish a rich change:
// a)
// 1. push to textChangePosition,
// 2. push to textRemovalEnd,
// 3. push to insertedDocument;
// b)
// 1. push to textChangePosition,
// 2. push to textRemovalEnd,
// 3. push to insertionLength;
// c)
// 1. push to styleChangePosition
// 2. push to styleChangeEnd
// 3. push to styleChangeDone.
private final EventSource textChangePosition = new EventSource<>();
private final EventSource styleChangePosition = new EventSource<>();
private final EventSource textRemovalEnd = new EventSource<>();
private final EventSource styleChangeEnd = new EventSource<>();
private final EventSource insertedText = new EventSource<>();
private final EventSource> insertedDocument = new EventSource<>();
private final EventSource insertionLength = new EventSource<>();
private final EventSource styleChangeDone = new EventSource<>();
private final EventStream plainTextChanges;
public EventStream plainTextChanges() { return plainTextChanges; }
private final EventStream> richChanges;
public EventStream> richChanges() { return richChanges; }
{
EventStream removedText = EventStreams.zip(textChangePosition, textRemovalEnd).map((a, b) -> getText(a, b));
EventStream changePosition = EventStreams.merge(textChangePosition, styleChangePosition);
EventStream removalEnd = EventStreams.merge(textRemovalEnd, styleChangeEnd);
EventStream> removedDocument = EventStreams.zip(changePosition, removalEnd).map((a, b) -> subSequence(a, b));
EventStream insertionEnd = EventStreams.merge(
changePosition.emitBothOnEach(insertionLength).map((start, len) -> start + len),
styleChangeEnd.emitOn(styleChangeDone));
EventStream> insertedDocument = EventStreams.merge(
this.insertedDocument,
changePosition.emitBothOnEach(insertionEnd).map((a, b) -> subSequence(a, b)));
plainTextChanges = EventStreams.zip(textChangePosition, removedText, insertedText)
.map((pos, removed, inserted) -> new PlainTextChange(pos, removed, inserted));
richChanges = EventStreams.zip(changePosition, removedDocument, insertedDocument)
.map((pos, removed, inserted) -> new RichTextChange(pos, removed, inserted));
}
/* ********************************************************************** *
* *
* Constructors *
* *
* ********************************************************************** */
@SuppressWarnings("unchecked")
EditableStyledDocument(S initialStyle) {
super(FXCollections.observableArrayList(new Paragraph("", initialStyle)));
length.set(0);
}
/* ********************************************************************** *
* *
* Actions *
* *
* Actions change the state of the object. They typically cause a change *
* of one or more observables and/or produce an event. *
* *
* ********************************************************************** */
public void replaceText(int start, int end, String replacement) {
ensureValidRange(start, end);
replace(start, end, replacement,
(repl, pos) -> stringToParagraphs(repl, getStyleForInsertionAt(pos)),
repl -> {
insertedText.push(repl);
insertionLength.push(repl.length());
});
}
public void replace(int start, int end, StyledDocument replacement) {
ensureValidRange(start, end);
replace(start, end, replacement,
(repl, pos) -> repl.getParagraphs(),
repl -> {
insertedText.push(repl.toString());
StyledDocument doc =
repl instanceof ReadOnlyStyledDocument
? repl
: new ReadOnlyStyledDocument<>(repl.getParagraphs(), COPY);
insertedDocument.push(doc);
});
}
public void setStyle(int from, int to, S style) {
ensureValidRange(from, to);
try(Guard commitOnClose = beginStyleChange(from, to)) {
Position start = navigator.offsetToPosition(from, Forward);
Position end = to == from
? start
: start.offsetBy(to - from, Backward);
int firstParIdx = start.getMajor();
int firstParFrom = start.getMinor();
int lastParIdx = end.getMajor();
int lastParTo = end.getMinor();
if(firstParIdx == lastParIdx) {
Paragraph p = paragraphs.get(firstParIdx);
p = p.restyle(firstParFrom, lastParTo, style);
paragraphs.set(firstParIdx, p);
} else {
int affectedPars = lastParIdx - firstParIdx + 1;
List> restyledPars = new ArrayList<>(affectedPars);
Paragraph firstPar = paragraphs.get(firstParIdx);
restyledPars.add(firstPar.restyle(firstParFrom, firstPar.length(), style));
for(int i = firstParIdx + 1; i < lastParIdx; ++i) {
Paragraph p = paragraphs.get(i);
restyledPars.add(p.restyle(style));
}
Paragraph lastPar = paragraphs.get(lastParIdx);
restyledPars.add(lastPar.restyle(0, lastParTo, style));
setAll(firstParIdx, lastParIdx + 1, restyledPars);
}
}
}
public void setStyle(int paragraph, S style) {
Paragraph p = paragraphs.get(paragraph);
int start = position(paragraph, 0).toOffset();
int end = start + p.length();
try(Guard commitOnClose = beginStyleChange(start, end)) {
p = p.restyle(style);
paragraphs.set(paragraph, p);
}
}
public void setStyle(int paragraph, int fromCol, int toCol, S style) {
ensureValidParagraphRange(paragraph, fromCol, toCol);
int parOffset = position(paragraph, 0).toOffset();
int start = parOffset + fromCol;
int end = parOffset + toCol;
try(Guard commitOnClose = beginStyleChange(start, end)) {
Paragraph p = paragraphs.get(paragraph);
p = p.restyle(fromCol, toCol, style);
paragraphs.set(paragraph, p);
}
}
public void setStyleSpans(int from, StyleSpans styleSpans) {
int len = styleSpans.length();
ensureValidRange(from, from + len);
Position start = offsetToPosition(from, Forward);
Position end = start.offsetBy(len, Backward);
int skip = terminatorLengthToSkip(start);
int trim = terminatorLengthToTrim(end);
if(skip + trim >= len) {
return;
} else if(skip + trim > 0) {
styleSpans = styleSpans.subView(skip, len - trim);
len -= skip + trim;
from += skip;
start = start.offsetBy(skip, Forward);
end = end.offsetBy(-trim, Backward);
}
try(Guard commitOnClose = beginStyleChange(from, from + len)) {
int firstParIdx = start.getMajor();
int firstParFrom = start.getMinor();
int lastParIdx = end.getMajor();
int lastParTo = end.getMinor();
if(firstParIdx == lastParIdx) {
Paragraph p = paragraphs.get(firstParIdx);
Paragraph q = p.restyle(firstParFrom, styleSpans);
if(q != p) {
paragraphs.set(firstParIdx, q);
}
} else {
Paragraph firstPar = paragraphs.get(firstParIdx);
Position spansFrom = styleSpans.position(0, 0);
Position spansTo = spansFrom.offsetBy(firstPar.length() - firstParFrom, Backward);
Paragraph q = firstPar.restyle(firstParFrom, styleSpans.subView(spansFrom, spansTo));
if(q != firstPar) {
paragraphs.set(firstParIdx, q);
}
spansFrom = spansTo.offsetBy(firstPar.getLineTerminator().map(LineTerminator::length).orElse(0), Forward); // skip the newline
for(int i = firstParIdx + 1; i < lastParIdx; ++i) {
Paragraph par = paragraphs.get(i);
spansTo = spansFrom.offsetBy(par.length(), Backward);
q = par.restyle(0, styleSpans.subView(spansFrom, spansTo));
if(q != par) {
paragraphs.set(i, q);
}
spansFrom = spansTo.offsetBy(par.getLineTerminator().map(LineTerminator::length).orElse(0), Forward); // skip the newline
}
Paragraph lastPar = paragraphs.get(lastParIdx);
spansTo = spansFrom.offsetBy(lastParTo, Backward);
q = lastPar.restyle(0, styleSpans.subView(spansFrom, spansTo));
if(q != lastPar) {
paragraphs.set(lastParIdx, q);
}
}
}
}
public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) {
int len = styleSpans.length();
ensureValidParagraphRange(paragraph, from, len);
int parOffset = position(paragraph, 0).toOffset();
int start = parOffset + from;
int end = start + len;
try(Guard commitOnClose = beginStyleChange(start, end)) {
Paragraph p = paragraphs.get(paragraph);
Paragraph q = p.restyle(from, styleSpans);
if(q != p) {
paragraphs.set(paragraph, q);
}
}
}
/* ********************************************************************** *
* *
* Private methods *
* *
* ********************************************************************** */
private static List> stringToParagraphs(String str, S style) {
Matcher m = LineTerminator.regex().matcher(str);
int n = 1;
while(m.find()) ++n;
List> res = new ArrayList<>(n);
int start = 0;
m.reset();
while(m.find()) {
String s = str.substring(start, m.start());
LineTerminator t = LineTerminator.from(m.group());
res.add(new Paragraph(s, style).terminate(t));
start = m.end();
}
String last = str.substring(start);
res.add(new Paragraph<>(last, style));
return res;
}
private void ensureValidRange(int start, int end) {
ensureValidRange(start, end, length());
}
private void ensureValidParagraphRange(int par, int start, int end) {
if(par < 0 || par >= paragraphs.size()) {
throw new IllegalArgumentException(par + " is not a valid paragraph index. Must be from [0, " + paragraphs.size() + ")");
}
ensureValidRange(start, end, paragraphs.get(par).fullLength());
}
private void ensureValidRange(int start, int end, int len) {
if(start < 0) {
throw new IllegalArgumentException("start cannot be negative: " + start);
}
if(end > len) {
throw new IllegalArgumentException("end is greater than length: " + end + " > " + len);
}
if(start > end) {
throw new IllegalArgumentException("start is greater than end: " + start + " > " + end);
}
}
private int terminatorLengthToSkip(Position pos) {
Paragraph par = paragraphs.get(pos.getMajor());
int skipSum = 0;
while(pos.getMinor() >= par.length() && pos.getMinor() < par.fullLength()) {
int skipLen = par.fullLength() - pos.getMinor();
skipSum += skipLen;
pos = pos.offsetBy(skipLen, Forward); // will jump to the next paragraph, if not at the end
par = paragraphs.get(pos.getMajor());
}
return skipSum;
}
private int terminatorLengthToTrim(Position pos) {
int parLen = paragraphs.get(pos.getMajor()).length();
int trimSum = 0;
while(pos.getMinor() > parLen) {
int trimLen = pos.getMinor() - parLen;
trimSum += trimLen;
pos = pos.offsetBy(-trimLen, Backward); // may jump to the end of previous paragraph, if parLen was 0
parLen = paragraphs.get(pos.getMajor()).length();
}
return trimSum;
}
private Guard beginStyleChange(int start, int end) {
styleChangePosition.push(start);
styleChangeEnd.push(end);
return () -> styleChangeDone.push(null);
}
/**
* Generic implementation for text replacement.
* @param start
* @param end
* @param replacement
* @param replacementToParagraphs function to convert the replacement
* to paragraphs. In addition to replacement itself, it is also given
* the position at which the replacement is going to be inserted. This
* position can be used to determine the style of the resulting paragraphs.
* @param publishReplacement completes the change events. It has to push
* exactly one value {@link #insertedText} and exactly one value to exactly
* one of {@link #insertedDocument}, {@link #insertionLength}.
*/
private void replace(
int start, int end, D replacement,
BiFunction>> replacementToParagraphs,
Consumer publishReplacement) {
textChangePosition.push(start);
textRemovalEnd.push(end);
Position start2D = navigator.offsetToPosition(start, Forward);
Position end2D = start2D.offsetBy(end - start, Forward);
int firstParIdx = start2D.getMajor();
int firstParFrom = start2D.getMinor();
int lastParIdx = end2D.getMajor();
int lastParTo = end2D.getMinor();
// Get the leftovers after cutting out the deletion
Paragraph firstPar = paragraphs.get(firstParIdx).trim(firstParFrom);
Paragraph lastPar = paragraphs.get(lastParIdx).subSequence(lastParTo);
List> replacementPars = replacementToParagraphs.apply(replacement, start2D);
List> newPars = join(firstPar, replacementPars, lastPar);
setAll(firstParIdx, lastParIdx + 1, newPars);
// update length, invalidate text
int newLength = length.get() - (end - start) + replacement.length();
length.blockWhile(() -> { // don't publish length change until text is invalidated
length.set(newLength);
text.invalidate();
});
// complete the change events
publishReplacement.accept(replacement);
}
private List> join(Paragraph first, List> middle, Paragraph last) {
int m = middle.size();
if(m == 0) {
return join(first, last);
} else if(!first.isTerminated()) {
first = first.concat(middle.get(0));
middle = middle.subList(1, m);
return join(first, middle, last);
} else {
Paragraph lastMiddle = middle.get(m - 1);
if(lastMiddle.isTerminated()) {
int n = 1 + m + 1;
List> res = new ArrayList<>(n);
res.add(first);
res.addAll(middle);
res.add(last);
return res;
} else {
int n = 1 + m;
List> res = new ArrayList<>(n);
res.add(first);
res.addAll(middle.subList(0, m - 1));
res.add(lastMiddle.concat(last));
return res;
}
}
}
private List> join(Paragraph first, Paragraph last) {
return first.isTerminated()
? Arrays.asList(first, last)
: Arrays.asList(first.concat(last));
}
// TODO: Replace with ObservableList.setAll(from, to, col) when implemented.
// See https://javafx-jira.kenai.com/browse/RT-32655.
private void setAll(int startIdx, int endIdx, Collection> pars) {
if(startIdx > 0 || endIdx < paragraphs.size()) {
paragraphs.subList(startIdx, endIdx).clear(); // note that paragraphs remains non-empty at all times
paragraphs.addAll(startIdx, pars);
} else {
paragraphs.setAll(pars);
}
}
private S getStyleForInsertionAt(Position insertionPos) {
Paragraph par = paragraphs.get(insertionPos.getMajor());
return par.getStyleAtPosition(insertionPos.getMinor());
}
}