org.fxmisc.richtext.CaretSelectionBindImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of richtextfx Show documentation
Show all versions of richtextfx Show documentation
Rich-text area for JavaFX
package org.fxmisc.richtext;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Bounds;
import javafx.scene.control.IndexRange;
import javafx.util.Duration;
import org.fxmisc.richtext.model.StyledDocument;
import org.reactfx.Subscription;
import org.reactfx.Suspendable;
import org.reactfx.SuspendableNo;
import org.reactfx.util.Tuple3;
import org.reactfx.util.Tuples;
import org.reactfx.value.SuspendableVal;
import org.reactfx.value.Val;
import org.reactfx.value.Var;
import java.text.BreakIterator;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.function.Function;
final class CaretSelectionBindImpl implements CaretSelectionBind {
// caret
@Override public Var showCaretProperty() { return delegateCaret.showCaretProperty(); }
@Override public CaretVisibility getShowCaret() { return delegateCaret.getShowCaret(); }
@Override public void setShowCaret(CaretVisibility value) { delegateCaret.setShowCaret(value); }
/* ********************************************************************** *
* *
* Observables *
* *
* Observables are "dynamic" (i.e. changing) characteristics of this *
* control. They are not directly settable by the client code, but change *
* in response to user input and/or API actions. *
* *
* ********************************************************************** */
// caret
@Override public ObservableValue positionProperty() { return delegateCaret.positionProperty(); }
@Override public int getPosition() { return delegateCaret.getPosition(); }
@Override public ObservableValue paragraphIndexProperty() { return delegateCaret.paragraphIndexProperty(); }
@Override public int getParagraphIndex() { return delegateCaret.getParagraphIndex(); }
@Override public ObservableValue lineIndexProperty() { return delegateCaret.lineIndexProperty(); }
@Override public OptionalInt getLineIndex() { return delegateCaret.getLineIndex(); }
@Override public ObservableValue columnPositionProperty() { return delegateCaret.columnPositionProperty(); }
@Override public int getColumnPosition() { return delegateCaret.getColumnPosition(); }
@Override public ObservableValue visibleProperty() { return delegateCaret.visibleProperty(); }
@Override public boolean isVisible() { return delegateCaret.isVisible(); }
@Override public ObservableValue blinkRateProperty() { return delegateCaret.blinkRateProperty(); }
@Override public Duration getBlinkRate() { return delegateCaret.getBlinkRate(); }
@Override public void setBlinkRate(Duration blinkRate) { delegateCaret.setBlinkRate(blinkRate); }
@Override public ObservableValue> caretBoundsProperty() { return delegateCaret.caretBoundsProperty(); }
@Override public Optional getCaretBounds() { return delegateCaret.getCaretBounds(); }
@Override public void clearTargetOffset() { delegateCaret.clearTargetOffset(); }
@Override public ParagraphBox.CaretOffsetX getTargetOffset() { return delegateCaret.getTargetOffset(); }
// selection
@Override public ObservableValue rangeProperty() { return delegateSelection.rangeProperty(); }
@Override public IndexRange getRange() { return delegateSelection.getRange(); }
@Override public ObservableValue lengthProperty() { return delegateSelection.lengthProperty(); }
@Override public int getLength() { return delegateSelection.getLength(); }
@Override public ObservableValue paragraphSpanProperty() { return delegateSelection.paragraphSpanProperty(); }
@Override public int getParagraphSpan() { return delegateSelection.getParagraphSpan(); }
@Override public final ObservableValue> selectedDocumentProperty() { return delegateSelection.selectedDocumentProperty(); }
@Override public final StyledDocument getSelectedDocument() { return delegateSelection.getSelectedDocument(); }
@Override public ObservableValue selectedTextProperty() { return delegateSelection.selectedTextProperty(); }
@Override public String getSelectedText() { return delegateSelection.getSelectedText(); }
@Override public ObservableValue startPositionProperty() { return delegateSelection.startPositionProperty(); }
@Override public int getStartPosition() { return delegateSelection.getStartPosition(); }
@Override public ObservableValue startParagraphIndexProperty() { return delegateSelection.startParagraphIndexProperty(); }
@Override public int getStartParagraphIndex() { return delegateSelection.getStartParagraphIndex(); }
@Override public ObservableValue startColumnPositionProperty() { return delegateSelection.startColumnPositionProperty(); }
@Override public int getStartColumnPosition() { return delegateSelection.getStartColumnPosition(); }
@Override public ObservableValue endPositionProperty() { return delegateSelection.endPositionProperty(); }
@Override public int getEndPosition() { return delegateSelection.getEndPosition(); }
@Override public ObservableValue endParagraphIndexProperty() { return delegateSelection.endParagraphIndexProperty(); }
@Override public int getEndParagraphIndex() { return delegateSelection.getEndParagraphIndex(); }
@Override public ObservableValue endColumnPositionProperty() { return delegateSelection.endColumnPositionProperty(); }
@Override public int getEndColumnPosition() { return delegateSelection.getEndColumnPosition(); }
@Override public ObservableValue> selectionBoundsProperty() { return delegateSelection.selectionBoundsProperty(); }
@Override public Optional getSelectionBounds() { return delegateSelection.getSelectionBounds(); }
private final CaretNode delegateCaret;
@Override public CaretNode getUnderlyingCaret() { return delegateCaret; }
@Override public String getCaretName() { return delegateCaret.getCaretName(); }
private final Selection delegateSelection;
@Override public Selection getUnderlyingSelection() { return delegateSelection; }
@Override public String getSelectionName() { return delegateSelection.getSelectionName(); }
@Override
public void configureSelectionPath(SelectionPath path) {
delegateSelection.configureSelectionPath(path);
}
@Override public GenericStyledArea getArea() { return delegateSelection.getArea(); }
// caret selection bind
private final Val anchorPosition;
@Override public int getAnchorPosition() { return anchorPosition.getValue(); }
@Override public ObservableValue anchorPositionProperty() { return anchorPosition; }
private final Val anchorParIndex;
@Override public int getAnchorParIndex() { return anchorParIndex.getValue(); }
@Override public ObservableValue anchorParIndexProperty() { return anchorParIndex; }
private final Val anchorColPosition;
@Override public int getAnchorColPosition() { return anchorColPosition.getValue(); }
@Override public ObservableValue anchorColPositionProperty() { return anchorColPosition; }
private final SuspendableNo beingUpdated = new SuspendableNo();
public final boolean isBeingUpdated() { return beingUpdated.get(); }
public final ObservableValue beingUpdatedProperty() { return beingUpdated; }
private final Var internalStartedByAnchor = Var.newSimpleVar( new BooleanEvent( true ) );
private final SuspendableVal startedByAnchor = internalStartedByAnchor.suspendable();
private boolean anchorIsStart() { return startedByAnchor.getValue().get(); }
private class BooleanEvent { // See #874
public BooleanEvent( boolean b ) { value = b; }
public boolean get() { return value; }
private final boolean value;
}
private Subscription subscription = () -> {};
CaretSelectionBindImpl(String caretName, String selectionName, GenericStyledArea area) {
this(caretName, selectionName, area, new IndexRange(0, 0));
}
CaretSelectionBindImpl(String caretName, String selectionName, GenericStyledArea area,
IndexRange startingRange) {
this(
(updater) -> new CaretNode(caretName, area, updater, startingRange.getStart()),
(updater) -> new SelectionImpl<>(selectionName, area, startingRange, updater)
);
}
CaretSelectionBindImpl(Function createCaret,
Function> createSelection) {
SuspendableNo delegateUpdater = new SuspendableNo();
delegateCaret = createCaret.apply(delegateUpdater);
delegateSelection = createSelection.apply(delegateUpdater);
if (delegateCaret.getArea() != delegateSelection.getArea()) {
throw new IllegalArgumentException(String.format(
"Caret and Selection must be asociated with the same area. Caret area = %s | Selection area = %s",
delegateCaret.getArea(), delegateSelection.getArea()
));
}
Val> anchorPositions = startedByAnchor.flatMap(b ->
b.get()
? Val.constant(Tuples.t(getStartPosition(), getStartParagraphIndex(), getStartColumnPosition()))
: Val.constant(Tuples.t(getEndPosition(), getEndParagraphIndex(), getEndColumnPosition()))
);
anchorPosition = anchorPositions.map(Tuple3::get1);
anchorParIndex = anchorPositions.map(Tuple3::get2);
anchorColPosition = anchorPositions.map(Tuple3::get3);
Suspendable omniSuspendable = Suspendable.combine(
// first, so it's released last
beingUpdated,
startedByAnchor,
// last, so it's released before startedByAnchor, so that anchor's values are correct
delegateUpdater
);
subscription = omniSuspendable.suspendWhen(getArea().beingUpdatedProperty());
}
/* ********************************************************************** *
* *
* Actions *
* *
* Actions change the state of this control. They typically cause a *
* change of one or more observables and/or produce an event. *
* *
* ********************************************************************** */
// caret
@Override
public void moveBreaksForwards(int numOfBreaks, BreakIterator breakIterator) {
if (getAreaLength() == 0) {
return;
}
breakIterator.setText(getArea().getText());
int position = calculatePositionViaBreakingForwards(numOfBreaks, breakIterator, getPosition());
moveTo(position, NavigationActions.SelectionPolicy.CLEAR);
}
@Override
public void moveBreaksBackwards(int numOfBreaks, BreakIterator breakIterator) {
if (getAreaLength() == 0) {
return;
}
breakIterator.setText(getArea().getText());
int position = calculatePositionViaBreakingBackwards(numOfBreaks, breakIterator, getPosition());
moveTo(position, NavigationActions.SelectionPolicy.CLEAR);
}
// selection
@Override
public void selectRange(int startPosition, int endPosition) {
doSelect(startPosition, endPosition, anchorIsStart());
}
@Override
public void selectRange(int startParagraphIndex, int startColPosition, int endParagraphIndex, int endColPosition) {
selectRange(textPosition(startParagraphIndex, startColPosition), textPosition(endParagraphIndex, endColPosition));
}
@Override
public void updateStartBy(int amount, Direction direction) {
int updatedStart = direction == Direction.LEFT
? getStartPosition() - amount
: getStartPosition() + amount;
selectRange(updatedStart, getEndPosition());
}
@Override
public void updateEndBy(int amount, Direction direction) {
int updatedEnd = direction == Direction.LEFT
? getEndPosition() - amount
: getEndPosition() + amount;
selectRange(getStartPosition(), updatedEnd);
}
@Override
public void updateStartTo(int position) {
selectRange(position, getEndPosition());
}
@Override
public void updateStartTo(int paragraphIndex, int columnPosition) {
updateStartTo(textPosition(paragraphIndex, columnPosition));
}
@Override
public void updateStartByBreaksForward(int numOfBreaks, BreakIterator breakIterator) {
if (getAreaLength() == 0) {
return;
}
breakIterator.setText(getArea().getText());
int position = calculatePositionViaBreakingForwards(numOfBreaks, breakIterator, getStartPosition());
updateStartTo(position);
}
@Override
public void updateStartByBreaksBackward(int numOfBreaks, BreakIterator breakIterator) {
if (getAreaLength() == 0) {
return;
}
breakIterator.setText(getArea().getText());
int position = calculatePositionViaBreakingBackwards(numOfBreaks, breakIterator, getStartPosition());
updateStartTo(position);
}
@Override
public void updateEndTo(int position) {
selectRange(getStartPosition(), position);
}
@Override
public void updateEndTo(int paragraphIndex, int columnPosition) {
updateEndTo(textPosition(paragraphIndex, columnPosition));
}
@Override
public void updateEndByBreaksForward(int numOfBreaks, BreakIterator breakIterator) {
if (getAreaLength() == 0) {
return;
}
breakIterator.setText(getArea().getText());
int position = calculatePositionViaBreakingForwards(numOfBreaks, breakIterator, getStartPosition());
updateEndTo(position);
}
@Override
public void updateEndByBreaksBackward(int numOfBreaks, BreakIterator breakIterator) {
if (getAreaLength() == 0) {
return;
}
breakIterator.setText(getArea().getText());
int position = calculatePositionViaBreakingBackwards(numOfBreaks, breakIterator, getStartPosition());
updateEndTo(position);
}
@Override
public void selectAll() {
selectRange(0, getAreaLength());
}
@Override
public void selectParagraph(int paragraphIndex) {
int start = textPosition(paragraphIndex, 0);
int end = getArea().getParagraphLength(paragraphIndex);
selectRange(start, end);
}
@Override
public void selectWord(int wordPositionInArea) {
if (getAreaLength() == 0) {
return;
}
BreakIterator breakIterator = BreakIterator.getWordInstance( getArea().getLocale() );
breakIterator.setText(getArea().getText());
int start = calculatePositionViaBreakingBackwards(1, breakIterator, wordPositionInArea);
int end = calculatePositionViaBreakingForwards(1, breakIterator, wordPositionInArea);
selectRange(start, end);
}
@Override
public void deselect() {
selectRangeExpl(getPosition(), getPosition());
}
// caret selection bind
@Override
public void selectRangeExpl(int anchorParagraph, int anchorColumn, int caretParagraph, int caretColumn) {
selectRangeExpl(textPosition(anchorParagraph, anchorColumn), textPosition(caretParagraph, caretColumn));
}
@Override
public void selectRangeExpl(int anchorPosition, int caretPosition) {
if (anchorPosition <= caretPosition) {
doSelect(anchorPosition, caretPosition, true);
} else {
doSelect(caretPosition, anchorPosition, false);
}
}
@Override
public void moveTo(int pos, NavigationActions.SelectionPolicy selectionPolicy) {
switch(selectionPolicy) {
case CLEAR:
selectRangeExpl(pos, pos);
break;
case ADJUST:
selectRangeExpl(getAnchorPosition(), pos);
break;
case EXTEND:
IndexRange sel = getRange();
int anchor;
if (pos <= sel.getStart()) {
anchor = sel.getEnd();
} else if(pos >= sel.getEnd()) {
anchor = sel.getStart();
} else {
anchor = getAnchorPosition();
}
selectRangeExpl(anchor, pos);
break;
}
}
@Override
public void moveTo(int paragraphIndex, int columnIndex, NavigationActions.SelectionPolicy selectionPolicy) {
moveTo(textPosition(paragraphIndex, columnIndex), selectionPolicy);
}
@Override
public void moveToPrevChar(NavigationActions.SelectionPolicy selectionPolicy) {
if (getPosition() > 0) {
int newCaretPos = Character.offsetByCodePoints(getArea().getText(), getPosition(), -1);
moveTo(newCaretPos, selectionPolicy);
}
}
@Override
public void moveToNextChar(NavigationActions.SelectionPolicy selectionPolicy) {
if (getPosition() < getAreaLength()) {
int newCaretPos = Character.offsetByCodePoints(getArea().getText(), getPosition(), 1);
moveTo(newCaretPos, selectionPolicy);
}
}
@Override
public void moveToParStart(NavigationActions.SelectionPolicy selectionPolicy) {
moveTo(getPosition() - getColumnPosition(), selectionPolicy);
}
@Override
public void moveToParEnd(NavigationActions.SelectionPolicy selectionPolicy) {
moveTo(getPosition() - getColumnPosition() + getArea().getParagraphLength(getParagraphIndex()), selectionPolicy);
}
@Override
public void moveToAreaStart(NavigationActions.SelectionPolicy selectionPolicy) {
moveTo(0, selectionPolicy);
}
@Override
public void moveToAreaEnd(NavigationActions.SelectionPolicy selectionPolicy) {
moveTo(getArea().getLength(), selectionPolicy);
}
@Override
public void displaceCaret(int position) {
doUpdate(() -> delegateCaret.moveTo(position));
}
@Override
public void displaceSelection(int startPosition, int endPosition) {
doUpdate(() -> {
delegateSelection.selectRange(startPosition, endPosition);
internalStartedByAnchor.setValue( new BooleanEvent( startPosition < endPosition ) );
});
}
@Override
public void dispose() {
subscription.unsubscribe();
}
/* ********************************************************************** *
* *
* Private methods *
* *
* ********************************************************************** */
private void doSelect(int startPosition, int endPosition, boolean anchorIsStart) {
doUpdate(() -> {
delegateSelection.selectRange(startPosition, endPosition);
internalStartedByAnchor.setValue( new BooleanEvent( anchorIsStart ) );
delegateCaret.moveTo(anchorIsStart ? endPosition : startPosition);
});
}
private void doUpdate(Runnable updater) {
if (isBeingUpdated()) {
updater.run();
} else {
getArea().beingUpdatedProperty().suspendWhile(updater);
}
}
private int textPosition(int paragraphIndex, int columnPosition) {
return getArea().position(paragraphIndex, columnPosition).toOffset();
}
private int getAreaLength() { return getArea().getLength(); }
/** Assumes that {@code getArea().getLength != 0} is true and {@link BreakIterator#setText(String)} has been called */
private int calculatePositionViaBreakingForwards(int numOfBreaks, BreakIterator breakIterator, int position) {
breakIterator.following(position);
for (int i = 1; i < numOfBreaks; i++) {
breakIterator.next(numOfBreaks);
}
return breakIterator.current();
}
/** Assumes that {@code getArea().getLength != 0} is true and {@link BreakIterator#setText(String)} has been called */
private int calculatePositionViaBreakingBackwards(int numOfBreaks, BreakIterator breakIterator, int position) {
breakIterator.preceding(position);
for (int i = 1; i < numOfBreaks; i++) {
breakIterator.previous();
}
return breakIterator.current();
}
}