Java tutorial
/******************************************************************************* * Copyright (c) 2016 BestSolution.at and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Tom Schindl<tom.schindl@bestsolution.at> - initial API and implementation *******************************************************************************/ package org.eclipse.fx.ui.controls.styledtext.internal; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import org.eclipse.fx.ui.controls.styledtext.events.HoverTarget; import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotation; import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter; import com.google.common.collect.Range; import javafx.animation.Animation; import javafx.animation.FadeTransition; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; import javafx.scene.paint.Color; import javafx.scene.shape.ClosePath; import javafx.scene.shape.Line; import javafx.scene.shape.LineTo; import javafx.scene.shape.MoveTo; import javafx.scene.shape.Path; import javafx.scene.shape.StrokeLineCap; import javafx.scene.text.Text; import javafx.util.Duration; @SuppressWarnings("javadoc") public class LineNode extends StackPane { int leftPadding = 0; static final boolean debugAnimation = Boolean.getBoolean("styledtext.debuganimation"); //$NON-NLS-1$ private DebugMarker debugUpdateText; DebugMarker debugUpdateAnnotations; DebugMarker debugUpdateSelection; DebugMarker debugUpdateCaret; private HBox debugBox; /** line index */ int index; LineHelper lineHelper; private static final String CSS_CLASS_SOURCE_SEGMENT_CONTAINER = "source-segment-container"; //$NON-NLS-1$ private static final String CSS_CLASS_SELECTION_MARKER = "selection-marker"; //$NON-NLS-1$ private static final String CSS_CLASS_STYLED_TEXT_LINE = "styled-text-line"; //$NON-NLS-1$ private static final String CSS_CLASS_CURRENT_LINE = "current-line"; //$NON-NLS-1$ private static boolean USE_SINGLE_CHARS = Boolean.getBoolean("efxclipse.styledtext.singlechars"); private int getLineIndex() { return this.index; } static TextNode createNode(IntegerProperty tabCharAdvance) { TextNode textNode = USE_SINGLE_CHARS ? new TextNodeOriginal("", tabCharAdvance) //$NON-NLS-1$ : new TextNodeSingle("", tabCharAdvance); return textNode; } public class TextLayer extends Region { protected final ReuseCache<TextNode> cache; public TextLayer(IntegerProperty tabCharAdvance) { getStyleClass().add(CSS_CLASS_SOURCE_SEGMENT_CONTAINER); // setCache(true); // setCacheHint(CacheHint.QUALITY); setMinWidth(Region.USE_COMPUTED_SIZE); this.cache = new ReuseCache<>(() -> LineNode.createNode(tabCharAdvance)); this.cache.addOnActivate(node -> { getChildren().add((Node) node); // if (!getChildren().contains(node)) { // getChildren().add(node); // } // node.setVisible(true); // node.setManaged(true); }); this.cache.addOnRelease(node -> { getChildren().remove(node); // node.setVisible(false); // node.setManaged(false); }); // cache.addOnClear(node->getChildren().remove(node)); } private List<Segment> currentContent = new ArrayList<>(); private List<TextNode> currentTextNodes = new ArrayList<>(); public boolean updateContent(List<Segment> content) { if (this.currentContent.equals(content)) { return false; } for (TextNode c : this.currentTextNodes) { this.cache.releaseElement(c); } this.currentTextNodes.clear(); for (Segment segment : content) { TextNode node = this.cache.getElement(); node.updateText(segment.text); node.getStyleClass().setAll(segment.styleClasses); this.currentTextNodes.add(node); } if (content.isEmpty()) { TextNode node = this.cache.getElement(); node.updateText(""); //$NON-NLS-1$ this.currentTextNodes.add(node); } this.currentContent = content; // applyCss(); // layout(); return true; } public int getCaretIndexAtPoint(Point2D point) { Point2D scenePoint = localToScene(point); int offset = 0; for (TextNode t : this.currentTextNodes) { if (t.localToScene(t.getBoundsInLocal()).contains(scenePoint)) { int idx = t.getCaretIndexAtPoint(t.sceneToLocal(scenePoint)); if (idx != -1) { int result = idx + offset; return result; } } offset += t.getText().length(); } return -1; } public double getCharLocation(int charOffset) { double result = 0; int startOffset = 0; for (TextNode t : this.currentTextNodes) { int len = t.getText().length(); if (charOffset >= startOffset && charOffset <= startOffset + len) { // hit int textNodeOffset = charOffset - startOffset; result = t.getCharLocation(textNodeOffset); break; } else if (charOffset > startOffset + len) { result = t.getLayoutX() + t.getWidth(); } startOffset += len; } return result; // double location = 0; // int offset = 0; // for (TextNode t : this.currentTextNodes) { // int length = t.getText().length(); // int endOffset = offset + length; // // if (offset <= charOffset && charOffset <= endOffset ) { // int localCharOffset = charOffset - offset; // location = t.getLayoutX() + t.getCharLocation(localCharOffset); // break; // } // else if (charOffset > endOffset) { // location = t.getLayoutX() + t.getWidth(); // } // offset += length; // } // return location; } @Override protected void layoutChildren() { // super.layoutChildren(); double x = 0; double h = getHeight(); for (Node n : getChildren()) { if (n.isManaged() && n instanceof TextNode) { double w = n.prefWidth(-1); double y = h / 2 - n.prefHeight(-1) / 2; n.resizeRelocate(x, y, w, n.prefHeight(-1)); x += w; } } } // @Override // protected double computePrefHeight(double width) { // double x = 0; // for( Node n : getChildren() ) { // if( n.isManaged() && n instanceof TextNode ) { // x = Math.max(x,n.prefHeight(-1)); // } // } // return x; // } @Override protected double computePrefWidth(double height) { double x = 0; for (Node n : getChildren()) { if (n.isManaged() && n instanceof TextNode) { double w = n.prefWidth(-1); x += w; } } return x; } protected String getText() { StringBuilder b = new StringBuilder(); for (TextNode t : this.currentTextNodes) { b.append(t.getText()); } return b.toString(); } private int getStartOffset(Segment segment) { int result = 0; for (Segment s : this.currentContent) { if (s == segment) { break; } result += s.text.length(); } return result; } private int getLength(Segment segment) { return segment.text.length(); } public Collection<? extends HoverTarget> findHoverTargets(Point2D localLocation) { for (TextNode t : this.currentTextNodes) { Bounds segmentBounds = t.getBoundsInParent(); if (segmentBounds.contains(localLocation)) { Segment segment = this.currentContent.get(currentTextNodes.indexOf(t)); Point2D anchor = new Point2D(segmentBounds.getMinX(), segmentBounds.getMaxY()); int segmentBegin = getStartOffset(segment); int segmentEnd = segmentBegin + getLength(segment); Range<Integer> range = Range.closed(segmentBegin, segmentEnd); HoverTarget segmentTarget = new HoverTarget(segment, toGlobal(range), localToScreen(anchor), localToScreen(segmentBounds)); return Collections.singletonList(segmentTarget); } } return Collections.emptyList(); } public Optional<TextNode> findTextNode(Point2D localLocation) { for (TextNode t : this.currentTextNodes) { Bounds segmentBounds = t.getBoundsInParent(); if (segmentBounds.contains(localLocation)) { return Optional.of(t); } } return Optional.empty(); } } public class SelectionLayer extends Region { Region selectionMarker = new Region(); com.google.common.collect.Range<Integer> selection; private boolean continues; public SelectionLayer() { this.selectionMarker.getStyleClass().add(CSS_CLASS_SELECTION_MARKER); this.selectionMarker.setManaged(false); this.getChildren().setAll(this.selectionMarker); } private boolean isSelectionChange(com.google.common.collect.Range<Integer> localSelection) { if (localSelection == null && this.selection == null) { return false; } return this.selection == null || !this.selection.equals(localSelection); } public void updateSelection(com.google.common.collect.Range<Integer> localSelection, boolean continues) { this.continues = continues; if (isSelectionChange(localSelection)) { if (localSelection == null) { this.selectionMarker.setVisible(false); } else { this.selectionMarker.setVisible(true); } this.selection = localSelection; requestLayout(); if (debugAnimation) { LineNode.this.debugUpdateSelection.play(); } } } @Override protected void layoutChildren() { //if (model == null) return; if (this.selection != null) { LineNode.this.textLayer.applyCss(); LineNode.this.textLayer.layout(); double begin = LineNode.this.textLayer.getCharLocation(this.selection.lowerEndpoint().intValue()); double end = LineNode.this.textLayer.getCharLocation(this.selection.upperEndpoint().intValue()); if (this.selection.upperEndpoint().intValue() == LineNode.this.lineHelper .getLength(LineNode.this.index) && this.continues) { end = getWidth(); } this.selectionMarker.resizeRelocate(begin, 0, end - begin, getHeight()); } } } static Animation createCaretAnimation(Node caret) { // Timeline t = new Timeline( // new KeyFrame(Duration.millis(200), new KeyValue(caret.opacityProperty(), 1, Interpolator.DISCRETE)), // new KeyFrame(Duration.millis(200), new KeyValue(caret.opacityProperty(), 0)) // ); // t.setCycleCount(Animation.INDEFINITE); // return t; FadeTransition t = new FadeTransition(Duration.millis(500), caret); t.setInterpolator(new Interpolator() { @Override protected double curve(double t) { if (t < 0.5) { return 0; } else { return 1; } } }); t.setAutoReverse(true); t.setFromValue(1); t.setToValue(0); t.setCycleCount(Animation.INDEFINITE); return t; } public class CaretLayer extends Region { private int caretIndex = -1; private Line caret = new Line(); private Animation caretAnimation; public CaretLayer() { this.caret.setVisible(false); this.caret.setStrokeWidth(2); this.caret.getStyleClass().add("text-caret"); //$NON-NLS-1$ this.caret.setVisible(false); this.getChildren().add(this.caret); this.caretAnimation = createCaretAnimation(this.caret); this.sceneProperty().addListener((x, o, n) -> { if (n == null) { if (this.caretAnimation != null) { this.caretAnimation.stop(); } this.caretAnimation = null; } else { this.caretAnimation = createCaretAnimation(this.caret); } }); } void hideCaret() { this.caret.setVisible(false); } private void showCaret() { this.caret.setVisible(true); } Timeline scheduledAnimation = new Timeline(new KeyFrame(Duration.millis(200), (a) -> { this.caretAnimation.playFromStart(); })); public void updateCaret(int index) { if (index != this.caretIndex) { if (this.scheduledAnimation != null) { this.scheduledAnimation.stop(); } this.caretAnimation.stop(); if (index == -1) { hideCaret(); } else { showCaret(); this.caret.setOpacity(1); this.scheduledAnimation.playFromStart(); } this.caretIndex = index; requestLayout(); if (debugAnimation) { LineNode.this.debugUpdateCaret.play(); } } } @Override public void layoutChildren() { double caretOffset = getCharLocation(this.caretIndex); this.caret.setStartX(caretOffset); this.caret.setStrokeLineCap(StrokeLineCap.BUTT); this.caret.setEndX(caretOffset); this.caret.setStartY(0); this.caret.setEndY(getHeight()); this.caret.toFront(); } } private Node createInsertionMarker(double lineHeight) { double lineWidth = lineHeight / 15d; double arrowSide = lineHeight / 2d; double arrowHeight = arrowSide / 2d; Path marker = new Path(); marker.getElements().add(new MoveTo(-arrowSide / 2d, -arrowHeight)); marker.getElements().add(new LineTo(+arrowSide / 2d, -arrowHeight)); marker.getElements().add(new LineTo(+lineWidth / 2d, 0)); marker.getElements().add(new LineTo(+lineWidth / 2d, lineHeight)); marker.getElements().add(new LineTo(+arrowSide / 2d, lineHeight + arrowHeight)); marker.getElements().add(new LineTo(-arrowSide / 2d, lineHeight + arrowHeight)); marker.getElements().add(new LineTo(-lineWidth / 2d, lineHeight)); marker.getElements().add(new LineTo(-lineWidth / 2d, 0)); marker.getElements().add(new ClosePath()); marker.setVisible(false); marker.getStyleClass().add("insertion-marker"); //$NON-NLS-1$ marker.setMouseTransparent(true); return marker; } public class InsertionMarkerLayer extends Region { private int insertionIndex = -1; private Node insertionMarker; public InsertionMarkerLayer() { heightProperty().addListener((x, o, n) -> { this.getChildren().remove(this.insertionMarker); this.insertionMarker = createInsertionMarker(n.doubleValue()); this.getChildren().add(this.insertionMarker); }); this.insertionMarker = createInsertionMarker(getHeight()); this.getChildren().add(this.insertionMarker); } void hideMarker() { this.insertionMarker.setVisible(false); } private void showMarker() { this.insertionMarker.setVisible(true); } public void updateInsertionIndex(int index) { if (index != this.insertionIndex) { if (index == -1) { hideMarker(); } else { showMarker(); } this.insertionIndex = index; requestLayout(); } } @Override public void layoutChildren() { double caretOffset = getCharLocation(this.insertionIndex); this.insertionMarker.setLayoutX(caretOffset); this.insertionMarker.setLayoutY(0); this.insertionMarker.toFront(); } } public class AnnotationLayer extends StackPane { private Set<TextAnnotation> currentAnnotations; private Set<TextAnnotationPresenter> currentPresenters; public AnnotationLayer() { //setStyle("-fx-border-color: red; -fx-border-style: dashed; -fx-border-width: 1;"); // setCache(true); // setCacheHint(CacheHint.SPEED); } private class AnnotationOverlay extends NodeCachePane { private TextAnnotationPresenter presenter; public AnnotationOverlay(TextAnnotationPresenter presenter) { super(presenter::createNode); this.presenter = presenter; // setStyle("-fx-border-color: red; -fx-border-style: dashed; -fx-border-width: 1;"); // setBackground(new Background(new BackgroundFill(Color.LIGHTGREEN, CornerRadii.EMPTY, Insets.EMPTY))); // setBorder(new Border(new BorderStroke(Color.CADETBLUE, BorderStrokeStyle.SOLID, CornerRadii.EMPTY, BorderWidths.DEFAULT))); setOpacity(0.5); // setPrefHeight(10); // cache.addOnActivate(node->node.setManaged(false)); } private Map<TextAnnotation, Node> usedNodes = new HashMap<>(); private Node getNode(TextAnnotation a) { Node node = this.usedNodes.get(a); if (node == null) { node = getNode(); this.usedNodes.put(a, node); } return node; } private Set<TextAnnotation> current = new HashSet<>(); public void prepareNodes(Set<TextAnnotation> annotations) { boolean same = this.current.equals(annotations); if (same) { return; } this.current = annotations; // release invisible nodes int released = 0; Iterator<Entry<TextAnnotation, Node>> iterator = this.usedNodes.entrySet().iterator(); while (iterator.hasNext()) { Entry<TextAnnotation, Node> entry = iterator.next(); if (!annotations.contains(entry.getKey())) { releaseNode(entry.getValue()); iterator.remove(); released++; } else if (!this.presenter.isVisible(entry.getKey())) { releaseNode(entry.getValue()); iterator.remove(); released++; } } // release non used nodes // HashSet<Node> all = new HashSet<>(getChildren()); // all.removeAll(usedNodes.values()); // all.forEach(this::releaseNode); int presented = 0; // prepare nodes for (TextAnnotation a : annotations) { Node n = getNode(a); this.presenter.updateNode(n, a); presented++; } // iterator = this.usedNodes.entrySet().iterator(); // while (iterator.hasNext()) { // Entry<TextAnnotation, Node> entry = iterator.next(); // presenter.updateNode(entry.getValue(), entry.getKey()); // presented ++; // } } @Override protected void layoutChildren() { Iterator<Entry<TextAnnotation, Node>> iterator = this.usedNodes.entrySet().iterator(); while (iterator.hasNext()) { Entry<TextAnnotation, Node> entry = iterator.next(); com.google.common.collect.Range<Integer> range = entry.getKey().getRange(); double x = getCharLocation(range.lowerEndpoint().intValue()); double width = getCharLocation(range.upperEndpoint().intValue()) - x; entry.getValue().resizeRelocate(x, 0, width, getHeight()); } } public Collection<? extends HoverTarget> findHoverTargets(Point2D localLocation) { return this.usedNodes.entrySet().stream() .filter(e -> e.getValue().getBoundsInParent().contains(localLocation)).map(e -> { TextAnnotation annotation = e.getKey(); Bounds bounds = e.getValue().getBoundsInLocal(); Point2D anchor = new Point2D(bounds.getMinX(), bounds.getMaxY()); HoverTarget annotationTarget = new HoverTarget(annotation, toGlobal(annotation.getRange()), e.getValue().localToScreen(anchor), e.getValue().localToScreen(bounds)); return annotationTarget; }).collect(Collectors.toList()); } } Map<TextAnnotationPresenter, AnnotationOverlay> overlays = new HashMap<>(); public void updateAnnoations(Set<TextAnnotation> annotations, Set<TextAnnotationPresenter> presenters) { if (this.currentAnnotations != null && this.currentAnnotations.equals(annotations) && this.currentPresenters != null && this.currentPresenters.equals(presenters)) { // nothing to updated return; } Iterator<Entry<TextAnnotationPresenter, AnnotationOverlay>> iterator = this.overlays.entrySet() .iterator(); // cleanup presenters while (iterator.hasNext()) { Entry<TextAnnotationPresenter, AnnotationOverlay> entry = iterator.next(); if (!presenters.contains(entry.getKey())) { getChildren().remove(entry.getValue()); iterator.remove(); } } // update presenters for (TextAnnotationPresenter presenter : presenters) { Set<TextAnnotation> applicableAnnotations = annotations.stream().filter(presenter::isApplicable) .collect(Collectors.toSet()); AnnotationOverlay overlay = this.overlays.get(presenter); if (overlay == null) { overlay = new AnnotationOverlay(presenter); getChildren().add(overlay); this.overlays.put(presenter, overlay); } overlay.prepareNodes(applicableAnnotations); overlay.requestLayout(); } requestLayout(); if (debugAnimation) { LineNode.this.debugUpdateAnnotations.play(); } this.currentAnnotations = annotations; this.currentPresenters = presenters; } @Override protected void layoutChildren() { super.layoutChildren(); } public Collection<? extends HoverTarget> findHoverTargets(Point2D localLocation) { List<HoverTarget> hoverTargets = new ArrayList<>(); for (AnnotationOverlay overlay : this.overlays.values()) { hoverTargets.addAll(overlay.findHoverTargets(localLocation)); } return hoverTargets; } } public BooleanProperty caretLayerVisibleProperty() { return this.caretLayer.visibleProperty(); } protected Range<Integer> toGlobal(Range<Integer> range) { int lineOffset = this.lineHelper.getOffset(index); return Range.range(lineOffset + range.lowerEndpoint(), range.lowerBoundType(), lineOffset + range.upperEndpoint(), range.upperBoundType()); } protected Range<Integer> toLocal(Range<Integer> range) { int lineOffset = -this.lineHelper.getOffset(index); return Range.range(lineOffset + range.lowerEndpoint(), range.lowerBoundType(), lineOffset + range.upperEndpoint(), range.upperBoundType()); } final TextLayer textLayer; private SelectionLayer selectionLayer = new SelectionLayer(); private CaretLayer caretLayer = new CaretLayer(); private AnnotationLayer annotationLayer = new AnnotationLayer(); private InsertionMarkerLayer insertionMarkerLayer = new InsertionMarkerLayer(); public LineNode(IntegerProperty tabCharAdvance) { this.textLayer = new TextLayer(tabCharAdvance); // this.model = model; getStyleClass().add(CSS_CLASS_STYLED_TEXT_LINE); setPadding(new Insets(0, 0, 0, this.leftPadding)); // setStyle("-fx-border-width: 0.1px; -fx-border-color: red"); getChildren().setAll(this.insertionMarkerLayer, this.selectionLayer, this.textLayer, this.caretLayer, this.annotationLayer); if (debugAnimation) { this.debugUpdateAnnotations = new DebugMarker(Color.RED, 400); this.debugUpdateText = new DebugMarker(Color.AQUAMARINE, 300); this.debugUpdateSelection = new DebugMarker(Color.BLUE, 150); this.debugUpdateCaret = new DebugMarker(Color.GRAY, 150); this.debugUpdateAnnotations.setWidth(10); this.debugUpdateText.setWidth(10); this.debugUpdateSelection.setWidth(10); this.debugUpdateCaret.setWidth(10); this.debugBox = new HBox(); this.debugBox.setManaged(false); this.debugBox.getChildren().addAll(this.debugUpdateAnnotations, this.debugUpdateSelection, this.debugUpdateCaret, this.debugUpdateText); this.getChildren().add(this.debugBox); } } @Override protected void layoutChildren() { super.layoutChildren(); if (debugAnimation) { this.debugBox.resizeRelocate(0, 0, 40, getHeight()); this.debugUpdateAnnotations.resize(getHeight(), getHeight()); this.debugUpdateText.resize(getHeight(), getHeight()); this.debugUpdateSelection.resize(getHeight(), getHeight()); this.debugUpdateCaret.resize(getHeight(), getHeight()); // debugUpdateAnnotations.resizeRelocate(0, 0, getWidth(), getHeight()); // debugUpdateText.resizeRelocate(0, 0, getWidth(), getHeight()); // debugUpdateSelection.resizeRelocate(0, 0, getWidth(), getHeight()); // debugUpdateCaret.resizeRelocate(0, 0, getWidth(), getHeight()); } } public void setLineHelper(LineHelper helper) { this.lineHelper = helper; } public void update(Set<TextAnnotationPresenter> presenters) { requestLayout(); // Workaround for sick/efxclipse/issues/39 int idx = this.index; updateContent(this.lineHelper.getSegments(idx)); updateSelection(this.lineHelper.getSelection(idx), this.lineHelper.isValidLineIndex(idx + 1) ? this.lineHelper.getSelection(idx + 1) : null); updateCaret(this.lineHelper.getCaret(idx)); updateAnnotations(this.lineHelper.getTextAnnotations(idx), presenters); } public void updateInsertionMarkerIndex(int globalOffset) { final int lineOffset = this.lineHelper.getOffset(this.index); final int lineWidth = this.lineHelper.getLength(this.index); final int localOffset = globalOffset >= lineOffset && globalOffset <= lineOffset + lineWidth ? globalOffset - lineOffset : -1; this.insertionMarkerLayer.updateInsertionIndex(localOffset); } // public void update(StyledTextLine model, Set<TextAnnotationPresenter> annotationPresenter) { // // this.model = model; // // updateContent(model.getSegments()); // updateSelection(model.getSelectionRange()); // updateCaret(model.getCaretIndex()); // // Set<TextAnnotation> textAnnotations = model.getAnnotations().stream().filter(m->m instanceof TextAnnotation).map(m->(TextAnnotation)m).collect(Collectors.toSet()); // this.annotationLayer.updateAnnoations(textAnnotations, annotationPresenter); // } public void updateSelection(com.google.common.collect.Range<Integer> lineSelection, com.google.common.collect.Range<Integer> nextLine) { if (lineSelection != null && lineSelection.isEmpty()) { this.selectionLayer.updateSelection(null, false); } else { this.selectionLayer.updateSelection(lineSelection, nextLine != null); } } boolean currentLine = false; public void updateCaret(int caret) { this.caretLayer.updateCaret(caret); updateCurrentLine(caret != -1); } public void updateCurrentLine(boolean current) { if (this.currentLine != current) { if (current) { getStyleClass().add(CSS_CLASS_CURRENT_LINE); } else { getStyleClass().remove(CSS_CLASS_CURRENT_LINE); } this.currentLine = current; requestLayout(); } } public void updateContent(List<Segment> content) { boolean updated = this.textLayer.updateContent(content); if (updated) { applyCss(); if (debugAnimation) { this.debugUpdateText.play(); } } } public void updateAnnotations(Set<TextAnnotation> annotations, Set<TextAnnotationPresenter> presenters) { this.annotationLayer.updateAnnoations(annotations, presenters); } public int getCaretIndexAtPoint(Point2D p) { return this.textLayer.getCaretIndexAtPoint(p); } public int getStartOffset() { return this.lineHelper.getOffset(this.index); } public int getEndOffset() { return this.lineHelper.getOffset(this.index) + this.lineHelper.getLength(this.index); } public double getCharLocation(int charOffset) { return this.textLayer.getCharLocation(charOffset); } public List<HoverTarget> findHoverTargets(Point2D localLocation) { List<HoverTarget> results = new ArrayList<>(); results.addAll(this.textLayer.findHoverTargets(localLocation)); results.addAll(this.annotationLayer.findHoverTargets(localLocation)); return results; } public Optional<TextNode> findTextNode(Point2D localLocation) { return this.textLayer.findTextNode(localLocation); } /** * Check if the offset is between the start and end * * @param start * the start * @param end * the end * @return <code>true</code> if intersects the offset */ // public boolean intersectOffset(int start, int end) { // if (getStartOffset() > end) { // return false; // } else if (getEndOffset() < start) { // return false; // } // return true; // } // // public boolean intersects(Range globalRange) { // return intersectOffset(globalRange.getOffset(), globalRange.getEndOffset()); // } // // public Range computeLineLocalIntersection(Range globalRange) { // Range result; // if (intersects(globalRange)) { // int begin = Math.max(0, globalRange.getOffset() - getStartOffset()); // int end = Math.min(getEndOffset(), globalRange.getEndOffset() - getStartOffset()); // result = new Range(begin, end - begin); // } // else { // result = new Range(0, 0); // } // return result; // } @Override public String toString() { return "LineNode(idx: " + getLineIndex() + ")@" + hashCode(); //$NON-NLS-1$ //$NON-NLS-2$ } public int getLineLength() { return this.lineHelper.getLength(this.index); } public void release() { this.index = -1; this.caretLayer.hideCaret(); this.selectionLayer.selection = null; this.selectionLayer.selectionMarker.resize(0, 0); } public void setIndex(int idx) { this.index = idx; } }