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

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

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.Locale;
import java.util.Objects;
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.Predicate;
import java.util.function.UnaryOperator;

import javafx.application.ConditionalFeature;
import javafx.application.Platform;
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.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.IndexRange;
import javafx.scene.input.InputMethodEvent;
import javafx.scene.input.InputMethodRequests;
import javafx.scene.input.InputMethodTextRun;
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. * * * * ********************************************************************** */ /** * 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; } /** * @param undoManager may be null in which case a no op undo manager will be set. */ @Override public void setUndoManager(UndoManager undoManager) { this.undoManager.close(); this.undoManager = undoManager != null ? undoManager : UndoUtils.noOpUndoManager(); } private Locale textLocale = Locale.getDefault(); /** * This is used to determine word and sentence breaks while navigating or selecting. * Override this method if your paragraph or text style accommodates Locales as well. * @return Locale.getDefault() by default */ @Override public Locale getLocale() { return textLocale; } public void setLocale( Locale editorLocale ) { textLocale = editorLocale; } 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; } public void recreateParagraphGraphic( int parNdx ) { ObjectProperty> gProp; gProp = getCell(parNdx).graphicFactoryProperty(); gProp.unbind(); gProp.bind(paragraphGraphicFactoryProperty()); } public Node getParagraphGraphic( int parNdx ) { return getCell(parNdx).getGraphic(); } /** * This Node is shown to the user, centered over the area, when the area has no text content. *
To customize the placeholder's layout override {@link #configurePlaceholder( Node )} */ public final void setPlaceholder(Node value) { setPlaceholder(value,Pos.CENTER); } public void setPlaceholder(Node value, Pos where) { placeHolderProp.set(value); placeHolderPos = Objects.requireNonNull(where); } private ObjectProperty placeHolderProp = new SimpleObjectProperty<>(this, "placeHolder", null); public final ObjectProperty placeholderProperty() { return placeHolderProp; } public final Node getPlaceholder() { return placeHolderProp.get(); } private Pos placeHolderPos = Pos.CENTER; 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 -> { moveTo( hit( e.getX(), e.getY() ).getInsertionIndex(), SelectionPolicy.CLEAR ); }); @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 -> { moveTo( hit( e.getX(), e.getY() ).getInsertionIndex(), SelectionPolicy.CLEAR ); }); 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 -> { moveSelectedText( hit( e.getX(), e.getY() ).getInsertionIndex() ); }); // 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 paging, 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); caretSelectionBind.paragraphIndexProperty().addListener( this::skipOverFoldedParagraphs ); 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); // Setup place holder visibility & placement final Val showPlaceholder = Val.create ( () -> getLength() == 0 && ! isFocused(), lengthProperty(), focusedProperty() ); placeHolderProp.addListener( (ob,ov,newNode) -> displayPlaceHolder( showPlaceholder.getValue(), newNode ) ); showPlaceholder.addListener( (ob,ov,show) -> displayPlaceHolder( show, getPlaceholder() ) ); if ( Platform.isFxApplicationThread() ) initInputMethodHandling(); else Platform.runLater( () -> initInputMethodHandling() ); } private void initInputMethodHandling() { if( Platform.isSupported( ConditionalFeature.INPUT_METHOD ) ) { setOnInputMethodTextChanged( event -> handleInputMethodEvent(event) ); // Both of these have to be set for input composition to work ! setInputMethodRequests( new InputMethodRequests() { @Override public Point2D getTextLocation( int offset ) { return getCaretBounds() .or( () -> getCharacterBoundsOnScreen( offset, offset ) ) .map( cb -> new Point2D( cb.getMaxX() - 5, cb.getMaxY() ) ) .orElseGet( () -> new Point2D( 10,10 ) ); } @Override public int getLocationOffset( int x, int y ) { return 0; } @Override public void cancelLatestCommittedText() {} @Override public String getSelectedText() { return getSelectedText(); } }); } } // Start/Length of the text under input method composition private int imstart; private int imlength; protected void handleInputMethodEvent( InputMethodEvent event ) { if ( isEditable() && !isDisabled() ) { // remove previous input method text (if any) or selected text if ( imlength != 0 ) { selectRange( imstart, imstart + imlength ); } // Insert committed text if ( event.getCommitted().length() != 0 ) { replaceText( getSelection(), event.getCommitted() ); } // Replace composed text imstart = getSelection().getStart(); StringBuilder composed = new StringBuilder(); for ( InputMethodTextRun run : event.getComposed() ) { composed.append( run.getText() ); } replaceText( getSelection(), composed.toString() ); imlength = composed.length(); if ( imlength != 0 ) { int pos = imstart; for ( InputMethodTextRun run : event.getComposed() ) { int endPos = pos + run.getText().length(); pos = endPos; } // Set caret position in composed text int caretPos = event.getCaretPosition(); if ( caretPos >= 0 && caretPos < imlength ) { selectRange( imstart + caretPos, imstart + caretPos ); } } } } private Node placeholder; private boolean positionPlaceholder = false; private void displayPlaceHolder( boolean show, Node newNode ) { if ( placeholder != null && (! show || newNode != placeholder) ) { placeholder.layoutXProperty().unbind(); placeholder.layoutYProperty().unbind(); getChildren().remove( placeholder ); placeholder = null; setClip( null ); } if ( newNode != null && show && newNode != placeholder ) { configurePlaceholder( newNode ); getChildren().add( newNode ); placeholder = newNode; } } /** * Override this to customize the placeholder's layout. *
The default position is centered over the area. */ protected void configurePlaceholder( Node placeholder ) { positionPlaceholder = true; } /* ********************************************************************** * * * * 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("The given paragraph index (allParIndex) cannot be negative but was " + allParIndex); } if (allParIndex >= getParagraphs().size()) { throw new IllegalArgumentException(String.format( "Paragraphs' last index is [%s] but allParIndex was [%s]", getParagraphs().size() - 1, allParIndex) ); } List, ParagraphBox>> visibleList = virtualFlow.visibleCells(); int firstVisibleParIndex = visibleList.get( 0 ).getNode().getIndex(); int targetIndex = allParIndex - firstVisibleParIndex; if ( allParIndex >= firstVisibleParIndex && targetIndex < visibleList.size() ) { if ( visibleList.get( targetIndex ).getNode().getIndex() == allParIndex ) { return Optional.of( targetIndex ); } } 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 > 0 && visibleParIndex >= getVisibleParagraphs().size()) { throw new IllegalArgumentException(String.format( "Visible paragraphs' last index is [%s] but visibleParIndex was [%s]", getVisibleParagraphs().size() - 1, visibleParIndex) ); } Cell, ParagraphBox> visibleCell = null; if ( visibleParIndex > 0 ) visibleCell = virtualFlow.visibleCells().get( visibleParIndex ); else visibleCell = virtualFlow.getCellIfVisible( virtualFlow.getFirstVisibleIndex() ) .orElseGet( () -> virtualFlow.visibleCells().get( visibleParIndex ) ); return visibleCell.getNode().getIndex(); } @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)); } if (from == to) { CaretNode cursor = new CaretNode( "", this, from ); int parIdx = offsetToPosition( from, Bias.Forward ).getMajor(); ParagraphBox paragrafBox = virtualFlow.getCell( parIdx ).getNode(); paragrafBox.caretsProperty().add( cursor ); Bounds cursorBounds = paragrafBox.getCaretBoundsOnScreen( cursor ); paragrafBox.caretsProperty().remove( cursor ); if ( cursorBounds != null && ! cursorBounds.isEmpty() ) { Bounds emptyCharBounds = new BoundingBox( cursorBounds.getMinX()+1, cursorBounds.getMinY()+1, cursorBounds.getWidth()-1, cursorBounds.getHeight()-2 ); return Optional.of( emptyCharBounds ); } return Optional.empty(); } // 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(selection.getLength() == 0 || 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) { Optional caretBounds; try { // This is the default mechanism, but sometimes throws just like in followCaret() caretBounds = virtualFlow.getCellIfVisible(caret.getParagraphIndex()) .map(c -> c.getNode().getCaretBoundsOnScreen(caret)); } catch ( IllegalArgumentException EX ) { // This is an alternative mechanism, to address https://github.com/FXMisc/RichTextFX/issues/1048 caretBounds = Optional.ofNullable( caretSelectionBind.getUnderlyingCaret().getLayoutBounds() ); } return caretBounds; } /* ********************************************************************** * * * * 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)); } public void showParagraphAtCenter(int paragraphIndex) { double offset = Math.floor( getHeight() / 2.0 ); suspendVisibleParsWhile(() -> virtualFlow.showAtOffset(paragraphIndex,offset)); } @Override public void requestFollowCaret() { followCaretRequested = true; requestLayout(); } @Override public void lineStart(SelectionPolicy policy) { moveTo(getCurrentParagraph(), getCurrentLineStartInParargraph(), policy); } @Override public void lineEnd(SelectionPolicy policy) { moveTo(getCurrentParagraph(), getCurrentLineEndInParargraph(), policy); } public int getCurrentLineStartInParargraph() { return virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineStartPosition(caretSelectionBind.getUnderlyingCaret()); } public int getCurrentLineEndInParargraph() { return virtualFlow.getCell(getCurrentParagraph()).getNode().getCurrentLineEndPosition(caretSelectionBind.getUnderlyingCaret()); } private double caretPrevY = -1; private LineSelection lineHighlighter; private ObjectProperty lineHighlighterFill; /** * The default fill is "highlighter" yellow. It can also be styled using CSS with:
* .styled-text-area .line-highlighter { -fx-fill: lime; }
* CSS selectors from Path, Shape, and Node can also be used. */ public void setLineHighlighterFill( Paint highlight ) { if ( lineHighlighterFill != null && highlight != null ) { lineHighlighterFill.set( highlight ); } else { boolean lineHighlightOn = isLineHighlighterOn(); if ( lineHighlightOn ) setLineHighlighterOn( false ); if ( highlight == null ) lineHighlighterFill = null; else lineHighlighterFill = new SimpleObjectProperty( highlight ); if ( lineHighlightOn ) setLineHighlighterOn( true ); } } public boolean isLineHighlighterOn() { return lineHighlighter != null && selectionSet.contains( lineHighlighter ) ; } /** * Highlights the line that the main caret is on.
* Line highlighting automatically follows the caret. */ public void setLineHighlighterOn( boolean show ) { if ( show ) { if ( lineHighlighter != null ) return; lineHighlighter = new LineSelection<>( this, lineHighlighterFill ); Consumer caretListener = b -> { if ( lineHighlighter != null && (b.getMinY() != caretPrevY || getCaretColumn() == 1) ) { if ( getSelection().getLength() != 0 ) lineHighlighter.deselect(); else lineHighlighter.selectCurrentLine(); caretPrevY = b.getMinY(); } }; caretBoundsProperty().addListener( (ob,ov,nv) -> nv.ifPresent( caretListener ) ); getCaretBounds().ifPresent( caretListener ); selectionProperty().addListener( (ob,ov,nv) -> { if ( lineHighlighter != null ) { if ( nv.getLength() == 0 ) lineHighlighter.selectCurrentLine(); else if ( ov.getLength() == 0 ) lineHighlighter.deselect(); } }); selectionSet.add( lineHighlighter ); } else if ( lineHighlighter != null ) { selectionSet.remove( lineHighlighter ); lineHighlighter.deselect(); lineHighlighter = null; caretPrevY = -1; } } /** * Scrolls the text one line UP while maintaining the caret's * position on screen, so that it is now on the NEXT line. * Note: If the caret isn't visible then this is a noop. */ public void nextLine(SelectionPolicy selectionPolicy) { scrollLine( +1, selectionPolicy ); } /** * Scrolls the text one line DOWN while maintaining the caret's * position on screen, so that it is now on the PREVIOUS line. * Note: If the caret isn't visible then this is a noop. */ public void prevLine(SelectionPolicy selectionPolicy) { scrollLine( -1, selectionPolicy ); } /** * Scrolls the text (direction*caretHeight) while maintaining the caret's current * position on screen. Note: If the caret isn't visible then this is a noop. */ private void scrollLine(int direction, SelectionPolicy selectionPolicy) { getCaretBoundsOnScreen( getCaretSelectionBind().getUnderlyingCaret() ) .map( caretBounds -> (caretBounds.getHeight()-2) * direction ) .ifPresent( deltaY -> scrollText(deltaY, selectionPolicy) ); } @Override public void prevPage(SelectionPolicy selectionPolicy) { // Paging up and we're in the first frame then move/select to start. if ( firstVisibleParToAllParIndex() == 0 ) { caretSelectionBind.moveTo( 0, selectionPolicy ); } else page( -1, selectionPolicy ); } @Override public void nextPage(SelectionPolicy selectionPolicy) { // Paging down and we're in the last frame then move/select to end. if ( lastVisibleParToAllParIndex() == getParagraphs().size()-1 ) { caretSelectionBind.moveTo( getLength(), selectionPolicy ); } else page( +1, selectionPolicy ); } /** * @param pgCount the number of pages to page up/down. *
Negative numbers for paging up and positive for down. */ private void page(int pgCount, SelectionPolicy selectionPolicy) { scrollText( pgCount * getViewportHeight(), selectionPolicy ); } /** * Scrolls the text by deltaY while maintaining the caret's current position on screen. */ private void scrollText(double deltaY, SelectionPolicy selectionPolicy) { // Use underlying caret to get the same behaviour as navigating up/down a line where the x position is sticky Optional cb = caretSelectionBind.getUnderlyingCaret().getCaretBounds(); paging = true; // Prevent scroll from reverting back to the current caret position suspendVisibleParsWhile( () -> virtualFlow.scrollYBy( deltaY ) ); cb.map( this::screenToLocal ) // Place caret near the same on screen position as before .map( b -> hit( b.getMinX(), b.getMinY()+b.getHeight()/2.0 ).getInsertionIndex() ) .ifPresent( i -> caretSelectionBind.moveTo( i, selectionPolicy ) ); // Adjust scroll by a few pixels to get the caret at the exact on screen location as before cb.ifPresent( prev -> getCaretBounds().map( newB -> newB.getMinY() - prev.getMinY() ) .filter( delta -> delta != 0.0 ).ifPresent( delta -> scrollYBy( delta ) ) ); } @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); } /** * If you want to preset the style to be used for inserted text. Note that useInitialStyleForInsertion overrides this if true. */ public final void setTextInsertionStyle( S txtStyle ) { insertionTextStyle = txtStyle; } public final S getTextInsertionStyle() { return insertionTextStyle; } private S insertionTextStyle; @Override public final S getTextStyleForInsertionAt(int pos) { if ( insertionTextStyle != null ) { return insertionTextStyle; } else if ( useInitialStyleForInsertion.get() ) { return initialTextStyle; } else { return content.getStyleAtPosition(pos); } } private PS insertionParagraphStyle; /** * If you want to preset the style to be used. Note that useInitialStyleForInsertion overrides this if true. */ public final void setParagraphInsertionStyle( PS paraStyle ) { insertionParagraphStyle = paraStyle; } public final PS getParagraphInsertionStyle() { return insertionParagraphStyle; } @Override public final PS getParagraphStyleForInsertionAt(int pos) { if ( insertionParagraphStyle != null ) { return insertionParagraphStyle; } else 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) { if (style == null) style = getTextStyleForInsertionAt(start); 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); } /** * Convenience method to fold (hide/collapse) the currently selected paragraphs, * into (i.e. excluding) the first paragraph of the range. * * @param styleMixin Given a paragraph style PS, return a new PS that will activate folding. * *

See {@link #fold(int, int, UnaryOperator)} for more info.

*/ protected void foldSelectedParagraphs( UnaryOperator styleMixin ) { IndexRange range = getSelection(); fold( range.getStart(), range.getEnd(), styleMixin ); } /** * Folds (hides/collapses) paragraphs from start to * end, into (i.e. excluding) the first paragraph of the range. * * @param styleMixin Given a paragraph style PS, return a new PS that will activate folding. * *

See {@link #fold(int, int, UnaryOperator)} for more info.

*/ protected void foldParagraphs( int start, int end, UnaryOperator styleMixin ) { start = getAbsolutePosition( start, 0 ); end = getAbsolutePosition( end, getParagraphLength( end ) ); fold( start, end, styleMixin ); } /** * Folds (hides/collapses) paragraphs from character position startPos * to endPos, into (i.e. excluding) the first paragraph of the range. * *

Folding is achieved with the help of paragraph styling, which is applied to the paragraph's * TextFlow object through the applyParagraphStyle BiConsumer (supplied in the constructor to * GenericStyledArea). When applyParagraphStyle is to apply fold styling it just needs to set * the TextFlow's visibility to collapsed for it to be folded. See {@code InlineCssTextArea}, * {@code StyleClassedTextArea}, and {@code RichTextDemo} for different ways of doing this. * Also read the GitHub Wiki.

* *

The UnaryOperator styleMixin must return a * different paragraph style Object to what was submitted.

* * @param styleMixin Given a paragraph style PS, return a new PS that will activate folding. */ protected void fold( int startPos, int endPos, UnaryOperator styleMixin ) { ReadOnlyStyledDocument subDoc; UnaryOperator> mapper; subDoc = (ReadOnlyStyledDocument) subDocument( startPos, endPos ); mapper = p -> p.setParagraphStyle( styleMixin.apply( p.getParagraphStyle() ) ); for ( int p = 1; p < subDoc.getParagraphCount(); p++ ) { subDoc = subDoc.replaceParagraph( p, mapper ).get1(); } replace( startPos, endPos, subDoc ); recreateParagraphGraphic( offsetToPosition( startPos, Bias.Backward ).getMajor() ); moveTo( startPos ); foldCheck = true; } protected boolean foldCheck = false; private void skipOverFoldedParagraphs( ObservableValue ob, Integer prevParagraph, Integer newParagraph ) { if ( foldCheck && getCell( newParagraph ).isFolded() ) { // Prevent Ctrl+A and Ctrl+End breaking when the last paragraph is folded // github.com/FXMisc/RichTextFX/pull/965#issuecomment-706268116 if ( newParagraph == getParagraphs().size() - 1 ) return; int skip = (newParagraph - prevParagraph > 0) ? +1 : -1; int p = newParagraph + skip; while ( p > 0 && p < getParagraphs().size() ) { if ( getCell( p ).isFolded() ) p += skip; else break; } if ( p < 0 || p == getParagraphs().size() ) p = prevParagraph; int col = Math.min( getCaretColumn(), getParagraphLength( p ) ); if ( getSelection().getLength() == 0 ) moveTo( p, col ); else moveTo( p, col, SelectionPolicy.EXTEND ); } } /** * Unfolds paragraphs startingFrom onwards for the currently folded block. * *

The UnaryOperator styleMixin must return a * different paragraph style Object to what was submitted.

* * @param isFolded Given a paragraph style PS check if it's folded. * @param styleMixin Given a paragraph style PS, return a new PS that excludes fold styling. */ protected void unfoldParagraphs( int startingFrom, Predicate isFolded, UnaryOperator styleMixin ) { LiveList> pList = getParagraphs(); int to = startingFrom; while ( ++to < pList.size() ) { if ( ! isFolded.test( pList.get( to ).getParagraphStyle() ) ) break; } if ( --to > startingFrom ) { ReadOnlyStyledDocument subDoc; UnaryOperator> mapper; int startPos = getAbsolutePosition( startingFrom, 0 ); int endPos = getAbsolutePosition( to, getParagraphLength( to ) ); subDoc = (ReadOnlyStyledDocument) subDocument( startPos, endPos ); mapper = p -> p.setParagraphStyle( styleMixin.apply( p.getParagraphStyle() ) ); for ( int p = 1; p < subDoc.getParagraphCount(); p++ ) { subDoc = subDoc.replaceParagraph( p, mapper ).get1(); } replace( startPos, endPos, subDoc ); moveTo( startingFrom, getParagraphLength( startingFrom ) ); recreateParagraphGraphic( startingFrom ); } } /* ********************************************************************** * * * * Public API * * * * ********************************************************************** */ @Override public void dispose() { if (undoManager != null) { undoManager.close(); } subscriptions.unsubscribe(); virtualFlow.dispose(); } /* ********************************************************************** * * * * Layout * * * * ********************************************************************** */ private BooleanProperty autoHeightProp = new SimpleBooleanProperty(); public BooleanProperty autoHeightProperty() { return autoHeightProp; } public void setAutoHeight( boolean value ) { autoHeightProp.set( value ); } public boolean isAutoHeight() { return autoHeightProp.get(); } @Override protected double computePrefHeight( double width ) { if ( autoHeightProp.get() ) { if ( getWidth() == 0.0 ) Platform.runLater( () -> requestLayout() ); else { double height = 0.0; Insets in = getInsets(); for ( int p = 0; p < getParagraphs().size(); p++ ) { height += getCell( p ).getHeight(); } if ( height > 0.0 ) { return height + in.getTop() + in.getBottom(); } } } return super.computePrefHeight( width ); } @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 && ! paging) { try (Guard g = viewportDirty.suspend()) { followCaret(); } } followCaretRequested = false; paging = false; }); Node holder = placeholder; if (holder != null && holder.isManaged()) { if (holder.isResizable()) holder.autosize(); if (positionPlaceholder) Region.positionInArea ( holder, getLayoutX(), getLayoutY(), getWidth(), getHeight(), getBaselineOffset(), ins, placeHolderPos.getHpos(), placeHolderPos.getVpos(), isSnapToPixel() ); } } /* ********************************************************************** * * * * 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( () -> box.getIndex() != -1 ? getParagraphSelection(selection, box.getIndex()) : EMPTY_RANGE, selection.rangeProperty() ); SelectionPath path = new SelectionPath(range); path.getStyleClass().add( selection.getSelectionName() ); 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(); box.dispose(); 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(); ParagraphBox paragrafBox = virtualFlow.getCell( parIdx ).getNode(); Bounds caretBounds; try { // This is the default mechanism, but is also needed for https://github.com/FXMisc/RichTextFX/issues/1017 caretBounds = paragrafBox.getCaretBounds( caretSelectionBind.getUnderlyingCaret() ); } catch ( IllegalArgumentException EX ) { // This is an alternative mechanism, to address https://github.com/FXMisc/RichTextFX/issues/939 caretBounds = caretSelectionBind.getUnderlyingCaret().getLayoutBounds(); } double graphicWidth = paragrafBox.getGraphicPrefWidth(); Bounds region = extendLeft(caretBounds, graphicWidth); double scrollX = virtualFlow.getEstimatedScrollX(); // Ordinarily when a caret ends a selection in the target paragraph and scrolling left is required to follow // the caret then the selection won't be visible. So here we check for this scenario and adjust if needed. if ( ! isWrapText() && scrollX > 0.0 && getParagraphSelection( parIdx ).getLength() > 0 ) { CaretNode selectionStart = new CaretNode( "", this, getSelection().getStart() ); paragrafBox.caretsProperty().add( selectionStart ); Bounds startBounds = paragrafBox.getCaretBounds( selectionStart ); paragrafBox.caretsProperty().remove( selectionStart ); if ( startBounds.getMinX() - graphicWidth < scrollX ) { region = extendLeft( startBounds, graphicWidth ); } } // Addresses https://github.com/FXMisc/RichTextFX/issues/937#issuecomment-674319602 if ( parIdx == getParagraphs().size()-1 && paragrafBox.getLineCount() == 1 ) { region = new BoundingBox // Correcting the region's height ( region.getMinX(), region.getMinY(), region.getWidth(), paragrafBox.getLayoutBounds().getHeight() ); } 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_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_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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy