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.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import org.eclipse.fx.ui.controls.Util; import org.eclipse.fx.ui.controls.styledtext.StyledTextArea; import org.eclipse.fx.ui.controls.styledtext.StyledTextArea.LineLocation; import org.eclipse.fx.ui.controls.styledtext.StyledTextContent; import org.eclipse.fx.ui.controls.styledtext.StyledTextContent.TextChangeListener; import org.eclipse.fx.ui.controls.styledtext.TextChangedEvent; import org.eclipse.fx.ui.controls.styledtext.TextChangingEvent; import org.eclipse.fx.ui.controls.styledtext.TextSelection; import org.eclipse.fx.ui.controls.styledtext.events.HoverTarget; import org.eclipse.fx.ui.controls.styledtext.model.TextAnnotationPresenter; import com.google.common.collect.ContiguousSet; import com.google.common.collect.DiscreteDomain; import com.google.common.collect.Range; import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeSet; import javafx.beans.Observable; import javafx.beans.binding.Bindings; import javafx.beans.binding.DoubleBinding; import javafx.beans.property.DoubleProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.ReadOnlyIntegerProperty; import javafx.beans.property.SetProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.beans.property.SimpleSetProperty; import javafx.collections.FXCollections; import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Point2D; import javafx.scene.Node; import javafx.scene.control.ScrollBar; import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; @SuppressWarnings("javadoc") public class ContentView extends Pane { private SetProperty<TextAnnotationPresenter> textAnnotationPresenter = new SimpleSetProperty<>( FXCollections.observableSet()); public SetProperty<TextAnnotationPresenter> textAnnotationPresenterProperty() { return this.textAnnotationPresenter; } private class LineLayer extends VFlow<LineNode> { public LineLayer(Supplier<LineNode> nodeFactory, BiConsumer<LineNode, Integer> nodePopulator) { super(nodeFactory, nodePopulator); setOnRelease(n -> n.release()); setOnActivate((idx, n) -> n.setIndex(idx.intValue())); } // protected void releaseNode(int lineIndex) { // // get(model.get(lineIndex)).ifPresent(n->{ // n.setVisible(false); // n.release(); // }); // } // protected void releaseNode(StyledTextLine line) { // release(line); //// get(line).ifPresent(n->{ //// n.setVisible(false); //// n.release(); //// }); // } // private void updateNode(StyledTextLine line) { // LineNode node = get(line); // node.update(line, textAnnotationPresenter); //// LineNode node = getCreate(m); //// node.setVisible(true); ////// node.setModel(m); //// node.update(m, textAnnotationPresenter); // } // private void updateNode(int lineIndex, StyledTextLine m) { // LineNode node = getCreate(m); // node.setVisible(true); //// node.setModel(m); // node.update(m, textAnnotationPresenter); // } // @Override // public void requestLayout() { // super.requestLayout(); // } // @Override // protected void layoutChildren() { // ContiguousSet.create(visibleLines.get(), DiscreteDomain.integers()).forEach(e -> { // if (!yOffsetData.containsKey(e)) { // return; // } // double x = 0; // double y = yOffsetData.get(e); // double width = getWidth(); // double height = getLineHeight(); // // if (model.size() > e) { // StyledTextLine m = model.get(e); // LineNode lineNode = get(m); // lineNode.resizeRelocate(x, y, width, height); // //// get(m).ifPresent(n->n.resizeRelocate(x, y, width, height)); // // lineNode.layout(); // } // }); // } @Override protected void releaseNode(int lineIndex) { super.releaseNode(lineIndex); } private Stream<LineNode> createVisibleLineNodesStream() { ContiguousSet<Integer> visibleIndexes = ContiguousSet.create(ContentView.this.visibleLines.get(), DiscreteDomain.integers()); // return visibleIndexes.stream().filter(i->i<model.size()).map(idx->get(model.get(idx))).filter(n->n.isPresent()).map(n->n.get()).filter(n->n.isVisible()); return visibleIndexes.stream().filter(i -> i.intValue() < getNumberOfLines()) .map(idx -> getVisibleNode(idx.intValue())).filter(n -> n.isPresent()).map(n -> n.get()); } Optional<Integer> getLineIndex(javafx.geometry.Point2D point) { Predicate<Node> filter = n -> { Bounds b = n.getBoundsInParent(); return b.getMinY() <= point.getY() && point.getY() <= b.getMaxY(); }; final Optional<LineNode> hitLine = createVisibleLineNodesStream().filter(filter).findFirst(); final Optional<Integer> index = hitLine.map(n -> { int i = n.getCaretIndexAtPoint(new javafx.geometry.Point2D(point.getX(), n.getHeight() / 2)); if (i >= 0) { return Integer.valueOf(n.getStartOffset() + i); } else if (point.getX() > 0) { return Integer.valueOf(n.getEndOffset()); } else if (point.getX() < 0) { return Integer.valueOf(n.getStartOffset()); } else { return Integer.valueOf(-1); } }); return index; } public List<HoverTarget> findHoverTargets(Point2D localLocation) { ContiguousSet<Integer> visibleIndexes = ContiguousSet.create(ContentView.this.visibleLines.get(), DiscreteDomain.integers()); return visibleIndexes.stream().map(lineIndex -> getVisibleNode(lineIndex)).filter(x -> x.isPresent()) .filter(x -> x.get().getBoundsInParent().contains(localLocation)) .flatMap(x -> x.get().findHoverTargets(x.get().parentToLocal(localLocation)).stream()) .collect(Collectors.toList()); } public Optional<TextNode> findTextNode(Point2D localLocation) { ContiguousSet<Integer> visibleLineIndexes = ContiguousSet.create(ContentView.this.visibleLines.get(), DiscreteDomain.integers()); return visibleLineIndexes.stream().map(lineIndex -> getVisibleNode(lineIndex)) .filter(x -> x.isPresent()).filter(x -> x.get().getBoundsInParent().contains(localLocation)) .findFirst().flatMap(x -> x.get().findTextNode(x.get().parentToLocal(localLocation))); } } public List<HoverTarget> findHoverTargets(Point2D localLocation) { return this.lineLayer.findHoverTargets(localLocation); } public Optional<TextNode> findTextNode(Point2D localLocation) { localLocation = this.lineLayer.sceneToLocal(this.localToScene(localLocation)); return this.lineLayer.findTextNode(localLocation); } private StackPane contentBody = new StackPane(); private final LineLayer lineLayer; // private Predicate<Set<LineNode>> needsPresentation; private Map<Integer, Double> yOffsetData = new HashMap<>(); // private boolean forceLayout = true; private DoubleProperty lineHeigth = new SimpleDoubleProperty(this, "lineHeight", 16.0); //$NON-NLS-1$ public DoubleProperty lineHeightProperty() { return this.lineHeigth; } public double getLineHeight() { return this.lineHeigth.get(); } public void setLineHeight(double lineHeight) { this.lineHeigth.set(lineHeight); } IntegerProperty numberOfLines = new SimpleIntegerProperty(this, "numberOfLines", 0); //$NON-NLS-1$ public ReadOnlyIntegerProperty numberOfLinesProperty() { return this.numberOfLines; } public int getNumberOfLines() { return this.numberOfLines.get(); } private IntegerProperty caretOffset = new SimpleIntegerProperty(this, "caretOffset", 0); //$NON-NLS-1$ public IntegerProperty caretOffsetProperty() { return this.caretOffset; } private ObjectProperty<TextSelection> textSelection = new SimpleObjectProperty<>(this, "textSelection", //$NON-NLS-1$ new TextSelection(0, 0)); public ObjectProperty<TextSelection> textSelectionProperty() { return this.textSelection; } private ObjectProperty<StyledTextContent> content = new SimpleObjectProperty<>(this, "content"); //$NON-NLS-1$ public ObjectProperty<StyledTextContent> contentProperty() { return this.content; } public StyledTextContent getContent() { return this.content.get(); } private LineHelper lineHelper; protected LineLayer getLineLayer() { return this.lineLayer; } protected LineHelper getLineHelper() { return this.lineHelper; } // private ObservableList<StyledTextLine> model; // private IntegerBinding modelSize; // public void setModel(ObservableList<StyledTextLine> model) { // this.model = model; // // //// model.addListener((InvalidationListener)(x)->prepareNodes(getVisibleLines())); // //// model.addListener((ListChangeListener<StyledTextLine>)(c)->{ //// RangeSet<Integer> updateNodes = TreeRangeSet.create(); //// //// while (c.next()) { //// if (c.wasPermutated()) { ////// for (int i = c.getFrom(); i < c.getTo(); i++) { ////// lineLayer.permutate(i, c.getPermutation(i)); ////// } ////// lineLayer.requestLayout(); //// } //// if (c.wasUpdated() || c.wasReplaced()) { //// updateNodes.add(Range.closedOpen(c.getFrom(), c.getTo())); //// //// //// } //// if (c.wasAdded()) { //// updateNodes.add(Range.closedOpen(c.getFrom(), model.size())); //// } //// if (c.wasRemoved()) { //// //// c.getRemoved().forEach(line->lineLayer.releaseNode(line)); //// //// updateNodes.add(Range.closedOpen(c.getFrom(), model.size())); //// } //// } //// //// updateNodesNow(updateNodes); //// }); //// //// modelSize = Bindings.size(model); //// //// modelSize.addListener((x, o, n)-> { //// int newSize = n.intValue(); //// int oldSize = o.intValue(); //// if (newSize < oldSize) { ////// for (int lineIdx = newSize; lineIdx < oldSize; lineIdx++) { ////// lineLayer.releaseNode(lineIdx); ////// } //// } //// }); // } // private ListProperty<StyledTextLine> model = new SimpleListProperty<>(this, "model", FXCollections.observableArrayList()); // public ListProperty<StyledTextLine> getModel() { // return this.model; // } ObjectProperty<Range<Integer>> visibleLines = new SimpleObjectProperty<>(this, "visibleLines", //$NON-NLS-1$ Range.closed(Integer.valueOf(0), Integer.valueOf(0))); public ObjectProperty<Range<Integer>> visibleLinesProperty() { return this.visibleLines; } public com.google.common.collect.Range<Integer> getVisibleLines() { return this.visibleLines.get(); } public void setVisibleLines(Range<Integer> visibleLines) { this.visibleLines.set(visibleLines); } private Range<Integer> curVisibleLines; private StyledTextArea area; private boolean skipCSSApply; public ContentView(LineHelper lineHelper, StyledTextArea area) { this.lineLayer = new LineLayer(() -> new LineNode(area.tabAvanceProperty()), (n, m) -> { n.caretLayerVisibleProperty().bind(area.focusedProperty()); n.setLineHelper(getLineHelper()); n.updateInsertionMarkerIndex(this.insertionMarkerIndex); n.update(this.textAnnotationPresenter.get()); }); this.area = area; this.lineHelper = lineHelper; // setStyle("-fx-border-color: green; -fx-border-width:2px; -fx-border-style: dashed;"); this.contentBody.setPadding(new Insets(0, 0, 0, 2)); this.contentBody.getChildren().setAll(this.lineLayer); // this.lineLayer.setStyle("-fx-border-color: orange; -fx-border-width:2px; -fx-border-style: solid;"); // this.contentBody.setStyle("-fx-border-color: blue; -fx-border-width:2px; -fx-border-style: dotted;"); this.getChildren().setAll(this.contentBody); // AnimationTimer t = new AnimationTimer() { // @Override // public void handle(long now) { // updatePulse(now); // } // }; // // visibleProperty().addListener((x, o, n)->{ // if (n) { // t.start(); // } // else { // t.stop(); // } // }); // // t.start(); // sceneProperty().addListener((x, o, n) -> { // if (n == null) { // t.stop(); // } // else { // t.start(); // } // }); setMinWidth(200); setMinHeight(200); this.visibleLines.addListener(this::onLineChange); this.offsetX.addListener(this::onLineChange); this.lineLayer.lineHeightProperty().bind(this.lineHeigth); this.lineLayer.yOffsetProperty().bind(this.offsetY); this.lineLayer.visibleLinesProperty().bind(this.visibleLines); this.lineLayer.numberOfLinesProperty().bind(this.numberOfLines); bindContentListener(); bindCaretListener(); bindSelectionListener(); initBindings(); this.charWidth.addListener(o -> this.cachedLongestLine = 0.0); } private DoubleBinding charWidth; private void initBindings() { DoubleBinding b0 = Util.createTextWidthBinding("M", this.area.fontProperty(), //$NON-NLS-1$ this.area.fontZoomFactorProperty()); this.charWidth = Bindings.createDoubleBinding(() -> Math.ceil(b0.get()), b0); } private void bindCaretListener() { this.caretOffset.addListener((x, o, n) -> { int oldCaretLine = getContent().getLineAtOffset(o.intValue()); int newCaretLine = getContent().getLineAtOffset(n.intValue()); RangeSet<Integer> toUpdate = TreeRangeSet.create(); toUpdate.add(Range.closed(Integer.valueOf(oldCaretLine), Integer.valueOf(oldCaretLine))); toUpdate.add(Range.closed(Integer.valueOf(newCaretLine), Integer.valueOf(newCaretLine))); updateNodesNow(toUpdate); }); } private Range<Integer> getAffectedLines(TextSelection selection) { int firstLine = getContent().getLineAtOffset(selection.offset); int lastLine = getContent().getLineAtOffset(selection.offset + selection.length); if (lastLine == -1) { lastLine = this.numberOfLines.get(); } return Range.closed(Integer.valueOf(firstLine), Integer.valueOf(Math.min(lastLine, this.numberOfLines.get()))); } // private static Range<Integer> toRange(TextSelection s) { // return Range.closedOpen(Integer.valueOf(s.offset), Integer.valueOf(s.offset + s.length)); // } // // private Range<Integer> toLineRange(Range<Integer> globalOffsetRange) { // int lower = getContent().getLineAtOffset(globalOffsetRange.lowerEndpoint().intValue()); // int upper = getContent().getLineAtOffset(globalOffsetRange.upperEndpoint().intValue()); // return Range.closed(Integer.valueOf(lower), Integer.valueOf(upper)); // } private void bindSelectionListener() { this.textSelection.addListener((x, o, n) -> { RangeSet<Integer> toUpdate = TreeRangeSet.create(); if (o != null) toUpdate.add(getAffectedLines(o)); if (n != null) toUpdate.add(getAffectedLines(n)); updateNodesNow(toUpdate); }); } private void bindContentListener() { this.content.addListener((x, o, n) -> { if (o != null) { o.removeTextChangeListener(this.textChangeListener); } if (n != null) { n.addTextChangeListener(this.textChangeListener); this.numberOfLines.set(n.getLineCount()); } }); StyledTextContent current = this.content.get(); if (current != null) { current.addTextChangeListener(this.textChangeListener); // set inital values this.numberOfLines.set(current.getLineCount()); } } private TextChangeListener textChangeListener = new TextChangeListener() { private Function<Integer, Integer> mapping; private RangeSet<Integer> toUpdate = TreeRangeSet.create(); private RangeSet<Integer> toRelease = TreeRangeSet.create(); @Override public void textSet(TextChangedEvent event) { if (!this.toUpdate.isEmpty()) { updateNodesNow(this.toUpdate); this.toUpdate.clear(); } // update number of lines ContentView.this.numberOfLines.set(getContent().getLineCount()); getLineLayer().requestLayout(); } private int computeFirstUnchangedLine(TextChangingEvent event) { int endOffset = event.offset + event.replaceCharCount; int endLineIndex = getContent().getLineAtOffset(endOffset); int endLineBegin = getContent().getOffsetAtLine(endLineIndex); // int endLineLength = ContentView.this.lineHelper.getLength(endLineIndex); int firstSafeLine; if (endLineBegin == event.offset) { // offset at beginning of line firstSafeLine = endLineIndex; } else { // offset in middle or at end of line firstSafeLine = endLineIndex + 1; } return firstSafeLine; } @Override public void textChanging(TextChangingEvent event) { final int changeBeginLine = getContent().getLineAtOffset(event.offset); // determine first unchanged line int firstUnchangedLine = computeFirstUnchangedLine(event); int deltaLines = event.newLineCount - event.replaceLineCount; if (deltaLines < 0) { this.toRelease.add(Range.closedOpen(Integer.valueOf(firstUnchangedLine + deltaLines), Integer.valueOf(firstUnchangedLine))); } // prepare permutation this.mapping = (idx) -> { if (idx.intValue() >= firstUnchangedLine) { return Integer.valueOf(idx.intValue() + deltaLines); } return idx; }; this.toUpdate.add(Range.closedOpen(Integer.valueOf(changeBeginLine), Integer.valueOf(firstUnchangedLine + deltaLines))); // At least update myself if (this.toUpdate.isEmpty()) { this.toUpdate.add(Range.closed(Integer.valueOf(changeBeginLine), Integer.valueOf(changeBeginLine))); } // // // simple insert // if (event.replaceCharCount == 0) { // if (event.newLineCount > 0) { // // int lineIndex = getContent().getLineAtOffset(event.offset); // int lineBegin = getContent().getOffsetAtLine(lineIndex); // int lineLength = lineHelper.getLength(lineIndex); // // int firstSafeLine; // Range<Integer> updateRange; // // if (lineBegin == event.offset) { // // at beginning of line // firstSafeLine = lineIndex; // updateRange = Range.closedOpen(lineIndex, lineIndex + event.newLineCount); // } // else if (lineBegin == event.offset + lineLength) { // // insert was at end of line // firstSafeLine= lineIndex + 1; // updateRange = Range.closedOpen(lineIndex, lineIndex + event.newLineCount); // } // else { // // insert was in middle of line // firstSafeLine = lineIndex + 2; // updateRange = Range.closedOpen(lineIndex, lineIndex + 1 + event.newLineCount); // } // // // prepare update // toUpdate.add(updateRange); // // // prepare permutation // this.mapping = (idx) -> { // if (idx >= firstSafeLine) { // return idx + event.newLineCount; // } // return idx; // }; // // } // } // int firstUnchangedLine = changeBeginLine + replaceLines; // // int newFirstUnchnagedLine = changeBeginLine + newLines; // // // // prepare updates // toUpdate.add(Range.closedOpen(changeBeginLine, changeBeginLine + newLines)); // // prepare permutation // this.mapping = (idx) -> { // if (idx >= firstUnchangedLine) { // return idx + firstUnchangedLine - newFirstUnchnagedLine; // } // return idx; // }; } @Override public void textChanged(TextChangedEvent event) { if (!this.toRelease.isEmpty()) { releaseNodesNow(this.toRelease); this.toRelease.clear(); } // execute permutation if (this.mapping != null) { getLineLayer().permutateNodes(this.mapping); this.mapping = null; } // execute updates if (!this.toUpdate.isEmpty()) { updateNodesNow(this.toUpdate); this.toUpdate.clear(); } // update number of lines ContentView.this.numberOfLines.set(getContent().getLineCount()); getLineLayer().requestLayout(); } }; // Timer t = new Timer(); // volatile boolean scheduled = false; // private void scheduleUpdate() { // if (true) return; // // try { // if (!scheduled) { // scheduled = true; // t.schedule(new TimerTask() { // @Override // public void run() { // Platform.runLater(()->doUpdate()); // scheduled = false; // } // }, 16); // } // } // catch (Exception e) { // e.printStackTrace(); // } // // } private void onLineChange(Observable o) { RangeSet<Integer> toUpdate = TreeRangeSet.create(); RangeSet<Integer> toRelease = TreeRangeSet.create(); double offsetY = offsetYProperty().get(); com.google.common.collect.Range<Integer> visibleLines = visibleLinesProperty().get(); ContiguousSet<Integer> set = ContiguousSet.create(visibleLines, DiscreteDomain.integers()); double lineHeight = lineHeightProperty().get(); // schedule visible line updates if (this.curVisibleLines == null) { toUpdate.add(visibleLines); } else { RangeSet<Integer> hiddenLines = TreeRangeSet.create(); hiddenLines.add(this.curVisibleLines); hiddenLines.remove(visibleLines); RangeSet<Integer> shownLines = TreeRangeSet.create(); shownLines.add(visibleLines); shownLines.remove(this.curVisibleLines); toUpdate.addAll(shownLines); toRelease.addAll(hiddenLines); } this.curVisibleLines = visibleLines; // store precomputed y data for (int index : set) { double y = index * lineHeight - offsetY; this.yOffsetData.put(Integer.valueOf(index), Double.valueOf(y)); // this.forceLayout = true; } releaseNodesNow(toRelease); updateNodesNow(toUpdate); this.lineLayer.requestLayout(); // scheduleUpdate(); } private double cachedLongestLine; private int lastContentLength; private double computeLongestLine() { if (this.cachedLongestLine != 0.0) { if (this.lastContentLength == getContent().getCharCount()) { return Math.max(this.cachedLongestLine, getWidth()); } } OptionalInt longestLine = IntStream.range(0, getNumberOfLines()) .map(index -> this.lineHelper.getLengthCountTabsAsChars(index)).max(); if (longestLine.isPresent()) { int lineLength = longestLine.getAsInt() + 2; this.cachedLongestLine = lineLength * getCharWidth(); this.lastContentLength = getContent().getCharCount(); return Math.max(getWidth(), this.cachedLongestLine); } else { this.cachedLongestLine = 0.0; } return getWidth(); } public double getCharWidth() { if (!this.skipCSSApply) { applyCss(); if (getParent() != null) { this.skipCSSApply = true; } } return this.charWidth.get(); } @Override protected void layoutChildren() { double scrollX = -this.offsetX.get(); if (Double.isNaN(scrollX)) { scrollX = 0.0; } this.contentBody.resizeRelocate(scrollX, 0, computeLongestLine(), getHeight()); } private DoubleProperty offsetY = new SimpleDoubleProperty(); private DoubleProperty offsetX = new SimpleDoubleProperty(); public void bindHorizontalScrollbar(ScrollBar bar) { bar.setMin(0); DoubleBinding max = this.contentBody.widthProperty().subtract(widthProperty()); DoubleBinding factor = this.contentBody.widthProperty().divide(max); // DoubleProperty santizedFactor = new SimpleDoubleProperty(); // santizedFactor.set( Double.isNaN(factor.get()) ? 1.0 : factor.get() ); // factor.addListener( (ob,ol,ne) -> { // santizedFactor.set( Double.isNaN(ne.doubleValue()) ? 1.0 : ne.doubleValue() ); // } ); maxValue = this.contentBody.widthProperty().divide(factor); visibleAmount = widthProperty().divide(factor); updateValue(maxValue.get(), bar.maxProperty()); updateValue(visibleAmount.get(), bar.visibleAmountProperty()); maxValue.addListener(o -> { updateValue(maxValue.get(), bar.maxProperty()); }); visibleAmount.addListener(o -> { updateValue(visibleAmount.get(), bar.visibleAmountProperty()); }); // bar.maxProperty().bind(maxValue); // bar.visibleAmountProperty().bind(visibleAmount); this.offsetX.bind(bar.valueProperty()); this.widthProperty().addListener((x, o, n) -> { if (!Double.isNaN(bar.getMax()) && !Double.isNaN(bar.getValue())) { bar.setValue(Math.max(0, Math.min(bar.getMax(), bar.getValue()))); } }); } private void updateValue(double v, DoubleProperty p) { if (Double.isNaN(v)) { p.set(0); } else { p.set(v); } } public void bindVerticalScrollbar(ScrollBar bar) { this.heightProperty().addListener((x, o, n) -> { if (!Double.isNaN(bar.getMax()) && !Double.isNaN(bar.getValue())) { bar.setValue(Math.max(0, Math.min(bar.getMax(), bar.getValue()))); } }); } public DoubleProperty offsetYProperty() { return this.offsetY; } // private RangeSet<Integer> toRelease = TreeRangeSet.create(); // private RangeSet<Integer> toUpdate = TreeRangeSet.create(); public void updatelines(com.google.common.collect.RangeSet<Integer> rs) { updateNodesNow(rs); } private int insertionMarkerIndex = -1; private DoubleBinding maxValue; private DoubleBinding visibleAmount; public void updateInsertionMarkerIndex(int index) { if (this.insertionMarkerIndex != index) { this.insertionMarkerIndex = index; } com.google.common.collect.RangeSet<Integer> rs = TreeRangeSet.create(); updateNodesNow(rs.complement()); } void updateNodesNow(com.google.common.collect.RangeSet<Integer> rs) { RangeSet<Integer> subRangeSet = rs.subRangeSet(getVisibleLines()) .subRangeSet(Range.closedOpen(Integer.valueOf(0), Integer.valueOf(getNumberOfLines()))); subRangeSet.asRanges().forEach(r -> { ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index -> { getLineLayer().updateNode(index.intValue()); // StyledTextLine m = this.model.get(index); // lineLayer.updateNode(m); }); }); } void releaseNodesNow(com.google.common.collect.RangeSet<Integer> rs) { RangeSet<Integer> subRangeSet = rs .subRangeSet(Range.closedOpen(Integer.valueOf(0), Integer.valueOf(getNumberOfLines()))); subRangeSet.asRanges().forEach(r -> { ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index -> { getLineLayer().releaseNode(index.intValue()); // StyledTextLine m = this.model.get(index); // lineLayer.releaseNode(m); }); }); } // private void updateNodes(com.google.common.collect.Range<Integer> range) { // toUpdate.add(range); // scheduleUpdate(); //// //// if (range.intersects(getVisibleLines())) { //// Range intersection = range.intersect(getVisibleLines()); //// for (int index = intersection.getOffset(); index <intersection.getEndOffset(); index++) { //// toUpdate.add(index); ////// StyledTextLine m = this.model.get(index); ////// prepareNode(index, m); //// } //// } // } // private boolean doUpdate() { // try { // long now = -System.nanoTime(); // if (!toRelease.isEmpty()) { // toRelease.asRanges().forEach(r-> { // ContiguousSet.create(r, DiscreteDomain.integers()).forEach(this.lineLayer::releaseNode); // }); // toRelease.clear(); // } // // if (!toUpdate.isEmpty()) { // toUpdate.subRangeSet(getVisibleLines()).subRangeSet(Range.closedOpen(0, this.model.size())).asRanges().forEach(r-> { // ContiguousSet.create(r, DiscreteDomain.integers()).forEach(index-> { // StyledTextLine m = this.model.get(index); // lineLayer.updateNode(index, m); // }); // }); // toUpdate.clear(); // } // // // now += System.nanoTime(); // // if (now > 1000_000 * 5) { // } // // if (!toRelease.isEmpty() || !toUpdate.isEmpty() || forceLayout) { // // lineLayer.requestLayout(); // // forceLayout = false; // // return true; // } // } // catch (Exception e) { // e.printStackTrace(); // } // return false; // // } // // private void updatePulse(long now) { // doUpdate(); // } public Optional<Point2D> getLocationInScene(int globalOffset, LineLocation locationHint) { applyCss(); layout(); int lineIndex = getContent().getLineAtOffset(globalOffset); Optional<LineNode> node = this.lineLayer.getVisibleNode(lineIndex); return node.map(n -> { double x = n.getCharLocation(globalOffset - n.getStartOffset()); double y = 0; switch (locationHint) { case BELOW: y = 0; break; case ABOVE: y = -getLineHeight(); break; case CENTER: y = -getLineHeight() / 2.0; break; } Point2D p = new Point2D(x, y); return n.localToScene(p); }); } public Optional<Integer> getLineIndex(Point2D point) { // transform point to respect horizontal scrolling Point2D p = this.lineLayer.sceneToLocal(this.localToScene(point)); Optional<Integer> result = this.lineLayer.getLineIndex(p); return result; } public void updateAnnotations(RangeSet<Integer> r) { updateNodesNow(r); } }