
org.fxmisc.richtext.GenericStyledArea Maven / Gradle / Ivy
Show all versions of richtextfx Show documentation
package org.fxmisc.richtext;
import static javafx.util.Duration.*;
import static org.fxmisc.richtext.PopupAlignment.*;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Backward;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
import static org.reactfx.EventStreams.*;
import static org.reactfx.util.Tuples.*;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.IntSupplier;
import java.util.function.IntUnaryOperator;
import java.util.function.UnaryOperator;
import java.util.stream.Stream;
import javafx.beans.NamedArg;
import javafx.beans.binding.Binding;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.event.Event;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.IndexRange;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Region;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.text.TextFlow;
import javafx.stage.PopupWindow;
import org.fxmisc.flowless.Cell;
import org.fxmisc.flowless.VirtualFlow;
import org.fxmisc.flowless.VirtualFlowHit;
import org.fxmisc.flowless.Virtualized;
import org.fxmisc.flowless.VirtualizedScrollPane;
import org.fxmisc.richtext.CssProperties.EditableProperty;
import org.fxmisc.richtext.model.Codec;
import org.fxmisc.richtext.model.EditActions;
import org.fxmisc.richtext.model.EditableStyledDocument;
import org.fxmisc.richtext.model.GenericEditableStyledDocument;
import org.fxmisc.richtext.model.Paragraph;
import org.fxmisc.richtext.model.ReadOnlyStyledDocument;
import org.fxmisc.richtext.model.StyleActions;
import org.fxmisc.richtext.model.NavigationActions;
import org.fxmisc.richtext.model.PlainTextChange;
import org.fxmisc.richtext.model.RichTextChange;
import org.fxmisc.richtext.model.SegmentOps;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyledDocument;
import org.fxmisc.richtext.model.TextChange;
import org.fxmisc.richtext.model.TextEditingArea;
import org.fxmisc.richtext.model.TextOps;
import org.fxmisc.richtext.model.TwoDimensional;
import org.fxmisc.richtext.model.TwoLevelNavigator;
import org.fxmisc.richtext.model.UndoActions;
import org.fxmisc.undo.UndoManager;
import org.fxmisc.undo.UndoManagerFactory;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.Guard;
import org.reactfx.StateMachine;
import org.reactfx.Subscription;
import org.reactfx.Suspendable;
import org.reactfx.SuspendableEventStream;
import org.reactfx.SuspendableNo;
import org.reactfx.collection.LiveList;
import org.reactfx.util.Tuple2;
import org.reactfx.value.SuspendableVal;
import org.reactfx.value.SuspendableVar;
import org.reactfx.value.Val;
import org.reactfx.value.Var;
/**
* Text editing control. Accepts user input (keyboard, mouse) and
* provides API to assign style to text ranges. It is suitable for
* syntax highlighting and rich-text editors.
*
* Adding Scrollbars to the Area
*
* The scroll bars no longer appear when the content spans outside
* of the viewport. To add scroll bars, the area needs to be wrapped in
* a {@link VirtualizedScrollPane}. For example,
*
* {@code
* // shows area without scroll bars
* InlineCssTextArea area = new InlineCssTextArea();
*
* // add scroll bars that will display as needed
* VirtualizedScrollPane vsPane = new VirtualizedScrollPane(area);
*
* Parent parent = //;
* parent.getChildren().add(vsPane)
* }
*
*
* Auto-Scrolling to the Caret
*
* Every time the underlying {@link EditableStyledDocument} changes via user interaction (e.g. typing) through
* the {@code StyledTextArea}, the area will scroll to insure the caret is kept in view. However, this does not
* occur if changes are done programmatically. For example, let's say the area is displaying the bottom part
* of the area's {@link EditableStyledDocument} and some code changes something in the top part of the document
* that is not currently visible. If there is no call to {@link #requestFollowCaret()} at the end of that code,
* the area will not auto-scroll to that section of the document. The change will occur, and the user will continue
* to see the bottom part of the document as before. If such a call is there, then the area will scroll
* to the top of the document and no longer display the bottom part of it.
*
* Additionally, when overriding the default user-interaction behavior, remember to include a call
* to {@link #requestFollowCaret()}.
*
* Overriding default keyboard behavior
*
* {@code StyledTextArea} uses {@code KEY_TYPED} handler to handle ordinary
* character input and {@code KEY_PRESSED} handler to handle control key
* combinations (including Enter and Tab). To add or override some keyboard
* shortcuts, while keeping the rest in place, you would combine the default
* event handler with a new one that adds or overrides some of the default
* key combinations. This is how to bind {@code Ctrl+S} to the {@code save()}
* operation:
*
* {@code
* import static javafx.scene.input.KeyCode.*;
* import static javafx.scene.input.KeyCombination.*;
* import static org.fxmisc.wellbehaved.event.EventPattern.*;
* import static org.fxmisc.wellbehaved.event.InputMap.*;
*
* import org.fxmisc.wellbehaved.event.Nodes;
*
* Nodes.addInputMap(area, consume(keyPressed(S, CONTROL_DOWN), event -> save()));
* }
*
*
* Overriding default mouse behavior
*
* The area's default mouse behavior properly handles auto-scrolling and dragging the selected text to a new location.
* As such, some parts cannot be partially overridden without it affecting other behavior.
*
* The following lists either {@link org.fxmisc.wellbehaved.event.EventPattern}s that cannot be overridden without
* negatively affecting the default mouse behavior or describe how to safely override things in a special way without
* disrupting the auto scroll behavior.
*
* -
* First (1 click count) Primary Button Mouse Pressed Events:
* (
EventPattern.mousePressed(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 1)
).
* Do not override. Instead, use {@link #onOutsideSelectionMousePress},
* {@link #onInsideSelectionMousePressRelease}, or see next item.
*
* - (
* All Other Mouse Pressed Events (e.g., Primary with 2+ click count):
* Aside from hiding the context menu if it is showing (use {@link #hideContextMenu()} some((where in your
* overriding InputMap to maintain this behavior), these can be safely overridden via any of the
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate InputMapTemplate's factory methods} or
* {@link org.fxmisc.wellbehaved.event.InputMap InputMap's factory methods}.
*
* -
* Primary-Button-only Mouse Drag Detection Events:
* (
EventPattern.eventType(MouseEvent.DRAG_DETECTED).onlyIf(e -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !e.isSecondaryButtonDown())
).
* Do not override. Instead, use {@link #onNewSelectionDrag} or {@link #onSelectionDrag}.
*
* -
* Primary-Button-only Mouse Drag Events:
* (
EventPattern.mouseDragged().onlyIf(e -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !e.isSecondaryButtonDown())
)
* Do not override, but see next item.
*
* -
* All Other Mouse Drag Events:
* You may safely override other Mouse Drag Events using different
* {@link org.fxmisc.wellbehaved.event.EventPattern}s without affecting default behavior only if
* process InputMaps (
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate#process(javafx.event.EventType, BiFunction)},
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate#process(org.fxmisc.wellbehaved.event.EventPattern, BiFunction)},
* {@link org.fxmisc.wellbehaved.event.InputMap#process(javafx.event.EventType, Function)}, or
* {@link org.fxmisc.wellbehaved.event.InputMap#process(org.fxmisc.wellbehaved.event.EventPattern, Function)}
* ) are used and {@link org.fxmisc.wellbehaved.event.InputHandler.Result#PROCEED} is returned.
* The area has a "catch all" Mouse Drag InputMap that will auto scroll towards the mouse drag event when it
* occurs outside the bounds of the area and will stop auto scrolling when the mouse event occurs within the
* area. However, this only works if the event is not consumed before the event reaches that InputMap.
* To insure the auto scroll feature is enabled, set {@link #isAutoScrollOnDragDesired()} to true in your
* process InputMap. If the feature is not desired for that specific drag event, set it to false in the
* process InputMap.
* Note: Due to this "catch-all" nature, all Mouse Drag Events are consumed.
*
* -
* Primary-Button-only Mouse Released Events:
* (
EventPattern.mouseReleased().onlyIf(e -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !e.isSecondaryButtonDown())
).
* Do not override. Instead, use {@link #onNewSelectionDragEnd}, {@link #onSelectionDrop}, or see next item.
*
* -
* All other Mouse Released Events:
* You may override other Mouse Released Events using different
* {@link org.fxmisc.wellbehaved.event.EventPattern}s without affecting default behavior only if
* process InputMaps (
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate#process(javafx.event.EventType, BiFunction)},
* {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate#process(org.fxmisc.wellbehaved.event.EventPattern, BiFunction)},
* {@link org.fxmisc.wellbehaved.event.InputMap#process(javafx.event.EventType, Function)}, or
* {@link org.fxmisc.wellbehaved.event.InputMap#process(org.fxmisc.wellbehaved.event.EventPattern, Function)}
* ) are used and {@link org.fxmisc.wellbehaved.event.InputHandler.Result#PROCEED} is returned.
* The area has a "catch-all" InputMap that will consume all mouse released events and stop auto scroll if it
* was scrolling. However, this only works if the event is not consumed before the event reaches that InputMap.
* Note: Due to this "catch-all" nature, all Mouse Released Events are consumed.
*
*
*
*
* @param type of style that can be applied to paragraphs (e.g. {@link TextFlow}.
* @param type of segment used in {@link Paragraph}. Can be only text (plain or styled) or
* a type that combines text and other {@link Node}s.
* @param type of style that can be applied to a segment.
*/
public class GenericStyledArea extends Region
implements
TextEditingArea,
EditActions,
ClipboardActions,
NavigationActions,
StyleActions,
UndoActions,
ViewActions,
TwoDimensional,
Virtualized {
/**
* 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;
}
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");
/* ********************************************************************** *
* *
* Properties *
* *
* Properties affect behavior and/or appearance of this control. *
* *
* They are readable and writable by the client code and never change by *
* other means, i.e. they contain either the default value or the value *
* set by the client code. *
* *
* ********************************************************************** */
/**
* Background fill for highlighted text.
*/
private final StyleableObjectProperty highlightFill
= new CssProperties.HighlightFillProperty(this, Color.DODGERBLUE);
/**
* Text color for highlighted text.
*/
private final StyleableObjectProperty highlightTextFill
= new CssProperties.HighlightTextFillProperty(this, Color.WHITE);
/**
* Controls the blink rate of the caret, when one is displayed. Setting
* the duration to zero disables blinking.
*/
private final StyleableObjectProperty caretBlinkRate
= new CssProperties.CaretBlinkRateProperty(this, javafx.util.Duration.millis(500));
// editable property
private final BooleanProperty editable = new EditableProperty<>(this);
@Override public final boolean isEditable() { return editable.get(); }
@Override public final void setEditable(boolean value) { editable.set(value); }
@Override public final BooleanProperty editableProperty() { return editable; }
// wrapText property
private final BooleanProperty wrapText = new SimpleBooleanProperty(this, "wrapText");
@Override public final boolean isWrapText() { return wrapText.get(); }
@Override public final void setWrapText(boolean value) { wrapText.set(value); }
@Override public final BooleanProperty wrapTextProperty() { return wrapText; }
// showCaret property
private final Var showCaret = Var.newSimpleVar(CaretVisibility.AUTO);
@Override public final CaretVisibility getShowCaret() { return showCaret.getValue(); }
@Override public final void setShowCaret(CaretVisibility value) { showCaret.setValue(value); }
@Override public final Var showCaretProperty() { return showCaret; }
// undo manager
private UndoManager undoManager;
@Override public UndoManager getUndoManager() { return undoManager; }
@Override public void setUndoManager(UndoManagerFactory undoManagerFactory) {
undoManager.close();
undoManager = preserveStyle
? createRichUndoManager(undoManagerFactory)
: createPlainUndoManager(undoManagerFactory);
}
private final ObjectProperty mouseOverTextDelay = new SimpleObjectProperty<>(null);
@Override public void setMouseOverTextDelay(Duration delay) { mouseOverTextDelay.set(delay); }
@Override public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); }
@Override public ObjectProperty mouseOverTextDelayProperty() { return mouseOverTextDelay; }
private final BooleanProperty autoScrollOnDragDesired = new SimpleBooleanProperty(true);
public final void setAutoScrollOnDragDesired(boolean val) { autoScrollOnDragDesired.set(val); }
public final boolean isAutoScrollOnDragDesired() { return autoScrollOnDragDesired.get(); }
private final Property> onOutsideSelectionMousePress = new SimpleObjectProperty<>(e -> {
CharacterHit hit = hit(e.getX(), e.getY());
moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR);
});
public final void setOnOutsideSelectionMousePress(Consumer consumer) { onOutsideSelectionMousePress.setValue(consumer); }
public final Consumer getOnOutsideSelectionMousePress() { return onOutsideSelectionMousePress.getValue(); }
private final Property> onInsideSelectionMousePressRelease = new SimpleObjectProperty<>(e -> {
CharacterHit hit = hit(e.getX(), e.getY());
moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR);
});
public final void setOnInsideSelectionMousePressRelease(Consumer consumer) { onInsideSelectionMousePressRelease.setValue(consumer); }
public final Consumer getOnInsideSelectionMousePressRelease() { return onInsideSelectionMousePressRelease.getValue(); }
private final Property> onNewSelectionDrag = new SimpleObjectProperty<>(p -> {
CharacterHit hit = hit(p.getX(), p.getY());
moveTo(hit.getInsertionIndex(), SelectionPolicy.ADJUST);
});
public final void setOnNewSelectionDrag(Consumer consumer) { onNewSelectionDrag.setValue(consumer); }
public final Consumer getOnNewSelectionDrag() { return onNewSelectionDrag.getValue(); }
private final Property> onNewSelectionDragEnd = new SimpleObjectProperty<>(e -> {
CharacterHit hit = hit(e.getX(), e.getY());
moveTo(hit.getInsertionIndex(), SelectionPolicy.ADJUST);
});
public final void setOnNewSelectionDragEnd(Consumer consumer) { onNewSelectionDragEnd.setValue(consumer); }
public final Consumer getOnNewSelectionDragEnd() { return onNewSelectionDragEnd.getValue(); }
private final Property> onSelectionDrag = new SimpleObjectProperty<>(p -> {
CharacterHit hit = hit(p.getX(), p.getY());
displaceCaret(hit.getInsertionIndex());
});
public final void setOnSelectionDrag(Consumer consumer) { onSelectionDrag.setValue(consumer); }
public final Consumer getOnSelectionDrag() { return onSelectionDrag.getValue(); }
private final Property> onSelectionDrop = new SimpleObjectProperty<>(e -> {
CharacterHit hit = hit(e.getX(), e.getY());
moveSelectedText(hit.getInsertionIndex());
});
@Override public final void setOnSelectionDrop(Consumer consumer) { onSelectionDrop.setValue(consumer); }
@Override public final Consumer getOnSelectionDrop() { return onSelectionDrop.getValue(); }
private final ObjectProperty> paragraphGraphicFactory = new SimpleObjectProperty<>(null);
@Override
public void setParagraphGraphicFactory(IntFunction extends Node> factory) { paragraphGraphicFactory.set(factory); }
@Override
public IntFunction extends Node> getParagraphGraphicFactory() { return paragraphGraphicFactory.get(); }
@Override
public ObjectProperty> paragraphGraphicFactoryProperty() { return paragraphGraphicFactory; }
private ObjectProperty contextMenu = new SimpleObjectProperty<>(null);
@Override public final ContextMenu getContextMenu() { return contextMenu.get(); }
@Override public final void setContextMenu(ContextMenu menu) { contextMenu.setValue(menu); }
@Override public final ObjectProperty contextMenuObjectProperty() { return contextMenu; }
protected final boolean isContextMenuPresent() { return contextMenu.get() != null; }
private double contextMenuXOffset = 2;
@Override public final double getContextMenuXOffset() { return contextMenuXOffset; }
@Override public final void setContextMenuXOffset(double offset) { contextMenuXOffset = offset; }
private double contextMenuYOffset = 2;
@Override public final double getContextMenuYOffset() { return contextMenuYOffset; }
@Override public final void setContextMenuYOffset(double offset) { contextMenuYOffset = offset; }
private final BooleanProperty useInitialStyleForInsertion = new SimpleBooleanProperty();
@Override
public BooleanProperty useInitialStyleForInsertionProperty() { return useInitialStyleForInsertion; }
@Override
public void setUseInitialStyleForInsertion(boolean value) { useInitialStyleForInsertion.set(value); }
@Override
public boolean getUseInitialStyleForInsertion() { return useInitialStyleForInsertion.get(); }
private Optional, Codec>> styleCodecs = Optional.empty();
@Override
public void setStyleCodecs(Codec paragraphStyleCodec, Codec textStyleCodec) {
styleCodecs = Optional.of(t(paragraphStyleCodec, textStyleCodec));
}
@Override
public Optional, Codec>> getStyleCodecs() {
return styleCodecs;
}
@Override
public Var estimatedScrollXProperty() { return virtualFlow.estimatedScrollXProperty(); }
@Override
public double getEstimatedScrollX() { return virtualFlow.estimatedScrollXProperty().getValue(); }
@Override
public void setEstimatedScrollX(double value) { virtualFlow.estimatedScrollXProperty().setValue(value); }
@Override
public Var estimatedScrollYProperty() { return virtualFlow.estimatedScrollYProperty(); }
@Override
public double getEstimatedScrollY() { return virtualFlow.estimatedScrollYProperty().getValue(); }
@Override
public void setEstimatedScrollY(double value) { virtualFlow.estimatedScrollYProperty().setValue(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. *
* *
* ********************************************************************** */
// text
@Override public final String getText() { return content.getText(); }
@Override public final ObservableValue textProperty() { return content.textProperty(); }
// rich text
@Override public final StyledDocument getDocument() { return content; }
// length
@Override public final int getLength() { return content.getLength(); }
@Override public final ObservableValue lengthProperty() { return content.lengthProperty(); }
// 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; }
// caret bounds
private final Val> caretBounds;
@Override public final Optional getCaretBounds() { return caretBounds.getValue(); }
@Override public final ObservableValue> caretBoundsProperty() { return caretBounds; }
// 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; }
// 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; }
// selected text
private final SuspendableVal selectedText;
@Override public final String getSelectedText() { return selectedText.getValue(); }
@Override public final ObservableValue selectedTextProperty() { return selectedText; }
// selection bounds
private final Val> selectionBounds;
@Override public final Optional getSelectionBounds() { return selectionBounds.getValue(); }
@Override public final ObservableValue> selectionBoundsProperty() { return selectionBounds; }
// current paragraph index
private final SuspendableVal currentParagraph;
@Override public final int getCurrentParagraph() { return currentParagraph.getValue(); }
@Override public final ObservableValue currentParagraphProperty() { return currentParagraph; }
// caret column
private final SuspendableVal caretColumn;
@Override public final int getCaretColumn() { return caretColumn.getValue(); }
@Override public final ObservableValue caretColumnProperty() { return caretColumn; }
// paragraphs
@Override public LiveList> getParagraphs() { return content.getParagraphs(); }
// beingUpdated
private final SuspendableNo beingUpdated = new SuspendableNo();
public ObservableBooleanValue beingUpdatedProperty() { return beingUpdated; }
public boolean isBeingUpdated() { return beingUpdated.get(); }
// total width estimate
@Override
public Val totalWidthEstimateProperty() { return virtualFlow.totalWidthEstimateProperty(); }
@Override
public double getTotalWidthEstimate() { return virtualFlow.totalWidthEstimateProperty().getValue(); }
// total height estimate
@Override
public Val totalHeightEstimateProperty() { return virtualFlow.totalHeightEstimateProperty(); }
@Override
public double getTotalHeightEstimate() { return virtualFlow.totalHeightEstimateProperty().getValue(); }
/* ********************************************************************** *
* *
* Event streams *
* *
* ********************************************************************** */
// text changes
@Override public final EventStream plainTextChanges() { return content.plainChanges(); }
// rich text changes
@Override public final EventStream> richChanges() { return content.richChanges(); }
/* ********************************************************************** *
* *
* Private fields *
* *
* ********************************************************************** */
private Position selectionStart2D;
private Position selectionEnd2D;
private Subscription subscriptions = () -> {};
// 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;
// used for two-level navigation, where on the higher level are
// paragraphs and on the lower level are lines within a paragraph
private final TwoLevelNavigator navigator;
private boolean followCaretRequested = false;
private final SuspendableEventStream> viewportDirty;
/* ********************************************************************** *
* *
* Fields necessary for Cloning *
* *
* ********************************************************************** */
private final EditableStyledDocument content;
/**
* The underlying document that can be displayed by multiple {@code StyledTextArea}s.
*/
public final EditableStyledDocument getContent() { return content; }
private final S initialTextStyle;
@Override public final S getInitialTextStyle() { return initialTextStyle; }
private final PS initialParagraphStyle;
@Override public final PS getInitialParagraphStyle() { return initialParagraphStyle; }
private final BiConsumer applyParagraphStyle;
@Override
public final BiConsumer getApplyParagraphStyle() { return applyParagraphStyle; }
// TODO: Currently, only undo/redo respect this flag.
private final boolean preserveStyle;
@Override public final boolean isPreserveStyle() { return preserveStyle; }
/* ********************************************************************** *
* *
* Miscellaneous *
* *
* ********************************************************************** */
private final TextOps segmentOps;
@Override public final SegmentOps getSegOps() { return segmentOps; }
/* ********************************************************************** *
* *
* Constructors *
* *
* ********************************************************************** */
/**
* Creates a text area with empty text content.
*
* @param initialParagraphStyle style to use in places where no other style is
* specified (yet).
* @param applyParagraphStyle function that, given a {@link TextFlow} node and
* a style, applies the style to the paragraph node. This function is
* used by the default skin to apply style to paragraph nodes.
* @param initialTextStyle style to use in places where no other style is
* specified (yet).
* @param segmentOps The operations which are defined on the text segment objects.
* @param nodeFactory A function which is used to create the JavaFX scene nodes for a
* particular segment.
*/
public GenericStyledArea(@NamedArg("initialParagraphStyle") PS initialParagraphStyle,
@NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle,
@NamedArg("initialTextStyle") S initialTextStyle,
@NamedArg("segmentOps") TextOps segmentOps,
@NamedArg("nodeFactory") Function nodeFactory) {
this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, true, nodeFactory);
}
public GenericStyledArea(@NamedArg("initialParagraphStyle") PS initialParagraphStyle,
@NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle,
@NamedArg("initialTextStyle") S initialTextStyle,
@NamedArg("segmentOps") TextOps segmentOps,
@NamedArg("preserveStyle") boolean preserveStyle,
@NamedArg("nodeFactory") Function nodeFactory) {
this(initialParagraphStyle, applyParagraphStyle, initialTextStyle,
new GenericEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), segmentOps, preserveStyle, nodeFactory);
}
/**
* The same as {@link #GenericStyledArea(Object, BiConsumer, Object, TextOps, Function)} except that
* this constructor can be used to create another {@code GenericStyledArea} object that
* shares the same {@link EditableStyledDocument}.
*/
public GenericStyledArea(
@NamedArg("initialParagraphStyle") PS initialParagraphStyle,
@NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle,
@NamedArg("initialTextStyle") S initialTextStyle,
@NamedArg("document") EditableStyledDocument document,
@NamedArg("segmentOps") TextOps segmentOps,
@NamedArg("nodeFactory") Function nodeFactory) {
this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, document, segmentOps, true, nodeFactory);
}
public GenericStyledArea(
@NamedArg("initialParagraphStyle") PS initialParagraphStyle,
@NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle,
@NamedArg("initialTextStyle") S initialTextStyle,
@NamedArg("document") EditableStyledDocument document,
@NamedArg("segmentOps") TextOps segmentOps,
@NamedArg("preserveStyle") boolean preserveStyle,
@NamedArg("nodeFactory") Function nodeFactory) {
this.initialTextStyle = initialTextStyle;
this.initialParagraphStyle = initialParagraphStyle;
this.preserveStyle = preserveStyle;
this.content = document;
this.applyParagraphStyle = applyParagraphStyle;
this.segmentOps = segmentOps;
undoManager = preserveStyle
? createRichUndoManager(UndoManagerFactory.unlimitedHistoryFactory())
: createPlainUndoManager(UndoManagerFactory.unlimitedHistoryFactory());
Val caretPosition2D = Val.create(
() -> content.offsetToPosition(internalCaretPosition.getValue(), Forward),
internalCaretPosition, getParagraphs());
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
caretPosition,
anchor,
selection,
selectedText,
currentParagraph,
caretColumn);
manageSubscription(omniSuspendable.suspendWhen(content.beingUpdatedProperty()));
// when content is updated by an area, update the caret
// and selection ranges of all the other
// clones that also share this document
subscribeTo(plainTextChanges(), 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
displaceCaret(
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);
}
}
});
// allow tab traversal into area
setFocusTraversable(true);
this.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY)));
getStyleClass().add("styled-text-area");
getStylesheets().add(StyledTextArea.class.getResource("styled-text-area.css").toExternalForm());
// keeps track of currently used non-empty cells
@SuppressWarnings("unchecked")
ObservableSet> nonEmptyCells = FXCollections.observableSet();
// Initialize content
virtualFlow = VirtualFlow.createVertical(
getParagraphs(),
par -> {
Cell, ParagraphBox> cell = createCell(
par,
applyParagraphStyle,
nodeFactory);
nonEmptyCells.add(cell.getNode());
return cell.beforeReset(() -> nonEmptyCells.remove(cell.getNode()))
.afterUpdateItem(p -> nonEmptyCells.add(cell.getNode()));
});
getChildren().add(virtualFlow);
// initialize navigator
IntSupplier cellCount = () -> getParagraphs().size();
IntUnaryOperator cellLength = i -> virtualFlow.getCell(i).getNode().getLineCount();
navigator = new TwoLevelNavigator(cellCount, cellLength);
// relayout the popup when any of its settings values change (besides the caret being dirty)
EventStream> popupAlignmentDirty = invalidationsOf(popupAlignmentProperty());
EventStream> popupAnchorAdjustmentDirty = invalidationsOf(popupAnchorAdjustmentProperty());
EventStream> popupAnchorOffsetDirty = invalidationsOf(popupAnchorOffsetProperty());
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
// scale
invalidationsOf(scaleXProperty()),
invalidationsOf(scaleYProperty()),
// scroll
invalidationsOf(estimatedScrollXProperty()),
invalidationsOf(estimatedScrollYProperty())
).suppressible();
EventStream> caretBoundsDirty = merge(viewportDirty, caretDirty)
.suppressWhen(beingUpdatedProperty());
EventStream> selectionBoundsDirty = merge(viewportDirty, invalidationsOf(selectionProperty()))
.suppressWhen(beingUpdatedProperty());
// updates the bounds of the caret/selection
caretBounds = Val.create(this::getCaretBoundsOnScreen, caretBoundsDirty);
selectionBounds = Val.create(this::impl_bounds_getSelectionBoundsOnScreen, selectionBoundsDirty);
// Adjust popup anchor by either a user-provided function,
// or user-provided offset, or don't adjust at all.
Val> userOffset = Val.map(
popupAnchorOffsetProperty(),
offset -> anchor -> anchor.add(offset));
_popupAnchorAdjustment =
Val.orElse(
popupAnchorAdjustmentProperty(),
userOffset)
.orElseConst(UnaryOperator.identity());
// dispatch MouseOverTextEvents when mouseOverTextDelay is not null
EventStreams.valuesOf(mouseOverTextDelayProperty())
.flatMap(delay -> delay != null
? mouseOverTextEvents(nonEmptyCells, delay)
: EventStreams.never())
.subscribe(evt -> Event.fireEvent(this, evt));
new StyledTextAreaBehavior(this);
}
/* ********************************************************************** *
* *
* CSS *
* *
* ********************************************************************** */
@Override
public List> getCssMetaData() {
List> styleables =
new ArrayList<>(Region.getClassCssMetaData());
styleables.add(highlightFill.getCssMetaData());
styleables.add(highlightTextFill.getCssMetaData());
styleables.add(caretBlinkRate.getCssMetaData());
return styleables;
}
/* ********************************************************************** *
* *
* Queries *
* *
* Queries are parameterized observables. *
* *
* ********************************************************************** */
/**
* Returns caret bounds relative to the viewport, i.e. the visual bounds
* of the embedded VirtualFlow.
*/
Optional getCaretBoundsInViewport() {
return virtualFlow.getCellIfVisible(getCurrentParagraph())
.map(c -> {
Bounds cellBounds = c.getNode().getCaretBounds();
return virtualFlow.cellToViewport(c, cellBounds);
});
}
/**
* Returns x coordinate of the caret in the current paragraph.
*/
ParagraphBox.CaretOffsetX getCaretOffsetX() {
int idx = getCurrentParagraph();
return getCell(idx).getCaretOffsetX();
}
double getViewportHeight() {
return virtualFlow.getHeight();
}
CharacterHit hit(ParagraphBox.CaretOffsetX x, TwoDimensional.Position targetLine) {
int parIdx = targetLine.getMajor();
ParagraphBox cell = virtualFlow.getCell(parIdx).getNode();
CharacterHit parHit = cell.hitTextLine(x, targetLine.getMinor());
return parHit.offset(getParagraphOffset(parIdx));
}
CharacterHit hit(ParagraphBox.CaretOffsetX x, double y) {
VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(0.0, y);
if(hit.isBeforeCells()) {
return CharacterHit.insertionAt(0);
} else if(hit.isAfterCells()) {
return CharacterHit.insertionAt(getLength());
} else {
int parIdx = hit.getCellIndex();
int parOffset = getParagraphOffset(parIdx);
ParagraphBox cell = hit.getCell().getNode();
Point2D cellOffset = hit.getCellOffset();
CharacterHit parHit = cell.hitText(x, cellOffset.getY());
return parHit.offset(parOffset);
}
}
@Override
public CharacterHit hit(double x, double y) {
VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(x, y);
if(hit.isBeforeCells()) {
return CharacterHit.insertionAt(0);
} else if(hit.isAfterCells()) {
return CharacterHit.insertionAt(getLength());
} else {
int parIdx = hit.getCellIndex();
int parOffset = getParagraphOffset(parIdx);
ParagraphBox cell = hit.getCell().getNode();
Point2D cellOffset = hit.getCellOffset();
CharacterHit parHit = cell.hit(cellOffset);
return parHit.offset(parOffset);
}
}
/**
* Returns the current line as a two-level index.
* The major number is the paragraph index, the minor
* number is the line number within the paragraph.
*
* This method has a side-effect of bringing the current
* paragraph to the viewport if it is not already visible.
*/
TwoDimensional.Position currentLine() {
int parIdx = getCurrentParagraph();
Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx);
int lineIdx = cell.getNode().getCurrentLineIndex();
return _position(parIdx, lineIdx);
}
TwoDimensional.Position _position(int par, int line) {
return navigator.position(par, line);
}
@Override
public int getParagraphLinesCount(int paragraphIndex) {
return virtualFlow.getCell(paragraphIndex).getNode().getLineCount();
}
@Override
public Optional getCharacterBoundsOnScreen(int from, int to) {
if (from < 0) {
throw new IllegalArgumentException("From is negative: " + from);
}
if (from > to) {
throw new IllegalArgumentException(String.format("From is greater than to. from=%s to=%s", from, to));
}
if (to > getLength()) {
throw new IllegalArgumentException(String.format("To is greater than area's length. length=%s, to=%s", getLength(), to));
}
// no bounds exist if range is just a newline character
if (getText(from, to).equals("\n")) {
return Optional.empty();
}
// if 'from' is the newline character at the end of a multi-line paragraph, it returns a Bounds that whose
// minX & minY are the minX and minY of the paragraph itself, not the newline character. So, ignore it.
int realFrom = getText(from, from + 1).equals("\n") ? from + 1 : from;
Position startPosition = offsetToPosition(realFrom, Bias.Forward);
int startRow = startPosition.getMajor();
Position endPosition = startPosition.offsetBy(to - realFrom, Bias.Forward);
int endRow = endPosition.getMajor();
if (startRow == endRow) {
return getRangeBoundsOnScreen(startRow, startPosition.getMinor(), endPosition.getMinor());
} else {
Optional rangeBounds = getRangeBoundsOnScreen(startRow, startPosition.getMinor(),
getParagraph(startRow).length());
for (int i = startRow + 1; i <= endRow; i++) {
Optional nextLineBounds = getRangeBoundsOnScreen(i, 0,
i == endRow
? endPosition.getMinor()
: getParagraph(i).length()
);
if (nextLineBounds.isPresent()) {
if (rangeBounds.isPresent()) {
Bounds lineBounds = nextLineBounds.get();
rangeBounds = rangeBounds.map(b -> {
double minX = Math.min(b.getMinX(), lineBounds.getMinX());
double minY = Math.min(b.getMinY(), lineBounds.getMinY());
double maxX = Math.max(b.getMaxX(), lineBounds.getMaxX());
double maxY = Math.max(b.getMaxY(), lineBounds.getMaxY());
return new BoundingBox(minX, minY, maxX - minX, maxY - minY);
});
} else {
rangeBounds = nextLineBounds;
}
}
}
return rangeBounds;
}
}
@Override
public final String getText(int start, int end) {
return content.getText(start, end);
}
@Override
public String getText(int paragraph) {
return content.getText(paragraph);
}
public Paragraph getParagraph(int index) {
return content.getParagraph(index);
}
public int getParagraphLenth(int index) {
return content.getParagraphLength(index);
}
@Override
public StyledDocument subDocument(int start, int end) {
return content.subSequence(start, end);
}
@Override
public StyledDocument subDocument(int paragraphIndex) {
return content.subDocument(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() : getParagraphLenth(paragraph);
// force selectionProperty() to be valid
getSelection();
return new IndexRange(start, end);
}
@Override
public S getStyleOfChar(int index) {
return content.getStyleOfChar(index);
}
@Override
public S getStyleAtPosition(int position) {
return content.getStyleAtPosition(position);
}
@Override
public IndexRange getStyleRangeAtPosition(int position) {
return content.getStyleRangeAtPosition(position);
}
@Override
public StyleSpans getStyleSpans(int from, int to) {
return content.getStyleSpans(from, to);
}
@Override
public S getStyleOfChar(int paragraph, int index) {
return content.getStyleOfChar(paragraph, index);
}
@Override
public S getStyleAtPosition(int paragraph, int position) {
return content.getStyleAtPosition(paragraph, position);
}
@Override
public IndexRange getStyleRangeAtPosition(int paragraph, int position) {
return content.getStyleRangeAtPosition(paragraph, position);
}
@Override
public StyleSpans getStyleSpans(int paragraph) {
return content.getStyleSpans(paragraph);
}
@Override
public StyleSpans getStyleSpans(int paragraph, int from, int to) {
return content.getStyleSpans(paragraph, from, to);
}
@Override
public int getAbsolutePosition(int paragraphIndex, int columnIndex) {
return content.getAbsolutePosition(paragraphIndex, columnIndex);
}
@Override
public Position position(int row, int col) {
return content.position(row, col);
}
@Override
public Position offsetToPosition(int charOffset, Bias bias) {
return content.offsetToPosition(charOffset, bias);
}
/* ********************************************************************** *
* *
* Actions *
* *
* Actions change the state of this control. They typically cause a *
* change of one or more observables and/or produce an event. *
* *
* ********************************************************************** */
@Override
public void scrollBy(Point2D deltas) {
virtualFlow.scrollXBy(deltas.getX());
virtualFlow.scrollYBy(deltas.getY());
}
void show(double y) {
virtualFlow.show(y);
}
@Override
public void showParagraphInViewport(int paragraphIndex) {
virtualFlow.show(paragraphIndex);
}
@Override
public void showParagraphAtTop(int paragraphIndex) {
virtualFlow.showAsFirst(paragraphIndex);
}
@Override
public void showParagraphAtBottom(int paragraphIndex) {
virtualFlow.showAsLast(paragraphIndex);
}
void showCaretAtBottom() {
int parIdx = getCurrentParagraph();
Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx);
Bounds caretBounds = cell.getNode().getCaretBounds();
double y = caretBounds.getMaxY();
virtualFlow.showAtOffset(parIdx, getViewportHeight() - y);
}
void showCaretAtTop() {
int parIdx = getCurrentParagraph();
Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx);
Bounds caretBounds = cell.getNode().getCaretBounds();
double y = caretBounds.getMinY();
virtualFlow.showAtOffset(parIdx, -y);
}
@Override
public void requestFollowCaret() {
followCaretRequested = true;
requestLayout();
}
private void followCaret() {
int parIdx = getCurrentParagraph();
Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx);
Bounds caretBounds = cell.getNode().getCaretBounds();
double graphicWidth = cell.getNode().getGraphicPrefWidth();
Bounds region = extendLeft(caretBounds, graphicWidth);
virtualFlow.show(parIdx, region);
}
@Override
public void lineStart(SelectionPolicy policy) {
int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineStartPosition();
moveTo(getCurrentParagraph(), columnPos, policy);
}
@Override
public void lineEnd(SelectionPolicy policy) {
int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineEndPosition();
moveTo(getCurrentParagraph(), columnPos, policy);
}
@Override
public void selectLine() {
lineStart(SelectionPolicy.CLEAR);
lineEnd(SelectionPolicy.ADJUST);
}
@Override
public void prevPage(SelectionPolicy selectionPolicy) {
showCaretAtBottom();
CharacterHit hit = hit(getTargetCaretOffset(), 1.0);
moveTo(hit.getInsertionIndex(), selectionPolicy);
}
@Override
public void nextPage(SelectionPolicy selectionPolicy) {
showCaretAtTop();
CharacterHit hit = hit(getTargetCaretOffset(), getViewportHeight() - 1.0);
moveTo(hit.getInsertionIndex(), selectionPolicy);
}
/**
* Displaces the caret from the selection by positioning only the caret to the new location without
* also affecting the selection's {@link #getAnchor() anchor} or the {@link #getSelection() selection}.
* Do not confuse this method with {@link #moveTo(int)}, which is the normal way of moving the caret.
* This method 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 displaceCaret(int pos) {
try(Guard g = suspend(caretPosition, currentParagraph, caretColumn)) {
internalCaretPosition.setValue(pos);
}
}
/**
* Hides the area's context menu if it is not {@code null} and it is {@link ContextMenu#isShowing() showing}.
*/
public final void hideContextMenu() {
ContextMenu menu = getContextMenu();
if (menu != null && menu.isShowing()) {
menu.hide();
}
}
@Override
public void setStyle(int from, int to, S style) {
content.setStyle(from, to, style);
}
@Override
public void setStyle(int paragraph, S style) {
content.setStyle(paragraph, style);
}
@Override
public void setStyle(int paragraph, int from, int to, S style) {
content.setStyle(paragraph, from, to, style);
}
@Override
public void setStyleSpans(int from, StyleSpans extends S> styleSpans) {
content.setStyleSpans(from, styleSpans);
}
@Override
public void setStyleSpans(int paragraph, int from, StyleSpans extends S> styleSpans) {
content.setStyleSpans(paragraph, from, styleSpans);
}
@Override
public void setParagraphStyle(int paragraph, PS paragraphStyle) {
content.setParagraphStyle(paragraph, paragraphStyle);
}
@Override
public void replaceText(int start, int end, String text) {
StyledDocument doc = ReadOnlyStyledDocument.fromString(
text, getParagraphStyleForInsertionAt(start), getStyleForInsertionAt(start), segmentOps);
replace(start, end, doc);
}
@Override
public void replace(int start, int end, StyledDocument replacement) {
content.replace(start, end, replacement);
int newCaretPos = start + replacement.length();
selectRange(newCaretPos, newCaretPos);
}
@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()));
}
}
/* ********************************************************************** *
* *
* Public API *
* *
* ********************************************************************** */
public void dispose() {
subscriptions.unsubscribe();
virtualFlow.dispose();
}
/* ********************************************************************** *
* *
* Layout *
* *
* ********************************************************************** */
@Override
protected void layoutChildren() {
virtualFlow.resize(getWidth(), getHeight());
if(followCaretRequested) {
followCaretRequested = false;
try (Guard g = viewportDirty.suspend()) {
followCaret();
}
}
// position popup
layoutPopup();
}
/* ********************************************************************** *
* *
* Private methods *
* *
* ********************************************************************** */
private Cell, ParagraphBox> createCell(
Paragraph paragraph,
BiConsumer applyParagraphStyle,
Function nodeFactory) {
ParagraphBox box = new ParagraphBox<>(paragraph, applyParagraphStyle, nodeFactory);
box.highlightFillProperty().bind(highlightFill);
box.highlightTextFillProperty().bind(highlightTextFill);
box.wrapTextProperty().bind(wrapTextProperty());
box.graphicFactoryProperty().bind(paragraphGraphicFactoryProperty());
box.graphicOffset.bind(virtualFlow.breadthOffsetProperty());
Val hasCaret = Val.combine(
box.indexProperty(),
currentParagraphProperty(),
(bi, cp) -> bi.intValue() == cp.intValue());
Subscription hasCaretPseudoClass = hasCaret.values().subscribe(value -> box.pseudoClassStateChanged(HAS_CARET, value));
Subscription firstParPseudoClass = box.indexProperty().values().subscribe(idx -> box.pseudoClassStateChanged(FIRST_PAR, idx == 0));
Subscription lastParPseudoClass = EventStreams.combine(
box.indexProperty().values(),
getParagraphs().sizeProperty().values()
).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));
box.caretVisibleProperty().bind(cellCaretVisible);
// bind cell's caret position to area's caret column,
// when the cell is the one with the caret
box.caretPositionProperty().bind(hasCaret.flatMap(has -> has
? caretColumnProperty()
: Val.constant(0)));
// keep paragraph selection updated
ObjectBinding cellSelection = Bindings.createObjectBinding(() -> {
int idx = box.getIndex();
return idx != -1
? getParagraphSelection(idx)
: StyledTextArea.EMPTY_RANGE;
}, selectionProperty(), box.indexProperty());
box.selectionProperty().bind(cellSelection);
return new Cell, ParagraphBox>() {
@Override
public ParagraphBox getNode() {
return box;
}
@Override
public void updateIndex(int index) {
box.setIndex(index);
}
@Override
public void dispose() {
box.highlightFillProperty().unbind();
box.highlightTextFillProperty().unbind();
box.wrapTextProperty().unbind();
box.graphicFactoryProperty().unbind();
box.graphicOffset.unbind();
hasCaretPseudoClass.unsubscribe();
firstParPseudoClass.unsubscribe();
lastParPseudoClass.unsubscribe();
box.caretVisibleProperty().unbind();
box.caretPositionProperty().unbind();
box.selectionProperty().unbind();
cellSelection.dispose();
}
};
}
private ParagraphBox getCell(int index) {
return virtualFlow.getCell(index).getNode();
}
private EventStream mouseOverTextEvents(ObservableSet> cells, Duration delay) {
return merge(cells, c -> c.stationaryIndices(delay).map(e -> e.unify(
l -> l.map((pos, charIdx) -> MouseOverTextEvent.beginAt(c.localToScreen(pos), getParagraphOffset(c.getIndex()) + charIdx)),
r -> MouseOverTextEvent.end())));
}
private int getParagraphOffset(int parIdx) {
return position(parIdx, 0).toOffset();
}
private void layoutPopup() {
PopupWindow popup = getPopupWindow();
PopupAlignment alignment = getPopupAlignment();
UnaryOperator adjustment = _popupAnchorAdjustment.getValue();
if(popup != null) {
positionPopup(popup, alignment, adjustment);
}
}
private void positionPopup(
PopupWindow popup,
PopupAlignment alignment,
UnaryOperator adjustment) {
Optional bounds = null;
switch(alignment.getAnchorObject()) {
case CARET: bounds = getCaretBoundsOnScreen(); break;
case SELECTION: bounds = impl_popup_getSelectionBoundsOnScreen(); break;
}
bounds.ifPresent(b -> {
double x = 0, y = 0;
switch(alignment.getHorizontalAlignment()) {
case LEFT: x = b.getMinX(); break;
case H_CENTER: x = (b.getMinX() + b.getMaxX()) / 2; break;
case RIGHT: x = b.getMaxX(); break;
}
switch(alignment.getVerticalAlignment()) {
case TOP: y = b.getMinY();
case V_CENTER: y = (b.getMinY() + b.getMaxY()) / 2; break;
case BOTTOM: y = b.getMaxY(); break;
}
Point2D anchor = adjustment.apply(new Point2D(x, y));
popup.setAnchorX(anchor.getX());
popup.setAnchorY(anchor.getY());
});
}
private Optional getRangeBoundsOnScreen(int paragraphIndex, int from, int to) {
return virtualFlow.getCellIfVisible(paragraphIndex)
.map(c -> c.getNode().getRangeBoundsOnScreen(from, to));
}
private Optional getCaretBoundsOnScreen() {
return virtualFlow.getCellIfVisible(getCurrentParagraph())
.map(c -> c.getNode().getCaretBoundsOnScreen());
}
private Optional impl_popup_getSelectionBoundsOnScreen() {
IndexRange selection = getSelection();
if(selection.getLength() == 0) {
return getCaretBoundsOnScreen();
}
return impl_getSelectionBoundsOnScreen();
}
private Optional impl_bounds_getSelectionBoundsOnScreen() {
IndexRange selection = getSelection();
if (selection.getLength() == 0) {
return Optional.empty();
}
return impl_getSelectionBoundsOnScreen();
}
private Optional impl_getSelectionBoundsOnScreen() {
Bounds[] bounds = virtualFlow.visibleCells().stream()
.map(c -> c.getNode().getSelectionBoundsOnScreen())
.filter(Optional::isPresent)
.map(Optional::get)
.toArray(Bounds[]::new);
if(bounds.length == 0) {
return Optional.empty();
}
double minX = Stream.of(bounds).mapToDouble(Bounds::getMinX).min().getAsDouble();
double maxX = Stream.of(bounds).mapToDouble(Bounds::getMaxX).max().getAsDouble();
double minY = Stream.of(bounds).mapToDouble(Bounds::getMinY).min().getAsDouble();
double maxY = Stream.of(bounds).mapToDouble(Bounds::getMaxY).max().getAsDouble();
return Optional.of(new BoundingBox(minX, minY, maxX-minX, maxY-minY));
}
private void subscribeTo(EventStream src, Consumer cOnsumer) {
manageSubscription(src.subscribe(cOnsumer));
}
private void manageSubscription(Subscription subscription) {
subscriptions = subscriptions.and(subscription);
}
private void manageBinding(Binding> binding) {
subscriptions = subscriptions.and(binding::dispose);
}
private static Bounds extendLeft(Bounds b, double w) {
if(w == 0) {
return b;
} else {
return new BoundingBox(
b.getMinX() - w, b.getMinY(),
b.getWidth() + w, b.getHeight());
}
}
private S getStyleForInsertionAt(int pos) {
if(useInitialStyleForInsertion.get()) {
return initialTextStyle;
} else {
return content.getStyleAtPosition(pos);
}
}
private PS getParagraphStyleForInsertionAt(int pos) {
if(useInitialStyleForInsertion.get()) {
return initialParagraphStyle;
} else {
return content.getParagraphStyleAtPosition(pos);
}
}
private UndoManager createPlainUndoManager(UndoManagerFactory factory) {
Consumer apply = change -> replaceText(change.getPosition(), change.getPosition() + change.getRemoved().length(), change.getInserted());
BiFunction> merge = PlainTextChange::mergeWith;
return factory.create(plainTextChanges(), PlainTextChange::invert, apply, merge, TextChange::isIdentity);
}
private UndoManager createRichUndoManager(UndoManagerFactory factory) {
Consumer> apply = change -> replace(change.getPosition(), change.getPosition() + change.getRemoved().length(), change.getInserted());
BiFunction, RichTextChange, Optional>> merge = RichTextChange::mergeWith;
return factory.create(richChanges(), RichTextChange::invert, apply, merge, TextChange::isIdentity);
}
private Guard suspend(Suspendable... suspendables) {
return Suspendable.combine(beingUpdated, Suspendable.combine(suspendables)).suspend();
}
void clearTargetCaretOffset() {
targetCaretOffset = Optional.empty();
}
ParagraphBox.CaretOffsetX getTargetCaretOffset() {
if(!targetCaretOffset.isPresent())
targetCaretOffset = Optional.of(getCaretOffsetX());
return targetCaretOffset.get();
}
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();
}
/* ********************************************************************** *
* *
* Deprecated Popup API (Originally a part of "Properties" section *
* *
* Code was moved to bottom of this file to make it easier to stay *
* focused on code still in use. This whole section should be deleted *
* at a later time. *
* *
* ********************************************************************** */
/**
* Popup window that will be positioned by this text area relative to the
* caret or selection. Use {@link #popupAlignmentProperty()} to specify
* how the popup should be positioned relative to the caret or selection.
* Use {@link #popupAnchorOffsetProperty()} or
* {@link #popupAnchorAdjustmentProperty()} to further adjust the position.
*
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
private final ObjectProperty popupWindow = new SimpleObjectProperty<>();
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public void setPopupWindow(PopupWindow popup) { popupWindow.set(popup); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public PopupWindow getPopupWindow() { return popupWindow.get(); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public ObjectProperty popupWindowProperty() { return popupWindow; }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public void setPopupAtCaret(PopupWindow popup) { popupWindow.set(popup); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public PopupWindow getPopupAtCaret() { return popupWindow.get(); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public ObjectProperty popupAtCaretProperty() { return popupWindow; }
/**
* Specifies further offset (in pixels) of the popup window from the
* position specified by {@link #popupAlignmentProperty()}.
*
* If {@link #popupAnchorAdjustmentProperty()} is also specified, then
* it overrides the offset set by this property.
*
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
private final ObjectProperty popupAnchorOffset = new SimpleObjectProperty<>();
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public void setPopupAnchorOffset(Point2D offset) { popupAnchorOffset.set(offset); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public Point2D getPopupAnchorOffset() { return popupAnchorOffset.get(); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public ObjectProperty popupAnchorOffsetProperty() { return popupAnchorOffset; }
/**
* Specifies how to adjust the popup window's anchor point. The given
* operator is invoked with the screen position calculated according to
* {@link #popupAlignmentProperty()} and should return a new screen
* position. This position will be used as the popup window's anchor point.
*
* Setting this property overrides {@link #popupAnchorOffsetProperty()}.
*/
@Deprecated
private final ObjectProperty> popupAnchorAdjustment = new SimpleObjectProperty<>();
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public void setPopupAnchorAdjustment(UnaryOperator f) { popupAnchorAdjustment.set(f); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public UnaryOperator getPopupAnchorAdjustment() { return popupAnchorAdjustment.get(); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public ObjectProperty> popupAnchorAdjustmentProperty() { return popupAnchorAdjustment; }
/**
* Defines where the popup window given in {@link #popupWindowProperty()}
* is anchored, i.e. where its anchor point is positioned. This position
* can further be adjusted by {@link #popupAnchorOffsetProperty()} or
* {@link #popupAnchorAdjustmentProperty()}.
*
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
private final ObjectProperty popupAlignment = new SimpleObjectProperty<>(CARET_TOP);
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public void setPopupAlignment(PopupAlignment pos) { popupAlignment.set(pos); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public PopupAlignment getPopupAlignment() { return popupAlignment.get(); }
/**
* @deprecated Use {@link #getCaretBounds()}/{@link #caretBoundsProperty()} or {@link #getSelectionBounds()}/
* {@link #selectionBoundsProperty()} instead.
*/
@Deprecated
public ObjectProperty popupAlignmentProperty() { return popupAlignment; }
}
| |