All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.fxmisc.richtext.GenericStyledArea Maven / Gradle / Ivy

There is a newer version: 0.11.3
Show newest version
package org.fxmisc.richtext;

import static org.reactfx.EventStreams.*;
import static org.reactfx.util.Tuples.*;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
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 javafx.beans.NamedArg;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableSet;
import javafx.css.CssMetaData;
import javafx.css.PseudoClass;
import javafx.css.StyleConverter;
import javafx.css.Styleable;
import javafx.css.StyleableObjectProperty;
import javafx.event.Event;
import javafx.event.EventHandler;
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 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.model.Codec;
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.PlainTextChange;
import org.fxmisc.richtext.model.Replacement;
import org.fxmisc.richtext.model.RichTextChange;
import org.fxmisc.richtext.model.StyleSpans;
import org.fxmisc.richtext.model.StyledDocument;
import org.fxmisc.richtext.model.StyledSegment;
import org.fxmisc.richtext.model.TextOps;
import org.fxmisc.richtext.model.TwoDimensional;
import org.fxmisc.richtext.model.TwoLevelNavigator;
import org.fxmisc.richtext.event.MouseOverTextEvent;
import org.fxmisc.richtext.util.SubscribeableContentsObsSet;
import org.fxmisc.richtext.util.UndoUtils;
import org.fxmisc.undo.UndoManager;
import org.reactfx.EventStream;
import org.reactfx.EventStreams;
import org.reactfx.Guard;
import org.reactfx.Subscription;
import org.reactfx.Suspendable;
import org.reactfx.SuspendableEventStream;
import org.reactfx.SuspendableNo;
import org.reactfx.collection.LiveList;
import org.reactfx.collection.SuspendableList;
import org.reactfx.util.Tuple2;
import org.reactfx.value.Val;
import org.reactfx.value.Var;

