From 995d7758c4aab6927ccc0f6a3ed8eeb033e3b790 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 10:03:42 -0800 Subject: [PATCH 01/13] Extract EMPTY_RANGE and clamp() to utilities class - These are used throughout the project and are stand-alone. This reduces code duplication --- .../fxmisc/richtext/GenericStyledArea.java | 8 ++----- .../richtext/model/StyledTextAreaModel.java | 17 ++------------ .../org/fxmisc/richtext/util/Utilities.java | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/util/Utilities.java diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index d122c337f..26ee461f6 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -2,6 +2,7 @@ import static javafx.util.Duration.*; import static org.fxmisc.richtext.PopupAlignment.*; +import static org.fxmisc.richtext.util.Utilities.EMPTY_RANGE; import static org.reactfx.EventStreams.*; import static org.reactfx.util.Tuples.*; @@ -159,11 +160,6 @@ public class GenericStyledArea extends Region TwoDimensional, Virtualized { - /** - * Index range [0, 0). - */ - public static final IndexRange EMPTY_RANGE = new IndexRange(0, 0); - private static final PseudoClass HAS_CARET = PseudoClass.getPseudoClass("has-caret"); private static final PseudoClass FIRST_PAR = PseudoClass.getPseudoClass("first-paragraph"); private static final PseudoClass LAST_PAR = PseudoClass.getPseudoClass("last-paragraph"); @@ -1278,7 +1274,7 @@ private Cell, ParagraphBox> createCell( int idx = box.getIndex(); return idx != -1 ? getParagraphSelection(idx) - : StyledTextArea.EMPTY_RANGE; + : EMPTY_RANGE; }, selectionProperty(), box.indexProperty()); box.selectionProperty().bind(cellSelection); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index 9680195f3..b74ca0919 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -1,6 +1,8 @@ package org.fxmisc.richtext.model; import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; +import static org.fxmisc.richtext.util.Utilities.EMPTY_RANGE; +import static org.fxmisc.richtext.util.Utilities.clamp; import java.util.Optional; import java.util.function.BiFunction; @@ -40,21 +42,6 @@ public class StyledTextAreaModel UndoActions, TwoDimensional { - /** - * Index range [0, 0). - */ - public static final IndexRange EMPTY_RANGE = new IndexRange(0, 0); - - /** - * Private helper method. - */ - private static int clamp(int min, int val, int max) { - return val < min ? min - : val > max ? max - : val; - } - - /* ********************************************************************** * * * * Properties * diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/util/Utilities.java b/richtextfx/src/main/java/org/fxmisc/richtext/util/Utilities.java new file mode 100644 index 000000000..dd9490b9a --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/util/Utilities.java @@ -0,0 +1,23 @@ +package org.fxmisc.richtext.util; + +import javafx.scene.control.IndexRange; + +/** + * A class of static methods or properties used throughout the project. + */ +public class Utilities { + + /** + * Index range [0, 0). + */ + public static final IndexRange EMPTY_RANGE = new IndexRange(0, 0); + + /** + * Clamps the given {@code val} to insure it is within the bounds of {@code min} or {@code max}. + */ + public static int clamp(int min, int val, int max) { + return val < min ? min + : val > max ? max + : val; + } +} From 5881a03ba6baf6b85b8ed4796b97cb8d209140f1 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 10:33:47 -0800 Subject: [PATCH 02/13] Extract caret and selection-related model into `CaretSelectionModel` class. - Renamed `currentParagraph` to `caretParagraph` as this will eventually allow multiple carets/selections to be used. --- .../demo/lineindicator/LineIndicatorDemo.java | 2 +- .../richtext/demo/richtext/RichText.java | 2 +- .../fxmisc/richtext/GenericStyledArea.java | 28 +-- .../richtext/model/CaretSelectionModel.java | 216 ++++++++++++++++++ .../richtext/model/NavigationActions.java | 2 +- .../richtext/model/StyledTextAreaModel.java | 161 ++----------- .../richtext/model/TextEditingArea.java | 4 +- 7 files changed, 260 insertions(+), 155 deletions(-) create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/lineindicator/LineIndicatorDemo.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/lineindicator/LineIndicatorDemo.java index 3587434f5..1cd72ca8d 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/lineindicator/LineIndicatorDemo.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/lineindicator/LineIndicatorDemo.java @@ -34,7 +34,7 @@ public void start(Stage primaryStage) { CodeArea codeArea = new CodeArea(); IntFunction numberFactory = LineNumberFactory.get(codeArea); - IntFunction arrowFactory = new ArrowFactory(codeArea.currentParagraphProperty()); + IntFunction arrowFactory = new ArrowFactory(codeArea.caretParagraphProperty()); IntFunction graphicFactory = line -> { HBox hbox = new HBox( numberFactory.apply(line), diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RichText.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RichText.java index 7341ec050..3dd6dbd66 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RichText.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RichText.java @@ -169,7 +169,7 @@ protected boolean computeValue() { Color[] backgrounds = styles.styleStream().map(s -> s.backgroundColor.orElse(null)).distinct().toArray(i -> new Color[i]); backgroundColor = backgrounds.length == 1 ? backgrounds[0] : null; } else { - int p = area.getCurrentParagraph(); + int p = area.getCaretParagraph(); int col = area.getCaretColumn(); TextStyle style = area.getStyleAtPosition(p, col); bold = style.bold.orElse(false); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index 26ee461f6..f23548865 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -371,8 +371,8 @@ public Optional, Codec>> getStyleCodecs() { public final ObservableValue> selectionBoundsProperty() { return selectionBounds; } // current paragraph index - @Override public final int getCurrentParagraph() { return model.getCurrentParagraph(); } - @Override public final ObservableValue currentParagraphProperty() { return model.currentParagraphProperty(); } + @Override public final int getCaretParagraph() { return model.getCaretParagraph(); } + @Override public final ObservableValue caretParagraphProperty() { return model.caretParagraphProperty(); } // caret column @Override public final int getCaretColumn() { return model.getCaretColumn(); } @@ -710,7 +710,7 @@ public GenericStyledArea( * of the embedded VirtualFlow. */ Optional getCaretBoundsInViewport() { - return virtualFlow.getCellIfVisible(getCurrentParagraph()) + return virtualFlow.getCellIfVisible(getCaretParagraph()) .map(c -> { Bounds cellBounds = c.getNode().getCaretBounds(); return virtualFlow.cellToViewport(c, cellBounds); @@ -721,7 +721,7 @@ Optional getCaretBoundsInViewport() { * Returns x coordinate of the caret in the current paragraph. */ ParagraphBox.CaretOffsetX getCaretOffsetX() { - int idx = getCurrentParagraph(); + int idx = getCaretParagraph(); return getCell(idx).getCaretOffsetX(); } @@ -791,7 +791,7 @@ public CharacterHit hit(double x, double y) { * paragraph to the viewport if it is not already visible. */ TwoDimensional.Position currentLine() { - int parIdx = getCurrentParagraph(); + int parIdx = getCaretParagraph(); Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); int lineIdx = cell.getNode().getCurrentLineIndex(); return _position(parIdx, lineIdx); @@ -1008,7 +1008,7 @@ public void showParagraphAtBottom(int paragraphIndex) { } void showCaretAtBottom() { - int parIdx = getCurrentParagraph(); + int parIdx = getCaretParagraph(); Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); Bounds caretBounds = cell.getNode().getCaretBounds(); double y = caretBounds.getMaxY(); @@ -1016,7 +1016,7 @@ void showCaretAtBottom() { } void showCaretAtTop() { - int parIdx = getCurrentParagraph(); + int parIdx = getCaretParagraph(); Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); Bounds caretBounds = cell.getNode().getCaretBounds(); double y = caretBounds.getMinY(); @@ -1034,7 +1034,7 @@ public void requestFollowCaret() { } private void followCaret() { - int parIdx = getCurrentParagraph(); + int parIdx = getCaretParagraph(); Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); Bounds caretBounds = cell.getNode().getCaretBounds(); double graphicWidth = cell.getNode().getGraphicPrefWidth(); @@ -1049,8 +1049,8 @@ private void followCaret() { * @param policy */ public void lineStart(SelectionPolicy policy) { - int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineStartPosition(); - moveTo(getCurrentParagraph(), columnPos, policy); + int columnPos = virtualFlow.getCell(getCaretParagraph()).getNode().getCurrentLineStartPosition(); + moveTo(getCaretParagraph(), columnPos, policy); } /** @@ -1060,8 +1060,8 @@ public void lineStart(SelectionPolicy policy) { * @param policy */ public void lineEnd(SelectionPolicy policy) { - int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineEndPosition(); - moveTo(getCurrentParagraph(), columnPos, policy); + int columnPos = virtualFlow.getCell(getCaretParagraph()).getNode().getCurrentLineEndPosition(); + moveTo(getCaretParagraph(), columnPos, policy); } /** @@ -1249,7 +1249,7 @@ private Cell, ParagraphBox> createCell( Val hasCaret = Val.combine( box.indexProperty(), - currentParagraphProperty(), + caretParagraphProperty(), (bi, cp) -> bi.intValue() == cp.intValue()); Subscription hasCaretPseudoClass = hasCaret.values().subscribe(value -> box.pseudoClassStateChanged(HAS_CARET, value)); @@ -1365,7 +1365,7 @@ private void positionPopup( } private Optional getCaretBoundsOnScreen() { - return virtualFlow.getCellIfVisible(getCurrentParagraph()) + return virtualFlow.getCellIfVisible(getCaretParagraph()) .map(c -> c.getNode().getCaretBoundsOnScreen()); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java new file mode 100644 index 000000000..c52a11092 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java @@ -0,0 +1,216 @@ +package org.fxmisc.richtext.model; + +import javafx.beans.value.ObservableValue; +import javafx.scene.control.IndexRange; +import org.fxmisc.richtext.model.TwoDimensional.Bias; +import org.fxmisc.richtext.model.TwoDimensional.Position; +import org.reactfx.Guard; +import org.reactfx.Subscription; +import org.reactfx.Suspendable; +import org.reactfx.value.SuspendableVal; +import org.reactfx.value.SuspendableVar; +import org.reactfx.value.Val; +import org.reactfx.value.Var; + +import java.util.function.Supplier; + +import static org.fxmisc.richtext.util.Utilities.EMPTY_RANGE; +import static org.fxmisc.richtext.util.Utilities.clamp; +import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward; +import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward; + +/** + * Encapsulates all caret and selection related information for {@link StyledTextAreaModel} and utilizes + * {@link Suspendable suspendable properties} to insure that changes to these values don't propagate until + * changes have been finished to the underlying {@link EditableStyledDocument}. + * + *

Terms

+ *
    + *
  • Position: the position in-between letters of a text (e.g. if "|" represents the caret and our text is + * "text", then all possible positions are "|t|e|x|t|"
  • + *
+ * + */ +final class CaretSelectionModel { + + // caret position + private final Var internalCaretPosition = Var.newSimpleVar(0); + /** + * The position of the caret within the text + */ + private final SuspendableVal caretPosition = internalCaretPosition.suspendable(); + public final int getCaretPosition() { return caretPosition.getValue(); } + public final ObservableValue caretPositionProperty() { return caretPosition; } + + // selection anchor + private final SuspendableVar anchor = Var.newSimpleVar(0).suspendable(); + public final int getAnchor() { return anchor.getValue(); } + public final ObservableValue anchorProperty() { return anchor; } + + // selection + private final Var internalSelection = Var.newSimpleVar(EMPTY_RANGE); + private final SuspendableVal selection = internalSelection.suspendable(); + public final IndexRange getSelection() { return selection.getValue(); } + public final ObservableValue selectionProperty() { return selection; } + + // selected text + private final SuspendableVal selectedText; + public final String getSelectedText() { return selectedText.getValue(); } + public final ObservableValue selectedTextProperty() { return selectedText; } + + // caret paragraph index + private final SuspendableVal caretParagraph; + public final int getCaretParagraph() { return caretParagraph.getValue(); } + public final ObservableValue caretParagraphProperty() { return caretParagraph; } + + // caret column position + private final SuspendableVal caretColumn; + public final int getCaretColumn() { return caretColumn.getValue(); } + public final ObservableValue caretColumnProperty() { return caretColumn; } + + private Position selectionStart2D; + private Position selectionEnd2D; + + private final StyledTextAreaModel model; + private final Suspendable modelBeingUpdated; + private final Subscription subscription; + + CaretSelectionModel(StyledTextAreaModel model, Supplier beingUpdatedSupplier) { + this.model = model; + modelBeingUpdated = beingUpdatedSupplier.get(); + Val caretPosition2D = Val.create( + () -> offsetToPosition(internalCaretPosition.getValue(), Forward), + internalCaretPosition, model.getParagraphs() + ); + caretParagraph = caretPosition2D.map(Position::getMajor).suspendable(); + caretColumn = caretPosition2D.map(Position::getMinor).suspendable(); + + selectionStart2D = position(0, 0); + selectionEnd2D = position(0, 0); + internalSelection.addListener(obs -> { + IndexRange sel = internalSelection.getValue(); + selectionStart2D = offsetToPosition(sel.getStart(), Forward); + selectionEnd2D = sel.getLength() == 0 + ? selectionStart2D + : selectionStart2D.offsetBy(sel.getLength(), Backward); + }); + + selectedText = Val.create( + () -> model.getText(internalSelection.getValue()), + internalSelection, model.getParagraphs()).suspendable(); + + // when content is updated by an area, update the caret + // and selection ranges of all the other + // clones that also share this document + subscription = model.plainTextChanges().subscribe(plainTextChange -> { + int changeLength = plainTextChange.getInserted().length() - plainTextChange.getRemoved().length(); + if (changeLength != 0) { + int indexOfChange = plainTextChange.getPosition(); + // in case of a replacement: "hello there" -> "hi." + int endOfChange = indexOfChange + Math.abs(changeLength); + + // update caret + int caretPosition = getCaretPosition(); + if (indexOfChange < caretPosition) { + // if caret is within the changed content, move it to indexOfChange + // otherwise offset it by changeLength + positionCaret( + caretPosition < endOfChange + ? indexOfChange + : caretPosition + changeLength + ); + } + // update selection + int selectionStart = getSelection().getStart(); + int selectionEnd = getSelection().getEnd(); + if (selectionStart != selectionEnd) { + // if start/end is within the changed content, move it to indexOfChange + // otherwise, offset it by changeLength + // Note: if both are moved to indexOfChange, selection is empty. + if (indexOfChange < selectionStart) { + selectionStart = selectionStart < endOfChange + ? indexOfChange + : selectionStart + changeLength; + } + if (indexOfChange < selectionEnd) { + selectionEnd = selectionEnd < endOfChange + ? indexOfChange + : selectionEnd + changeLength; + } + selectRange(selectionStart, selectionEnd); + } else { + // force-update internalSelection in case caret is + // at the end of area and a character was deleted + // (prevents a StringIndexOutOfBoundsException because + // selection's end is one char farther than area's length). + int internalCaretPos = internalCaretPosition.getValue(); + selectRange(internalCaretPos, internalCaretPos); + } + } + }); + } + + Suspendable omniSuspendable() { + return Suspendable.combine(caretPosition, anchor, selection, selectedText, caretParagraph, caretColumn); + } + + Position position(int row, int col) { + return model.position(row, col); + } + + Position offsetToPosition(int charOffset, Bias bias) { + return model.offsetToPosition(charOffset, bias); + } + + /** + * Returns the selection range in the given paragraph. + */ + public IndexRange getParagraphSelection(int paragraph) { + int startPar = selectionStart2D.getMajor(); + int endPar = selectionEnd2D.getMajor(); + + if(paragraph < startPar || paragraph > endPar) { + return EMPTY_RANGE; + } + + int start = paragraph == startPar ? selectionStart2D.getMinor() : 0; + int end = paragraph == endPar ? selectionEnd2D.getMinor() : model.getParagraph(paragraph).length(); + + // force selectionProperty() to be valid + getSelection(); + + return new IndexRange(start, end); + } + + public void selectRange(int anchor, int caretPosition) { + try(Guard g = suspend( + this.caretPosition, caretParagraph, + caretColumn, this.anchor, + selection, selectedText)) { + this.internalCaretPosition.setValue(clamp(0, caretPosition, model.getLength())); + this.anchor.setValue(clamp(0, anchor, model.getLength())); + this.internalSelection.setValue(IndexRange.normalize(getAnchor(), getCaretPosition())); + } + } + + /** + * Positions only the caret. Doesn't move the anchor and doesn't change + * the selection. Can be used to achieve the special case of positioning + * the caret outside or inside the selection, as opposed to always being + * at the boundary. Use with care. + */ + public void positionCaret(int pos) { + try(Guard g = suspend(caretPosition, caretParagraph, caretColumn)) { + internalCaretPosition.setValue(pos); + } + } + + protected void dispose() { + subscription.unsubscribe(); + } + + private Guard suspend(Suspendable... suspendables) { + return Suspendable.combine(modelBeingUpdated, Suspendable.combine(suspendables)).suspend(); + } + +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/NavigationActions.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/NavigationActions.java index bc75204d5..cd441a9a2 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/NavigationActions.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/NavigationActions.java @@ -176,7 +176,7 @@ default void paragraphStart(SelectionPolicy selectionPolicy) { * Moves the caret to the end of the current paragraph. */ default void paragraphEnd(SelectionPolicy selectionPolicy) { - int lineLen = getText(getCurrentParagraph()).length(); + int lineLen = getText(getCaretParagraph()).length(); int newPos = getCaretPosition() - getCaretColumn() + lineLen; moveTo(newPos, selectionPolicy); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index b74ca0919..3a2b25bf7 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -1,7 +1,5 @@ package org.fxmisc.richtext.model; -import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; -import static org.fxmisc.richtext.util.Utilities.EMPTY_RANGE; import static org.fxmisc.richtext.util.Utilities.clamp; import java.util.Optional; @@ -25,9 +23,7 @@ import org.reactfx.collection.LiveList; import org.reactfx.collection.SuspendableList; import org.reactfx.value.SuspendableVal; -import org.reactfx.value.SuspendableVar; import org.reactfx.value.Val; -import org.reactfx.value.Var; /** * Model for {@link org.fxmisc.richtext.GenericStyledArea} @@ -97,37 +93,31 @@ public class StyledTextAreaModel @Override public final int getLength() { return length.getValue(); } @Override public final ObservableValue lengthProperty() { return length; } + private final CaretSelectionModel mainCaret; + // caret position - private final Var internalCaretPosition = Var.newSimpleVar(0); - private final SuspendableVal caretPosition = internalCaretPosition.suspendable(); - @Override public final int getCaretPosition() { return caretPosition.getValue(); } - @Override public final ObservableValue caretPositionProperty() { return caretPosition; } + @Override public final int getCaretPosition() { return mainCaret.getCaretPosition(); } + @Override public final ObservableValue caretPositionProperty() { return mainCaret.caretPositionProperty(); } // selection anchor - private final SuspendableVar anchor = Var.newSimpleVar(0).suspendable(); - @Override public final int getAnchor() { return anchor.getValue(); } - @Override public final ObservableValue anchorProperty() { return anchor; } + @Override public final int getAnchor() { return mainCaret.getAnchor(); } + @Override public final ObservableValue anchorProperty() { return mainCaret.anchorProperty(); } // selection - private final Var internalSelection = Var.newSimpleVar(EMPTY_RANGE); - private final SuspendableVal selection = internalSelection.suspendable(); - @Override public final IndexRange getSelection() { return selection.getValue(); } - @Override public final ObservableValue selectionProperty() { return selection; } + @Override public final IndexRange getSelection() { return mainCaret.getSelection(); } + @Override public final ObservableValue selectionProperty() { return mainCaret.selectionProperty(); } // selected text - private final SuspendableVal selectedText; - @Override public final String getSelectedText() { return selectedText.getValue(); } - @Override public final ObservableValue selectedTextProperty() { return selectedText; } + @Override public final String getSelectedText() { return mainCaret.getSelectedText(); } + @Override public final ObservableValue selectedTextProperty() { return mainCaret.selectedTextProperty(); } - // current paragraph index - private final SuspendableVal currentParagraph; - @Override public final int getCurrentParagraph() { return currentParagraph.getValue(); } - @Override public final ObservableValue currentParagraphProperty() { return currentParagraph; } + // caret paragraph + @Override public final int getCaretParagraph() { return mainCaret.getCaretParagraph(); } + @Override public final ObservableValue caretParagraphProperty() { return mainCaret.caretParagraphProperty(); } // caret column - private final SuspendableVal caretColumn; - @Override public final int getCaretColumn() { return caretColumn.getValue(); } - @Override public final ObservableValue caretColumnProperty() { return caretColumn; } + @Override public final int getCaretColumn() { return mainCaret.getCaretColumn(); } + @Override public final ObservableValue caretColumnProperty() { return mainCaret.caretColumnProperty(); } // paragraphs private final SuspendableList> paragraphs; @@ -164,9 +154,6 @@ public class StyledTextAreaModel private Subscription subscriptions = () -> {}; - private Position selectionStart2D; - private Position selectionEnd2D; - /** * content model */ @@ -256,91 +243,18 @@ public StyledTextAreaModel( plainTextChanges = content.plainChanges().pausable(); richTextChanges = content.richChanges().pausable(); - // when content is updated by an area, update the caret - // and selection ranges of all the other - // clones that also share this document - subscribeTo(content.plainChanges(), plainTextChange -> { - int changeLength = plainTextChange.getInserted().length() - plainTextChange.getRemoved().length(); - if (changeLength != 0) { - int indexOfChange = plainTextChange.getPosition(); - // in case of a replacement: "hello there" -> "hi." - int endOfChange = indexOfChange + Math.abs(changeLength); - - // update caret - int caretPosition = getCaretPosition(); - if (indexOfChange < caretPosition) { - // if caret is within the changed content, move it to indexOfChange - // otherwise offset it by changeLength - positionCaret( - caretPosition < endOfChange - ? indexOfChange - : caretPosition + changeLength - ); - } - // update selection - int selectionStart = getSelection().getStart(); - int selectionEnd = getSelection().getEnd(); - if (selectionStart != selectionEnd) { - // if start/end is within the changed content, move it to indexOfChange - // otherwise, offset it by changeLength - // Note: if both are moved to indexOfChange, selection is empty. - if (indexOfChange < selectionStart) { - selectionStart = selectionStart < endOfChange - ? indexOfChange - : selectionStart + changeLength; - } - if (indexOfChange < selectionEnd) { - selectionEnd = selectionEnd < endOfChange - ? indexOfChange - : selectionEnd + changeLength; - } - selectRange(selectionStart, selectionEnd); - } else { - // force-update internalSelection in case caret is - // at the end of area and a character was deleted - // (prevents a StringIndexOutOfBoundsException because - // selection's end is one char farther than area's length). - int internalCaretPos = internalCaretPosition.getValue(); - selectRange(internalCaretPos, internalCaretPos); - } - } - }); + mainCaret = new CaretSelectionModel(this, () -> beingUpdated); + manageSubscription(mainCaret::dispose); undoManager = preserveStyle ? createRichUndoManager(UndoManagerFactory.unlimitedHistoryFactory()) : createPlainUndoManager(UndoManagerFactory.unlimitedHistoryFactory()); - Val caretPosition2D = Val.create( - () -> content.offsetToPosition(internalCaretPosition.getValue(), Forward), - internalCaretPosition, paragraphs); - - currentParagraph = caretPosition2D.map(Position::getMajor).suspendable(); - caretColumn = caretPosition2D.map(Position::getMinor).suspendable(); - - selectionStart2D = position(0, 0); - selectionEnd2D = position(0, 0); - internalSelection.addListener(obs -> { - IndexRange sel = internalSelection.getValue(); - selectionStart2D = offsetToPosition(sel.getStart(), Forward); - selectionEnd2D = sel.getLength() == 0 - ? selectionStart2D - : selectionStart2D.offsetBy(sel.getLength(), Backward); - }); - - selectedText = Val.create( - () -> content.getText(internalSelection.getValue()), - internalSelection, content.getParagraphs()).suspendable(); - final Suspendable omniSuspendable = Suspendable.combine( beingUpdated, // must be first, to be the last one to release text, length, - caretPosition, - anchor, - selection, - selectedText, - currentParagraph, - caretColumn, + mainCaret.omniSuspendable(), // add streams after properties, to be released before them plainTextChanges, @@ -360,6 +274,10 @@ public StyledTextAreaModel( * * * ********************************************************************** */ + final String getText(IndexRange range) { + return content.getText(range); + } + @Override public final String getText(int start, int end) { return content.getText(start, end); @@ -388,20 +306,7 @@ public StyledDocument subDocument(int paragraphIndex) { * Returns the selection range in the given paragraph. */ public IndexRange getParagraphSelection(int paragraph) { - int startPar = selectionStart2D.getMajor(); - int endPar = selectionEnd2D.getMajor(); - - if(paragraph < startPar || paragraph > endPar) { - return EMPTY_RANGE; - } - - int start = paragraph == startPar ? selectionStart2D.getMinor() : 0; - int end = paragraph == endPar ? selectionEnd2D.getMinor() : paragraphs.get(paragraph).length(); - - // force selectionProperty() to be valid - getSelection(); - - return new IndexRange(start, end); + return mainCaret.getParagraphSelection(paragraph); } /** @@ -634,14 +539,7 @@ public void replace(int start, int end, StyledDocument replacement) @Override public void selectRange(int anchor, int caretPosition) { - try(Guard g = suspend( - this.caretPosition, currentParagraph, - caretColumn, this.anchor, - selection, selectedText)) { - this.internalCaretPosition.setValue(clamp(0, caretPosition, getLength())); - this.anchor.setValue(clamp(0, anchor, getLength())); - this.internalSelection.setValue(IndexRange.normalize(getAnchor(), getCaretPosition())); - } + mainCaret.selectRange(anchor, caretPosition); } /** @@ -651,9 +549,7 @@ public void selectRange(int anchor, int caretPosition) { * at the boundary. Use with care. */ public void positionCaret(int pos) { - try(Guard g = suspend(caretPosition, currentParagraph, caretColumn)) { - internalCaretPosition.setValue(pos); - } + mainCaret.positionCaret(pos); } /* ********************************************************************** * @@ -688,10 +584,6 @@ private PS getParagraphStyleForInsertionAt(int pos) { } } - private void subscribeTo(EventStream src, Consumer consumer) { - manageSubscription(src.subscribe(consumer)); - } - private void manageSubscription(Subscription subscription) { subscriptions = subscriptions.and(subscription); } @@ -708,7 +600,4 @@ private UndoManager createRichUndoManager(UndoManagerFactory factory) { return factory.create(richChanges(), RichTextChange::invert, apply, merge); } - private Guard suspend(Suspendable... suspendables) { - return Suspendable.combine(beingUpdated, Suspendable.combine(suspendables)).suspend(); - } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java index 584107460..7627994c8 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java @@ -78,8 +78,8 @@ public interface TextEditingArea { /** * Index of the current paragraph, i.e. the paragraph with the caret. */ - int getCurrentParagraph(); - ObservableValue currentParagraphProperty(); + int getCaretParagraph(); + ObservableValue caretParagraphProperty(); /** * The caret position within the current paragraph. From 4bab903f21a3ad79f1e4c171a272cd0b497f4de1 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 10:48:17 -0800 Subject: [PATCH 03/13] Make class public --- .../java/org/fxmisc/richtext/model/CaretSelectionModel.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java index c52a11092..0a2bac6a1 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java @@ -31,7 +31,7 @@ * * */ -final class CaretSelectionModel { +public final class CaretSelectionModel { // caret position private final Var internalCaretPosition = Var.newSimpleVar(0); From 7f6b84340385d95527635faddcdfba4cce8a4f47 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 10:56:32 -0800 Subject: [PATCH 04/13] Remove Supplier: not necessary --- .../java/org/fxmisc/richtext/model/CaretSelectionModel.java | 4 ++-- .../java/org/fxmisc/richtext/model/StyledTextAreaModel.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java index 0a2bac6a1..953441caf 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java @@ -75,9 +75,9 @@ public final class CaretSelectionModel { private final Suspendable modelBeingUpdated; private final Subscription subscription; - CaretSelectionModel(StyledTextAreaModel model, Supplier beingUpdatedSupplier) { + CaretSelectionModel(StyledTextAreaModel model, Suspendable modelBeingUpdated) { this.model = model; - modelBeingUpdated = beingUpdatedSupplier.get(); + this.modelBeingUpdated = modelBeingUpdated; Val caretPosition2D = Val.create( () -> offsetToPosition(internalCaretPosition.getValue(), Forward), internalCaretPosition, model.getParagraphs() diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index 3a2b25bf7..0fb51dd6f 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -243,7 +243,7 @@ public StyledTextAreaModel( plainTextChanges = content.plainChanges().pausable(); richTextChanges = content.richChanges().pausable(); - mainCaret = new CaretSelectionModel(this, () -> beingUpdated); + mainCaret = new CaretSelectionModel(this, beingUpdated); manageSubscription(mainCaret::dispose); undoManager = preserveStyle From 8d079e8d600e183c9917f57fc6fb3ff7edcff639 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 11:06:55 -0800 Subject: [PATCH 05/13] Extract `caretDirty` into caret selection model --- .../java/org/fxmisc/richtext/GenericStyledArea.java | 6 ++++++ .../fxmisc/richtext/model/CaretSelectionModel.java | 13 +++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index f23548865..9de66b98e 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -441,6 +441,7 @@ public Optional, Codec>> getStyleCodecs() { private boolean followCaretRequested = false; private final SuspendableEventStream viewportDirty; + final EventStream viewportDirtyEvents() { return viewportDirty; } /** * model @@ -1364,6 +1365,11 @@ private void positionPopup( }); } + Optional getCaretBoundsOnScreen(int paragraphIndex) { + return virtualFlow.getCellIfVisible(paragraphIndex) + .map(c -> c.getNode().getCaretBoundsOnScreen()); + } + private Optional getCaretBoundsOnScreen() { return virtualFlow.getCellIfVisible(getCaretParagraph()) .map(c -> c.getNode().getCaretBoundsOnScreen()); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java index 953441caf..327cd1727 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java @@ -4,6 +4,7 @@ import javafx.scene.control.IndexRange; import org.fxmisc.richtext.model.TwoDimensional.Bias; import org.fxmisc.richtext.model.TwoDimensional.Position; +import org.reactfx.EventStream; import org.reactfx.Guard; import org.reactfx.Subscription; import org.reactfx.Suspendable; @@ -12,12 +13,12 @@ import org.reactfx.value.Val; import org.reactfx.value.Var; -import java.util.function.Supplier; - import static org.fxmisc.richtext.util.Utilities.EMPTY_RANGE; import static org.fxmisc.richtext.util.Utilities.clamp; import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward; import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward; +import static org.reactfx.EventStreams.merge; +import static org.reactfx.EventStreams.invalidationsOf; /** * Encapsulates all caret and selection related information for {@link StyledTextAreaModel} and utilizes @@ -68,6 +69,9 @@ public final class CaretSelectionModel { public final int getCaretColumn() { return caretColumn.getValue(); } public final ObservableValue caretColumnProperty() { return caretColumn; } + private final EventStream caretDirty; + public final EventStream caretDirtyEvents() { return caretDirty; } + private Position selectionStart2D; private Position selectionEnd2D; @@ -148,6 +152,11 @@ public final class CaretSelectionModel { } } }); + caretDirty = merge( + invalidationsOf(caretPositionProperty()), + invalidationsOf(model.getParagraphs()), + invalidationsOf(selectionProperty()) + ); } Suspendable omniSuspendable() { From 0d6fe876c9f4a617880c103667b777b2280f509c Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 11:08:24 -0800 Subject: [PATCH 06/13] Add getter to caret selection model --- .../main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index 0fb51dd6f..4108a8305 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -94,6 +94,7 @@ public class StyledTextAreaModel @Override public final ObservableValue lengthProperty() { return length; } private final CaretSelectionModel mainCaret; + public final CaretSelectionModel getMainCaret() { return mainCaret; } // caret position @Override public final int getCaretPosition() { return mainCaret.getCaretPosition(); } From da2c3455a9b71806b3e66129180ac9c2b8b2bb68 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 11:33:47 -0800 Subject: [PATCH 07/13] Add eventstream for selection dirty events --- .../java/org/fxmisc/richtext/model/CaretSelectionModel.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java index 327cd1727..c5d3fe04a 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java @@ -72,6 +72,9 @@ public final class CaretSelectionModel { private final EventStream caretDirty; public final EventStream caretDirtyEvents() { return caretDirty; } + private final EventStream selectionDirty; + public final EventStream selectionDirtyEvents() { return selectionDirty; } + private Position selectionStart2D; private Position selectionEnd2D; @@ -157,6 +160,7 @@ public final class CaretSelectionModel { invalidationsOf(model.getParagraphs()), invalidationsOf(selectionProperty()) ); + selectionDirty = invalidationsOf(selectionProperty()); } Suspendable omniSuspendable() { From 86bdda01af4022070c914bf9d2509c22e124a2ee Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 11:39:56 -0800 Subject: [PATCH 08/13] Extract caret and selection-related view into `CaretSelectionView` class. --- .../main/java/org/fxmisc/richtext/Caret.java | 33 +++++ .../fxmisc/richtext/CaretSelectionView.java | 137 ++++++++++++++++++ .../fxmisc/richtext/GenericStyledArea.java | 8 +- 3 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/Caret.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/Caret.java b/richtextfx/src/main/java/org/fxmisc/richtext/Caret.java new file mode 100644 index 000000000..9a8aee397 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/Caret.java @@ -0,0 +1,33 @@ +package org.fxmisc.richtext; + +import javafx.beans.value.ObservableValue; +import javafx.scene.control.IndexRange; + + +public interface Caret { + + int getCaretPosition(); + + ObservableValue caretPositionProperty(); + + int getAnchor(); + + ObservableValue anchorProperty(); + + IndexRange getSelection(); + + ObservableValue selectionProperty(); + + String getSelectedText(); + + ObservableValue selectedTextProperty(); + + int getCaretParagraph(); + + ObservableValue caretParagraphProperty(); + + int getCaretColumn(); + + ObservableValue caretColumnProperty(); + +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java b/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java new file mode 100644 index 000000000..dc3c3a06d --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java @@ -0,0 +1,137 @@ +package org.fxmisc.richtext; + +import javafx.beans.binding.Binding; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Bounds; +import javafx.scene.control.IndexRange; +import org.fxmisc.richtext.GenericStyledArea.CaretVisibility; +import org.fxmisc.richtext.model.CaretSelectionModel; +import org.reactfx.EventStream; +import org.reactfx.EventStreams; +import org.reactfx.StateMachine; +import org.reactfx.Subscription; +import org.reactfx.value.Val; +import org.reactfx.value.Var; + +import java.time.Duration; +import java.util.Optional; + +import static javafx.util.Duration.ZERO; + +public class CaretSelectionView implements Caret { + + private final CaretSelectionModel model; + + public final int getCaretPosition() { return model.getCaretPosition(); } + public final ObservableValue caretPositionProperty() { return model.caretPositionProperty(); } + + public final int getAnchor() { return model.getAnchor(); } + public final ObservableValue anchorProperty() { return model.anchorProperty(); } + + public final IndexRange getSelection() { return model.getSelection(); } + public final ObservableValue selectionProperty() { return model.selectionProperty(); } + + public final String getSelectedText() { return model.getSelectedText(); } + public final ObservableValue selectedTextProperty() { return model.selectedTextProperty(); } + + public final int getCaretParagraph() { return model.getCaretParagraph(); } + public final ObservableValue caretParagraphProperty() { return model.caretParagraphProperty(); } + + public final int getCaretColumn() { return model.getCaretColumn(); } + public final ObservableValue caretColumnProperty() { return model.caretColumnProperty(); } + +// private final Val caretLine; +// public final int getCaretLine() { return caretLine.getValue(); } +// public final ObservableValue caretLineProperty() { return caretLine; } + + private final Var showCaret = Var.newSimpleVar(CaretVisibility.AUTO); + public final CaretVisibility getShowCaret() { return showCaret.getValue(); } + public final void setShowCaret(CaretVisibility value) { showCaret.setValue(value); } + public final Var showCaretProperty() { return showCaret; } + + private final Binding caretVisible; + final boolean isCaretVisible() { return caretVisible.getValue(); } + final ObservableValue caretVisibleProperty() { return caretVisible; } + + /** + * The bounds of the caret in the Screen's coordinate system or {@link Optional#empty()} if caret is not visible + * in the viewport. + */ + private final Val> caretBounds; + public final Optional getCaretBounds() { return caretBounds.getValue(); } + public final ObservableValue> caretBoundsProperty() { return caretBounds; } + + private Subscription subscriptions; + + /** + * The bounds of the selection in the Screen's coordinate system if something is selected and visible in the + * viewport, {@link #caretBounds} if nothing is selected and caret is visible in the viewport, or + * {@link Optional#empty()} if selection is not visible in the viewport. + */ + private final Val> selectionBounds; + public final Optional getSelectionBounds() { return selectionBounds.getValue(); } + public final ObservableValue> selectionBoundsProperty() { return selectionBounds; } + + CaretSelectionView(CaretSelectionModel model, GenericStyledArea area) { + this.model = model; + + EventStream blinkCaret = showCaret.values() + .flatMap(mode -> { + switch (mode) { + case ON: + return Val.constant(true).values(); + case OFF: + return Val.constant(false).values(); + default: + case AUTO: + return EventStreams.valuesOf(area.focusedProperty() + .and(area.editableProperty()) + .and(area.disabledProperty().not())); + } + }); + + // The caret is visible in periodic intervals, + // but only when blinkCaret is true. + caretVisible = EventStreams.combine(blinkCaret, area.blinkRates()) + .flatMap(tuple -> { + Boolean blink = tuple.get1(); + javafx.util.Duration rate = tuple.get2(); + if(blink) { + return rate.lessThanOrEqualTo(ZERO) + ? Val.constant(true).values() + : booleanPulse(rate, model.caretDirtyEvents()); + } else { + return Val.constant(false).values(); + } + }) + .toBinding(false); + manageBinding(caretVisible); + + caretBounds = Val.create( + () -> area.getCaretBoundsOnScreen(getCaretParagraph()), + area.boundsDirtyFor(model.caretDirtyEvents()) + ); + selectionBounds = Val.create( + area::impl_bounds_getSelectionBoundsOnScreen, + area.boundsDirtyFor(model.selectionDirtyEvents()) + ); + } + + public void dispose() { + subscriptions.unsubscribe(); + } + + private void manageBinding(Binding binding) { + subscriptions = subscriptions.and(binding::dispose); + } + + private static EventStream booleanPulse(javafx.util.Duration javafxDuration, EventStream restartImpulse) { + Duration duration = Duration.ofMillis(Math.round(javafxDuration.toMillis())); + EventStream ticks = EventStreams.restartableTicks(duration, restartImpulse); + return StateMachine.init(false) + .on(restartImpulse.withDefaultEvent(null)).transition((state, impulse) -> true) + .on(ticks).transition((state, tick) -> !state) + .toStateStream(); + } + +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index 9de66b98e..4f09db68e 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -195,6 +195,8 @@ public class GenericStyledArea extends Region */ private final StyleableObjectProperty caretBlinkRate = new CssProperties.CaretBlinkRateProperty(this, javafx.util.Duration.millis(500)); + final EventStream blinkRates() { return EventStreams.valuesOf(caretBlinkRate); } + // editable property /** @@ -1384,7 +1386,7 @@ private Optional impl_popup_getSelectionBoundsOnScreen() { return impl_getSelectionBoundsOnScreen(); } - private Optional impl_bounds_getSelectionBoundsOnScreen() { + Optional impl_bounds_getSelectionBoundsOnScreen() { IndexRange selection = getSelection(); if (selection.getLength() == 0) { return Optional.empty(); @@ -1450,6 +1452,10 @@ private static EventStream booleanPulse(javafx.util.Duration javafxDura .toStateStream(); } + EventStream boundsDirtyFor(EventStream caretOrSelectionDirtyEvents) { + return merge(viewportDirty, caretOrSelectionDirtyEvents).suppressWhen(model.beingUpdatedProperty()); + } + /* ********************************************************************** * * * * Deprecated Popup API (Originally a part of "Properties" section * From d432ac5708f48f9fa5f31cb66e19d6958c5220bf Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 11:54:41 -0800 Subject: [PATCH 09/13] Move javadoc to where it should reside --- .../java/org/fxmisc/richtext/model/CaretSelectionModel.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java index c5d3fe04a..4191e743b 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/CaretSelectionModel.java @@ -155,9 +155,12 @@ public final class CaretSelectionModel { } } }); + caretDirty = merge( + // follow the caret every time the caret position or paragraphs change invalidationsOf(caretPositionProperty()), invalidationsOf(model.getParagraphs()), + // need to reposition popup even when caret hasn't moved, but selection has changed (been deselected) invalidationsOf(selectionProperty()) ); selectionDirty = invalidationsOf(selectionProperty()); From 5dde6875c3bdb0895bc0eb4f8739e0dbf5bd3b3c Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 11:56:06 -0800 Subject: [PATCH 10/13] Make base area use CaretSelectionView --- .../fxmisc/richtext/CaretSelectionView.java | 3 +- .../fxmisc/richtext/GenericStyledArea.java | 81 +++---------------- 2 files changed, 14 insertions(+), 70 deletions(-) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java b/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java index dc3c3a06d..f029a5312 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java @@ -75,6 +75,7 @@ public class CaretSelectionView implements Caret { CaretSelectionView(CaretSelectionModel model, GenericStyledArea area) { this.model = model; + // whether or not to display the caret EventStream blinkCaret = showCaret.values() .flatMap(mode -> { switch (mode) { @@ -92,7 +93,7 @@ public class CaretSelectionView implements Caret { // The caret is visible in periodic intervals, // but only when blinkCaret is true. - caretVisible = EventStreams.combine(blinkCaret, area.blinkRates()) + caretVisible = EventStreams.combine(blinkCaret, area.caretBlinkRateEvents()) .flatMap(tuple -> { Boolean blink = tuple.get1(); javafx.util.Duration rate = tuple.get2(); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index 4f09db68e..537923904 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -195,7 +195,7 @@ public class GenericStyledArea extends Region */ private final StyleableObjectProperty caretBlinkRate = new CssProperties.CaretBlinkRateProperty(this, javafx.util.Duration.millis(500)); - final EventStream blinkRates() { return EventStreams.valuesOf(caretBlinkRate); } + final EventStream caretBlinkRateEvents() { return EventStreams.valuesOf(caretBlinkRate); } // editable property @@ -342,13 +342,8 @@ public Optional, Codec>> getStyleCodecs() { @Override public final ObservableValue caretPositionProperty() { return model.caretPositionProperty(); } // caret bounds - /** - * The bounds of the caret in the Screen's coordinate system or {@link Optional#empty()} if caret is not visible - * in the viewport. - */ - private final Val> caretBounds; - public final Optional getCaretBounds() { return caretBounds.getValue(); } - public final ObservableValue> caretBoundsProperty() { return caretBounds; } + public final Optional getCaretBounds() { return mainCaret.getCaretBounds(); } + public final ObservableValue> caretBoundsProperty() { return mainCaret.caretBoundsProperty(); } // selection anchor @Override public final int getAnchor() { return model.getAnchor(); } @@ -363,14 +358,8 @@ public Optional, Codec>> getStyleCodecs() { @Override public final ObservableValue selectedTextProperty() { return model.selectedTextProperty(); } // selection bounds - /** - * The bounds of the selection in the Screen's coordinate system if something is selected and visible in the - * viewport, {@link #caretBounds} if nothing is selected and caret is visible in the viewport, or - * {@link Optional#empty()} if selection is not visible in the viewport. - */ - private final Val> selectionBounds; - public final Optional getSelectionBounds() { return selectionBounds.getValue(); } - public final ObservableValue> selectionBoundsProperty() { return selectionBounds; } + public final Optional getSelectionBounds() { return mainCaret.getSelectionBounds(); } + public final ObservableValue> selectionBoundsProperty() { return mainCaret.selectionBoundsProperty(); } // current paragraph index @Override public final int getCaretParagraph() { return model.getCaretParagraph(); } @@ -419,6 +408,9 @@ public Optional, Codec>> getStyleCodecs() { // rich text changes @Override public final EventStream> richChanges() { return model.richChanges(); } + private final CaretSelectionView mainCaret; + public final Caret getMainCaret() { return mainCaret; } + /* ********************************************************************** * * * * Private fields * @@ -430,8 +422,6 @@ public Optional, Codec>> getStyleCodecs() { // Remembers horizontal position when traversing up / down. private Optional targetCaretOffset = Optional.empty(); - private final Binding caretVisible; - private final Val> _popupAnchorAdjustment; private final VirtualFlow, Cell, ParagraphBox>> virtualFlow; @@ -598,49 +588,6 @@ public GenericStyledArea( EventStream popupDirty = merge(popupAlignmentDirty, popupAnchorAdjustmentDirty, popupAnchorOffsetDirty); subscribeTo(popupDirty, x -> layoutPopup()); - // follow the caret every time the caret position or paragraphs change - EventStream caretPosDirty = invalidationsOf(caretPositionProperty()); - EventStream paragraphsDirty = invalidationsOf(getParagraphs()); - EventStream selectionDirty = invalidationsOf(selectionProperty()); - // need to reposition popup even when caret hasn't moved, but selection has changed (been deselected) - EventStream caretDirty = merge(caretPosDirty, paragraphsDirty, selectionDirty); - - // whether or not to display the caret - EventStream blinkCaret = EventStreams.valuesOf(showCaretProperty()) - .flatMap(mode -> { - switch (mode) { - case ON: - return EventStreams.valuesOf(Val.constant(true)); - case OFF: - return EventStreams.valuesOf(Val.constant(false)); - default: - case AUTO: - return EventStreams.valuesOf(focusedProperty() - .and(editableProperty()) - .and(disabledProperty().not())); - } - }); - - // the rate at which to display the caret - EventStream blinkRate = EventStreams.valuesOf(caretBlinkRate); - - // The caret is visible in periodic intervals, - // but only when blinkCaret is true. - caretVisible = EventStreams.combine(blinkCaret, blinkRate) - .flatMap(tuple -> { - Boolean blink = tuple.get1(); - javafx.util.Duration rate = tuple.get2(); - if(blink) { - return rate.lessThanOrEqualTo(ZERO) - ? EventStreams.valuesOf(Val.constant(true)) - : booleanPulse(rate, caretDirty); - } else { - return EventStreams.valuesOf(Val.constant(false)); - } - }) - .toBinding(false); - manageBinding(caretVisible); - viewportDirty = merge( // no need to check for width & height invalidations as scroll values update when these do @@ -652,14 +599,10 @@ public GenericStyledArea( invalidationsOf(estimatedScrollXProperty()), invalidationsOf(estimatedScrollYProperty()) ).suppressible(); - EventStream caretBoundsDirty = merge(viewportDirty, caretDirty) - .suppressWhen(model.beingUpdatedProperty()); - EventStream selectionBoundsDirty = merge(viewportDirty, invalidationsOf(selectionProperty())) - .suppressWhen(model.beingUpdatedProperty()); - // updates the bounds of the caret/selection - caretBounds = Val.create(this::getCaretBoundsOnScreen, caretBoundsDirty); - selectionBounds = Val.create(this::impl_bounds_getSelectionBoundsOnScreen, selectionBoundsDirty); + // initialize viewportDirty before mainCaret as constructor uses it in "boundsDirtyFor(EventStream)" + mainCaret = new CaretSelectionView(model.getMainCaret(), this); + manageSubscription(mainCaret::dispose); // Adjust popup anchor by either a user-provided function, // or user-provided offset, or don't adjust at all. @@ -1263,7 +1206,7 @@ private Cell, ParagraphBox> createCell( ).subscribe(in -> in.exec((i, n) -> box.pseudoClassStateChanged(LAST_PAR, i == n-1))); // caret is visible only in the paragraph with the caret - Val cellCaretVisible = hasCaret.flatMap(x -> x ? caretVisible : Val.constant(false)); + Val cellCaretVisible = hasCaret.flatMap(x -> x ? mainCaret.caretVisibleProperty() : Val.constant(false)); box.caretVisibleProperty().bind(cellCaretVisible); // bind cell's caret position to area's caret column, From 4900eababaa9c000f7719eb602ecd6f20412c251 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 11:56:38 -0800 Subject: [PATCH 11/13] Minor fix: update to use Utilities Empty_Range --- .../src/main/java/org/fxmisc/richtext/ParagraphText.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java index 6ecf7ab8f..ed14ae4e9 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java @@ -22,6 +22,8 @@ import org.reactfx.value.Val; import org.reactfx.value.Var; +import static org.fxmisc.richtext.util.Utilities.EMPTY_RANGE; + class ParagraphText extends TextFlowExt { // FIXME: changing it currently has not effect, because @@ -37,7 +39,7 @@ public ObjectProperty highlightTextFillProperty() { public void setCaretPosition(int pos) { caretPosition.setValue(pos); } private final Val clampedCaretPosition; - private final ObjectProperty selection = new SimpleObjectProperty<>(StyledTextArea.EMPTY_RANGE); + private final ObjectProperty selection = new SimpleObjectProperty<>(EMPTY_RANGE); public ObjectProperty selectionProperty() { return selection; } public void setSelection(IndexRange sel) { selection.set(sel); } From 0ab87f648ae80ad25d980d9ab0a4c7fb11a07bb0 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 12:05:14 -0800 Subject: [PATCH 12/13] Implement Selection interface --- .../fxmisc/richtext/CaretSelectionView.java | 2 +- .../java/org/fxmisc/richtext/Selection.java | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/Selection.java diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java b/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java index f029a5312..1ff610081 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/CaretSelectionView.java @@ -18,7 +18,7 @@ import static javafx.util.Duration.ZERO; -public class CaretSelectionView implements Caret { +public class CaretSelectionView implements Caret, Selection { private final CaretSelectionModel model; diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/Selection.java b/richtextfx/src/main/java/org/fxmisc/richtext/Selection.java new file mode 100644 index 000000000..031ad0631 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/Selection.java @@ -0,0 +1,23 @@ +package org.fxmisc.richtext; + +import javafx.beans.value.ObservableValue; +import javafx.geometry.Bounds; +import javafx.scene.control.IndexRange; + +import java.util.Optional; + +public interface Selection { + + IndexRange getSelection(); + + ObservableValue selectionProperty(); + + String getSelectedText(); + + ObservableValue selectedTextProperty(); + + Optional getSelectionBounds(); + + ObservableValue> selectionBoundsProperty(); + +} From 9b340c3fecba62cf1c17666974d9fbdf62ae8fa6 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 18 Jan 2017 12:06:04 -0800 Subject: [PATCH 13/13] Add getter to selection --- .../src/main/java/org/fxmisc/richtext/GenericStyledArea.java | 1 + 1 file changed, 1 insertion(+) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index 537923904..b0a1a5fe5 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -410,6 +410,7 @@ public Optional, Codec>> getStyleCodecs() { private final CaretSelectionView mainCaret; public final Caret getMainCaret() { return mainCaret; } + public final Selection getMainSelection() { return mainCaret; } /* ********************************************************************** * * *