/**
 * Text editing control that renders and edits a {@link EditableStyledDocument}.
 *
 * 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

* *

By default, scroll bars do not 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,

*

 * // shows area without scroll bars
 * InlineCssTextArea area = new InlineCssTextArea();
 *
 * // add scroll bars that will display as needed
 * VirtualizedScrollPane<InlineCssTextArea> vsPane = new VirtualizedScrollPane<>(area);
 *
 * Parent parent = // creation code
 * 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 GenericStyledArea}, 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.

*

For example...

*

 * // assuming the user is currently seeing the top of the area
 *
 * // then changing the bottom, currently not visible part of the area...
 * int startParIdx = 40;
 * int startColPosition = 2;
 * int endParIdx = 42;
 * int endColPosition = 10;
 *
 * // ...by itself will not scroll the viewport to where the change occurs
 * area.replaceText(startParIdx, startColPosition, endParIdx, endColPosition, "replacement text");
 *
 * // adding this line after the last modification to the area will cause the viewport to scroll to that change
 * // leaving the following line out will leave the viewport unaffected and the user will not notice any difference
 * area.requestFollowCaret();
 * 
* *

Additionally, when overriding the default user-interaction behavior, remember to include a call * to {@link #requestFollowCaret()}.

* *

Setting the area's {@link UndoManager}

* *

* The default UndoManager can undo/redo either {@link PlainTextChange}s or {@link RichTextChange}s. To create * your own specialized version that may use changes different than these (or a combination of these changes * with others), create them using the convenient factory methods in {@link UndoUtils}. *

* *

Overriding default keyboard behavior

* * {@code GenericStyledArea} uses {@link javafx.scene.input.KeyEvent#KEY_TYPED KEY_TYPED} to handle ordinary * character input and {@link javafx.scene.input.KeyEvent#KEY_PRESSED KEY_PRESSED} 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. *

* For example, this is how to bind {@code Ctrl+S} to the {@code save()} operation: *

*

 * 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;
 *
 * // installs the following consume InputMap,
 * // so that a CTRL+S event saves the document and consumes the event
 * 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 #onOutsideSelectionMousePressed}, * {@link #onInsideSelectionMousePressReleased}, 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 #onNewSelectionDragFinished}, {@link #onSelectionDropped}, 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. *
  • *
* *

CSS, Style Classes, and Pseudo Classes

*

* Refer to the * RichTextFX CSS Reference Guide * . *

* *

Area Actions and Other Operations

*

* To distinguish the actual operations one can do on this area from the boilerplate methods * within this area (e.g. properties and their getters/setters, etc.), look at the interfaces * this area implements. Each lists and documents methods that fall under that category. *

*

* To update multiple portions of the area's underlying document in one call, see {@link #createMultiChange()}. *

* *

Calculating a Position Within the Area

*

* To calculate a position or index within the area, read through the javadoc of * {@link org.fxmisc.richtext.model.TwoDimensional} and {@link org.fxmisc.richtext.model.TwoDimensional.Bias}. * Also, read the difference between "position" and "index" in * {@link org.fxmisc.richtext.model.StyledDocument#getAbsolutePosition(int, int)}. *

* * @see EditableStyledDocument * @see TwoDimensional * @see org.fxmisc.richtext.model.TwoDimensional.Bias * @see VirtualFlow * @see VirtualizedScrollPane * @see Caret * @see Selection * @see CaretSelectionBind * * @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 static final PseudoClass READ_ONLY = PseudoClass.getPseudoClass("readonly"); 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 CustomStyleableProperty<>(Color.DODGERBLUE, "highlightFill", this, HIGHLIGHT_FILL); /** * Text color for highlighted text. */ private final StyleableObjectProperty highlightTextFill = new CustomStyleableProperty<>(Color.WHITE, "highlightTextFill", this, HIGHLIGHT_TEXT_FILL); // editable property private final BooleanProperty editable = new SimpleBooleanProperty(this, "editable", true) { @Override protected void invalidated() { ((Region) getBean()).pseudoClassStateChanged(READ_ONLY, !get()); } }; @Override public final BooleanProperty editableProperty() { return editable; } // Don't remove as FXMLLoader doesn't recognise default methods ! @Override public void setEditable(boolean value) { editable.set(value); } @Override public boolean isEditable() { return editable.get(); } // wrapText property private final BooleanProperty wrapText = new SimpleBooleanProperty(this, "wrapText"); @Override public final BooleanProperty wrapTextProperty() { return wrapText; } // Don't remove as FXMLLoader doesn't recognise default methods ! @Override public void setWrapText(boolean value) { wrapText.set(value); } @Override public boolean isWrapText() { return wrapText.get(); } // undo manager private UndoManager undoManager; @Override public UndoManager getUndoManager() { return undoManager; } @Override public void setUndoManager(UndoManager undoManager) { this.undoManager.close(); this.undoManager = undoManager; } private final ObjectProperty mouseOverTextDelay = new SimpleObjectProperty<>(null); @Override public ObjectProperty mouseOverTextDelayProperty() { return mouseOverTextDelay; } private final ObjectProperty> paragraphGraphicFactory = new SimpleObjectProperty<>(null); @Override public ObjectProperty> paragraphGraphicFactoryProperty() { return paragraphGraphicFactory; } private ObjectProperty contextMenu = new SimpleObjectProperty<>(null); @Override public final ObjectProperty contextMenuObjectProperty() { return contextMenu; } // Don't remove as FXMLLoader doesn't recognise default methods ! @Override public void setContextMenu(ContextMenu menu) { contextMenu.set(menu); } @Override public ContextMenu getContextMenu() { return contextMenu.get(); } protected final boolean isContextMenuPresent() { return contextMenu.get() != null; } private DoubleProperty contextMenuXOffset = new SimpleDoubleProperty(2); @Override public final DoubleProperty contextMenuXOffsetProperty() { return contextMenuXOffset; } // Don't remove as FXMLLoader doesn't recognise default methods ! @Override public void setContextMenuXOffset(double offset) { contextMenuXOffset.set(offset); } @Override public double getContextMenuXOffset() { return contextMenuXOffset.get(); } private DoubleProperty contextMenuYOffset = new SimpleDoubleProperty(2); @Override public final DoubleProperty contextMenuYOffsetProperty() { return contextMenuYOffset; } // Don't remove as FXMLLoader doesn't recognise default methods ! @Override public void setContextMenuYOffset(double offset) { contextMenuYOffset.set(offset); } @Override public double getContextMenuYOffset() { return contextMenuYOffset.get(); } private final BooleanProperty useInitialStyleForInsertion = new SimpleBooleanProperty(); @Override public BooleanProperty useInitialStyleForInsertionProperty() { return useInitialStyleForInsertion; } private Optional, Codec>>> styleCodecs = Optional.empty(); @Override public void setStyleCodecs(Codec paragraphStyleCodec, Codec> styledSegCodec) { styleCodecs = Optional.of(t(paragraphStyleCodec, styledSegCodec)); } @Override public Optional, Codec>>> getStyleCodecs() { return styleCodecs; } @Override public Var estimatedScrollXProperty() { return virtualFlow.estimatedScrollXProperty(); } @Override public Var estimatedScrollYProperty() { return virtualFlow.estimatedScrollYProperty(); } private final SubscribeableContentsObsSet caretSet; private final SubscribeableContentsObsSet> selectionSet; public final boolean addCaret(CaretNode caret) { if (caret.getArea() != this) { throw new IllegalArgumentException(String.format( "The caret (%s) is associated with a different area (%s), " + "not this area (%s)", caret, caret.getArea(), this)); } return caretSet.add(caret); } public final boolean removeCaret(CaretNode caret) { if (caret != caretSelectionBind.getUnderlyingCaret()) { return caretSet.remove(caret); } else { return false; } } public final boolean addSelection(Selection selection) { if (selection.getArea() != this) { throw new IllegalArgumentException(String.format( "The selection (%s) is associated with a different area (%s), " + "not this area (%s)", selection, selection.getArea(), this)); } return selectionSet.add(selection); } public final boolean removeSelection(Selection selection) { if (selection != caretSelectionBind.getUnderlyingSelection()) { return selectionSet.remove(selection); } else { return false; } } /* ********************************************************************** * * * * Mouse Behavior Hooks * * * * Hooks for overriding some of the default mouse behavior * * * * ********************************************************************** */ @Override public final EventHandler getOnOutsideSelectionMousePressed() { return onOutsideSelectionMousePressed.get(); } @Override public final void setOnOutsideSelectionMousePressed(EventHandler handler) { onOutsideSelectionMousePressed.set( handler ); } @Override public final ObjectProperty> onOutsideSelectionMousePressedProperty() { return onOutsideSelectionMousePressed; } private final ObjectProperty> onOutsideSelectionMousePressed = new SimpleObjectProperty<>( e -> { onOutsideSelectionMousePressProperty().get().accept(e); }); @Override public final EventHandler getOnInsideSelectionMousePressReleased() { return onInsideSelectionMousePressReleased.get(); } @Override public final void setOnInsideSelectionMousePressReleased(EventHandler handler) { onInsideSelectionMousePressReleased.set( handler ); } @Override public final ObjectProperty> onInsideSelectionMousePressReleasedProperty() { return onInsideSelectionMousePressReleased; } private final ObjectProperty> onInsideSelectionMousePressReleased = new SimpleObjectProperty<>( e -> { onInsideSelectionMousePressReleaseProperty().get().accept(e); }); private final ObjectProperty> onNewSelectionDrag = new SimpleObjectProperty<>(p -> { CharacterHit hit = hit(p.getX(), p.getY()); moveTo(hit.getInsertionIndex(), SelectionPolicy.ADJUST); }); @Override public final ObjectProperty> onNewSelectionDragProperty() { return onNewSelectionDrag; } @Override public final EventHandler getOnNewSelectionDragFinished() { return onNewSelectionDragFinished.get(); } @Override public final void setOnNewSelectionDragFinished(EventHandler handler) { onNewSelectionDragFinished.set( handler ); } @Override public final ObjectProperty> onNewSelectionDragFinishedProperty() { return onNewSelectionDragFinished; } private final ObjectProperty> onNewSelectionDragFinished = new SimpleObjectProperty<>( e -> { CharacterHit hit = hit(e.getX(), e.getY()); moveTo(hit.getInsertionIndex(), SelectionPolicy.ADJUST); }); private final ObjectProperty> onSelectionDrag = new SimpleObjectProperty<>(p -> { CharacterHit hit = hit(p.getX(), p.getY()); displaceCaret(hit.getInsertionIndex()); }); @Override public final ObjectProperty> onSelectionDragProperty() { return onSelectionDrag; } @Override public final EventHandler getOnSelectionDropped() { return onSelectionDropped.get(); } @Override public final void setOnSelectionDropped(EventHandler handler) { onSelectionDropped.set( handler ); } @Override public final ObjectProperty> onSelectionDroppedProperty() { return onSelectionDropped; } private final ObjectProperty> onSelectionDropped = new SimpleObjectProperty<>( e -> { onSelectionDropProperty().get().accept(e); }); // not a hook, but still plays a part in the default mouse behavior private final BooleanProperty autoScrollOnDragDesired = new SimpleBooleanProperty(true); @Override public final BooleanProperty autoScrollOnDragDesiredProperty() { return autoScrollOnDragDesired; } // Don't remove as FXMLLoader doesn't recognise default methods ! @Override public void setAutoScrollOnDragDesired(boolean val) { autoScrollOnDragDesired.set(val); } @Override public boolean isAutoScrollOnDragDesired() { return autoScrollOnDragDesired.get(); } /* ********************************************************************** * * * * 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 ObservableValue textProperty() { return content.textProperty(); } // rich text @Override public final StyledDocument getDocument() { return content; } private CaretSelectionBind caretSelectionBind; @Override public final CaretSelectionBind getCaretSelectionBind() { return caretSelectionBind; } // length @Override public final ObservableValue lengthProperty() { return content.lengthProperty(); } // paragraphs @Override public LiveList> getParagraphs() { return content.getParagraphs(); } private final SuspendableList> visibleParagraphs; @Override public final LiveList> getVisibleParagraphs() { return visibleParagraphs; } // beingUpdated private final SuspendableNo beingUpdated = new SuspendableNo(); @Override public final SuspendableNo beingUpdatedProperty() { return beingUpdated; } // total width estimate @Override public Val totalWidthEstimateProperty() { return virtualFlow.totalWidthEstimateProperty(); } // total height estimate @Override public Val totalHeightEstimateProperty() { return virtualFlow.totalHeightEstimateProperty(); } /* ********************************************************************** * * * * Event streams * * * * ********************************************************************** */ @Override public EventStream>> multiRichChanges() { return content.multiRichChanges(); } @Override public EventStream> multiPlainChanges() { return content.multiPlainChanges(); } // text changes @Override public final EventStream plainTextChanges() { return content.plainChanges(); } // rich text changes @Override public final EventStream> richChanges() { return content.richChanges(); } private final SuspendableEventStream viewportDirty; @Override public final EventStream viewportDirtyEvents() { return viewportDirty; } /* ********************************************************************** * * * * Private fields * * * * ********************************************************************** */ private Subscription subscriptions = () -> {}; 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 paragraphLineNavigator; private boolean followCaretRequested = false; /* ********************************************************************** * * * * Fields necessary for Cloning * * * * ********************************************************************** */ private final EditableStyledDocument content; @Override 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 TextOps getSegOps() { return segmentOps; } private final EventStream autoCaretBlinksSteam; final EventStream autoCaretBlink() { return autoCaretBlinksSteam; } /* ********************************************************************** * * * * 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, Node> nodeFactory) { this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, true, nodeFactory); } /** * Same as {@link #GenericStyledArea(Object, BiConsumer, Object, TextOps, Function)} but also allows one * to specify whether the undo manager should be a plain or rich undo manager via {@code preserveStyle}. * * @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 preserveStyle whether to use an undo manager that can undo/redo {@link RichTextChange}s or * {@link PlainTextChange}s * @param nodeFactory A function which is used to create the JavaFX scene node for a particular segment. */ 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, Node> 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} that renders and edits the same * {@link EditableStyledDocument} or when one wants to use a custom {@link EditableStyledDocument} implementation. */ 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, Node> nodeFactory) { this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, document, segmentOps, true, nodeFactory); } /** * Creates an area with flexibility in all of its options. * * @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 document the document to render and edit * @param segmentOps The operations which are defined on the text segment objects. * @param preserveStyle whether to use an undo manager that can undo/redo {@link RichTextChange}s or * {@link PlainTextChange}s * @param nodeFactory A function which is used to create the JavaFX scene node for a particular segment. */ 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, Node> nodeFactory) { this.initialTextStyle = initialTextStyle; this.initialParagraphStyle = initialParagraphStyle; this.preserveStyle = preserveStyle; this.content = document; this.applyParagraphStyle = applyParagraphStyle; this.segmentOps = segmentOps; undoManager = UndoUtils.defaultUndoManager(this); // 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(); caretSet = new SubscribeableContentsObsSet<>(); manageSubscription(() -> { List l = new ArrayList<>(caretSet); caretSet.clear(); l.forEach(CaretNode::dispose); }); selectionSet = new SubscribeableContentsObsSet<>(); manageSubscription(() -> { List> l = new ArrayList<>(selectionSet); selectionSet.clear(); l.forEach(Selection::dispose); }); // 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(); paragraphLineNavigator = new TwoLevelNavigator(cellCount, cellLength); 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(); autoCaretBlinksSteam = EventStreams.valuesOf(focusedProperty() .and(editableProperty()) .and(disabledProperty().not()) ); caretSelectionBind = new CaretSelectionBindImpl<>("main-caret", "main-selection",this); caretSet.add(caretSelectionBind.getUnderlyingCaret()); selectionSet.add(caretSelectionBind.getUnderlyingSelection()); visibleParagraphs = LiveList.map(virtualFlow.visibleCells(), c -> c.getNode().getParagraph()).suspendable(); final Suspendable omniSuspendable = Suspendable.combine( beingUpdated, // must be first, to be the last one to release visibleParagraphs ); manageSubscription(omniSuspendable.suspendWhen(content.beingUpdatedProperty())); // 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 GenericStyledAreaBehavior(this); } /* ********************************************************************** * * * * Queries * * * * Queries are parameterized observables. * * * * ********************************************************************** */ @Override public final double getViewportHeight() { return virtualFlow.getHeight(); } @Override public final Optional allParToVisibleParIndex(int allParIndex) { if (allParIndex < 0) { throw new IllegalArgumentException("Visible paragraph index cannot be negative but was " + allParIndex); } if (allParIndex >= getVisibleParagraphs().size()) { throw new IllegalArgumentException(String.format( "Paragraphs' last index is [%s] but allParIndex was [%s]", getParagraphs().size() - 1, allParIndex) ); } Paragraph p = getParagraph(allParIndex); for (int index = 0; index < getVisibleParagraphs().size(); index++) { if (getVisibleParagraphs().get(index) == p) { return Optional.of(index); } } return Optional.empty(); } @Override public final int visibleParToAllParIndex(int visibleParIndex) { if (visibleParIndex < 0) { throw new IllegalArgumentException("Visible paragraph index cannot be negative but was " + visibleParIndex); } if (visibleParIndex >= getVisibleParagraphs().size()) { throw new IllegalArgumentException(String.format( "Visible paragraphs' last index is [%s] but visibleParIndex was [%s]", getVisibleParagraphs().size() - 1, visibleParIndex) ); } Paragraph visibleP = getVisibleParagraphs().get(visibleParIndex); for (int index = 0; index < getParagraphs().size(); index++) { if (getParagraph(index) == visibleP) { return index; } } throw new AssertionError("Unreachable code"); } @Override public CharacterHit hit(double x, double y) { // mouse position used, so account for padding double adjustedX = x - getInsets().getLeft(); double adjustedY = y - getInsets().getTop(); VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(adjustedX, adjustedY); 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); } } @Override public final int lineIndex(int paragraphIndex, int columnPosition) { Cell, ParagraphBox> cell = virtualFlow.getCell(paragraphIndex); return cell.getNode().getCurrentLineIndex(columnPosition); } @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); } @Override public String getText(IndexRange range) { return content.getText(range); } @Override public StyledDocument subDocument(int start, int end) { return content.subSequence(start, end); } @Override public StyledDocument subDocument(int paragraphIndex) { return content.subDocument(paragraphIndex); } @Override public IndexRange getParagraphSelection(Selection selection, int paragraph) { int startPar = selection.getStartParagraphIndex(); int endPar = selection.getEndParagraphIndex(); if(paragraph < startPar || paragraph > endPar) { return EMPTY_RANGE; } int start = paragraph == startPar ? selection.getStartColumnPosition() : 0; int end = paragraph == endPar ? selection.getEndColumnPosition() : getParagraphLength(paragraph) + 1; // force rangeProperty() to be valid selection.getRange(); 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); } @Override public Bounds getVisibleParagraphBoundsOnScreen(int visibleParagraphIndex) { return getParagraphBoundsOnScreen(virtualFlow.visibleCells().get(visibleParagraphIndex)); } @Override public Optional getParagraphBoundsOnScreen(int paragraphIndex) { return virtualFlow.getCellIfVisible(paragraphIndex).map(this::getParagraphBoundsOnScreen); } @Override public final Optional getCaretBoundsOnScreen(T caret) { return virtualFlow.getCellIfVisible(caret.getParagraphIndex()) .map(c -> c.getNode().getCaretBoundsOnScreen(caret)); } /* ********************************************************************** * * * * 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 scrollXToPixel(double pixel) { suspendVisibleParsWhile(() -> virtualFlow.scrollXToPixel(pixel)); } @Override public void scrollYToPixel(double pixel) { suspendVisibleParsWhile(() -> virtualFlow.scrollYToPixel(pixel)); } @Override public void scrollXBy(double deltaX) { suspendVisibleParsWhile(() -> virtualFlow.scrollXBy(deltaX)); } @Override public void scrollYBy(double deltaY) { suspendVisibleParsWhile(() -> virtualFlow.scrollYBy(deltaY)); } @Override public void scrollBy(Point2D deltas) { suspendVisibleParsWhile(() -> virtualFlow.scrollBy(deltas)); } @Override public void showParagraphInViewport(int paragraphIndex) { suspendVisibleParsWhile(() -> virtualFlow.show(paragraphIndex)); } @Override public void showParagraphAtTop(int paragraphIndex) { suspendVisibleParsWhile(() -> virtualFlow.showAsFirst(paragraphIndex)); } @Override public void showParagraphAtBottom(int paragraphIndex) { suspendVisibleParsWhile(() -> virtualFlow.showAsLast(paragraphIndex)); } @Override public void showParagraphRegion(int paragraphIndex, Bounds region) { suspendVisibleParsWhile(() -> virtualFlow.show(paragraphIndex, region)); } @Override public void requestFollowCaret() { followCaretRequested = true; requestLayout(); } @Override public void lineStart(SelectionPolicy policy) { int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineStartPosition(caretSelectionBind.getUnderlyingCaret()); moveTo(getCurrentParagraph(), columnPos, policy); } @Override public void lineEnd(SelectionPolicy policy) { int columnPos = virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineEndPosition(caretSelectionBind.getUnderlyingCaret()); moveTo(getCurrentParagraph(), columnPos, policy); } @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); } @Override public void displaceCaret(int pos) { caretSelectionBind.displaceCaret(pos); } @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 styleSpans) { content.setStyleSpans(from, styleSpans); } @Override public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { content.setStyleSpans(paragraph, from, styleSpans); } @Override public void setParagraphStyle(int paragraph, PS paragraphStyle) { content.setParagraphStyle(paragraph, paragraphStyle); } @Override public final S getTextStyleForInsertionAt(int pos) { if(useInitialStyleForInsertion.get()) { return initialTextStyle; } else { return content.getStyleAtPosition(pos); } } @Override public final PS getParagraphStyleForInsertionAt(int pos) { if(useInitialStyleForInsertion.get()) { return initialParagraphStyle; } else { return content.getParagraphStyleAtPosition(pos); } } @Override public void replaceText(int start, int end, String text) { StyledDocument doc = ReadOnlyStyledDocument.fromString( text, getParagraphStyleForInsertionAt(start), getTextStyleForInsertionAt(start), segmentOps ); replace(start, end, doc); } @Override public void replace(int start, int end, SEG seg, S style) { StyledDocument doc = ReadOnlyStyledDocument.fromSegment( seg, getParagraphStyleForInsertionAt(start), style, 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); } void replaceMulti(List> replacements) { content.replaceMulti(replacements); // don't update selection as this is not the main method through which the area is updated // leave that up to the developer using it to determine what to do } @Override public MultiChangeBuilder createMultiChange() { return new MultiChangeBuilder<>(this); } @Override public MultiChangeBuilder createMultiChange(int initialNumOfChanges) { return new MultiChangeBuilder<>(this, initialNumOfChanges); } /* ********************************************************************** * * * * Public API * * * * ********************************************************************** */ @Override public void dispose() { if (undoManager != null) { undoManager.close(); } subscriptions.unsubscribe(); virtualFlow.dispose(); } /* ********************************************************************** * * * * Layout * * * * ********************************************************************** */ @Override protected void layoutChildren() { Insets ins = getInsets(); visibleParagraphs.suspendWhile(() -> { virtualFlow.resizeRelocate( ins.getLeft(), ins.getTop(), getWidth() - ins.getLeft() - ins.getRight(), getHeight() - ins.getTop() - ins.getBottom()); if(followCaretRequested) { followCaretRequested = false; try (Guard g = viewportDirty.suspend()) { followCaret(); } } }); } /* ********************************************************************** * * * * Package-Private methods * * * * ********************************************************************** */ /** * 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(caretSelectionBind.getUnderlyingCaret()); return paragraphLineNavigator.position(parIdx, lineIdx); } void showCaretAtBottom() { int parIdx = getCurrentParagraph(); Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); Bounds caretBounds = cell.getNode().getCaretBounds(caretSelectionBind.getUnderlyingCaret()); double y = caretBounds.getMaxY(); suspendVisibleParsWhile(() -> virtualFlow.showAtOffset(parIdx, getViewportHeight() - y)); } void showCaretAtTop() { int parIdx = getCurrentParagraph(); Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); Bounds caretBounds = cell.getNode().getCaretBounds(caretSelectionBind.getUnderlyingCaret()); double y = caretBounds.getMinY(); suspendVisibleParsWhile(() -> virtualFlow.showAtOffset(parIdx, -y)); } /** * Returns x coordinate of the caret in the current paragraph. */ final ParagraphBox.CaretOffsetX getCaretOffsetX(CaretNode caret) { return getCell(caret.getParagraphIndex()).getCaretOffsetX(caret); } 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) { // don't account for padding here since height of virtualFlow is used, not area + potential padding 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); } } final Optional getSelectionBoundsOnScreen(Selection selection) { if (selection.getLength() == 0) { return Optional.empty(); } List bounds = new ArrayList<>(selection.getParagraphSpan()); for (int i = selection.getStartParagraphIndex(); i <= selection.getEndParagraphIndex(); i++) { virtualFlow.getCellIfVisible(i) .ifPresent(c -> c.getNode() .getSelectionBoundsOnScreen(selection) .ifPresent(bounds::add) ); } if(bounds.size() == 0) { return Optional.empty(); } double minX = bounds.stream().mapToDouble(Bounds::getMinX).min().getAsDouble(); double maxX = bounds.stream().mapToDouble(Bounds::getMaxX).max().getAsDouble(); double minY = bounds.stream().mapToDouble(Bounds::getMinY).min().getAsDouble(); double maxY = bounds.stream().mapToDouble(Bounds::getMaxY).max().getAsDouble(); return Optional.of(new BoundingBox(minX, minY, maxX-minX, maxY-minY)); } void clearTargetCaretOffset() { caretSelectionBind.clearTargetOffset(); } ParagraphBox.CaretOffsetX getTargetCaretOffset() { return caretSelectionBind.getTargetOffset(); } /* ********************************************************************** * * * * Private methods * * * * ********************************************************************** */ private Cell, ParagraphBox> createCell( Paragraph paragraph, BiConsumer applyParagraphStyle, Function, Node> nodeFactory) { ParagraphBox box = new ParagraphBox<>(paragraph, applyParagraphStyle, nodeFactory); box.highlightTextFillProperty().bind(highlightTextFill); box.wrapTextProperty().bind(wrapTextProperty()); box.graphicFactoryProperty().bind(paragraphGraphicFactoryProperty()); box.graphicOffset.bind(virtualFlow.breadthOffsetProperty()); EventStream boxIndexValues = box.indexProperty().values().filter(i -> i != -1); Subscription firstParPseudoClass = boxIndexValues.subscribe(idx -> box.pseudoClassStateChanged(FIRST_PAR, idx == 0)); Subscription lastParPseudoClass = EventStreams.combine( boxIndexValues, getParagraphs().sizeProperty().values() ).subscribe(in -> in.exec((i, n) -> box.pseudoClassStateChanged(LAST_PAR, i == n-1))); // set up caret Function subscribeToCaret = caret -> { EventStream caretIndexStream = EventStreams.nonNullValuesOf(caret.paragraphIndexProperty()); // a new event stream needs to be created for each caret added, so that it will immediately // fire the box's current index value as an event, thereby running the code in the subscribe block // Reusing boxIndexValues will not fire its most recent event, leading to a caret not being added // Thus, we'll call the new event stream "fresh" box index values EventStream freshBoxIndexValues = box.indexProperty().values().filter(i -> i != -1); return EventStreams.combine(caretIndexStream, freshBoxIndexValues) .subscribe(t -> { int caretParagraphIndex = t.get1(); int boxIndex = t.get2(); if (caretParagraphIndex == boxIndex) { box.caretsProperty().add(caret); } else { box.caretsProperty().remove(caret); } }); }; Subscription caretSubscription = caretSet.addSubscriber(subscribeToCaret); // TODO: how should 'hasCaret' be handled now? Subscription hasCaretPseudoClass = EventStreams .combine(boxIndexValues, Val.wrap(currentParagraphProperty()).values()) // box index (t1) == caret paragraph index (t2) .map(t -> t.get1().equals(t.get2())) .subscribe(value -> box.pseudoClassStateChanged(HAS_CARET, value)); Function, Subscription> subscribeToSelection = selection -> { EventStream startParagraphValues = EventStreams.nonNullValuesOf(selection.startParagraphIndexProperty()); EventStream endParagraphValues = EventStreams.nonNullValuesOf(selection.endParagraphIndexProperty()); // see comment in caret section about why a new box index EventStream is needed EventStream freshBoxIndexValues = box.indexProperty().values().filter(i -> i != -1); return EventStreams.combine(startParagraphValues, endParagraphValues, freshBoxIndexValues) .subscribe(t -> { int startPar = t.get1(); int endPar = t.get2(); int boxIndex = t.get3(); if (startPar <= boxIndex && boxIndex <= endPar) { // So that we don't add multiple paths for the same selection, // which leads to not removing the additional paths when selection is removed, // this is a `Map#putIfAbsent(Key, Value)` implementation that creates the path lazily SelectionPath p = box.selectionsProperty().get(selection); if (p == null) { // create & configure path Val range = Val.create( () -> boxIndex != -1 ? getParagraphSelection(selection, boxIndex) : EMPTY_RANGE, selection.rangeProperty() ); SelectionPath path = new SelectionPath(range); selection.configureSelectionPath(path); box.selectionsProperty().put(selection, path); } } else { box.selectionsProperty().remove(selection); } }); }; Subscription selectionSubscription = selectionSet.addSubscriber(subscribeToSelection); return new Cell, ParagraphBox>() { @Override public ParagraphBox getNode() { return box; } @Override public void updateIndex(int index) { box.setIndex(index); } @Override public void dispose() { box.highlightTextFillProperty().unbind(); box.wrapTextProperty().unbind(); box.graphicFactoryProperty().unbind(); box.graphicOffset.unbind(); firstParPseudoClass.unsubscribe(); lastParPseudoClass.unsubscribe(); caretSubscription.unsubscribe(); hasCaretPseudoClass.unsubscribe(); selectionSubscription.unsubscribe(); } }; } /** Assumes this method is called within a {@link #suspendVisibleParsWhile(Runnable)} block */ private void followCaret() { int parIdx = getCurrentParagraph(); Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); Bounds caretBounds = cell.getNode().getCaretBounds(caretSelectionBind.getUnderlyingCaret()); double graphicWidth = cell.getNode().getGraphicPrefWidth(); Bounds region = extendLeft(caretBounds, graphicWidth); virtualFlow.show(parIdx, region); } 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 Bounds getParagraphBoundsOnScreen(Cell, ParagraphBox> cell) { Bounds nodeLocal = cell.getNode().getBoundsInLocal(); Bounds nodeScreen = cell.getNode().localToScreen(nodeLocal); Bounds areaLocal = getBoundsInLocal(); Bounds areaScreen = localToScreen(areaLocal); // use area's minX if scrolled right and paragraph's left is not visible double minX = nodeScreen.getMinX() < areaScreen.getMinX() ? areaScreen.getMinX() : nodeScreen.getMinX(); // use area's minY if scrolled down vertically and paragraph's top is not visible double minY = nodeScreen.getMinY() < areaScreen.getMinY() ? areaScreen.getMinY() : nodeScreen.getMinY(); // use area's width whether paragraph spans outside of it or not // so that short or long paragraph takes up the entire space double width = areaScreen.getWidth(); // use area's maxY if scrolled up vertically and paragraph's bottom is not visible double maxY = nodeScreen.getMaxY() < areaScreen.getMaxY() ? nodeScreen.getMaxY() : areaScreen.getMaxY(); return new BoundingBox(minX, minY, width, maxY - minY); } private Optional getRangeBoundsOnScreen(int paragraphIndex, int from, int to) { return virtualFlow.getCellIfVisible(paragraphIndex) .map(c -> c.getNode().getRangeBoundsOnScreen(from, to)); } private void manageSubscription(Subscription subscription) { subscriptions = subscriptions.and(subscription); } 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 void suspendVisibleParsWhile(Runnable runnable) { Suspendable.combine(beingUpdated, visibleParagraphs).suspendWhile(runnable); } /* ********************************************************************** * * * * CSS * * * * ********************************************************************** */ private static final CssMetaData, Paint> HIGHLIGHT_FILL = new CustomCssMetaData<>( "-fx-highlight-fill", StyleConverter.getPaintConverter(), Color.DODGERBLUE, s -> s.highlightFill ); private static final CssMetaData, Paint> HIGHLIGHT_TEXT_FILL = new CustomCssMetaData<>( "-fx-highlight-text-fill", StyleConverter.getPaintConverter(), Color.WHITE, s -> s.highlightTextFill ); private static final List> CSS_META_DATA_LIST; static { List> styleables = new ArrayList<>(Region.getClassCssMetaData()); styleables.add(HIGHLIGHT_FILL); styleables.add(HIGHLIGHT_TEXT_FILL); CSS_META_DATA_LIST = Collections.unmodifiableList(styleables); } @Override public List> getCssMetaData() { return CSS_META_DATA_LIST; } public static List> getClassCssMetaData() { return CSS_META_DATA_LIST; } // Note: this code should be moved to `onOutsideSelectionMousePressed` property // in the next major release before removing this deprecated field @Deprecated private final ObjectProperty> onOutsideSelectionMousePress = new SimpleObjectProperty<>( e -> { CharacterHit hit = hit(e.getX(), e.getY()); moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR); }); @Deprecated @Override public final ObjectProperty> onOutsideSelectionMousePressProperty() { return onOutsideSelectionMousePress; } // Note: this code should be moved to `onInsideSelectionMouseReleased` property // in the next major release before removing this deprecated field @Deprecated private final ObjectProperty> onInsideSelectionMousePressRelease = new SimpleObjectProperty<>( e -> { CharacterHit hit = hit(e.getX(), e.getY()); moveTo(hit.getInsertionIndex(), SelectionPolicy.CLEAR); }); @Deprecated @Override public final ObjectProperty> onInsideSelectionMousePressReleaseProperty() { return onInsideSelectionMousePressRelease; } // Note: this code should be moved to `onSelectionDropped` property // in the next major release before removing this deprecated field @Deprecated private final ObjectProperty> onSelectionDrop = new SimpleObjectProperty<>( e -> { CharacterHit hit = hit(e.getX(), e.getY()); moveSelectedText(hit.getInsertionIndex()); }); @Deprecated @Override public final ObjectProperty> onSelectionDropProperty() { return onSelectionDrop; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy