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

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

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

import static org.fxmisc.richtext.TwoDimensional.Bias.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import javafx.scene.control.IndexRange;

import org.fxmisc.richtext.TwoDimensional.Position;

public final class Paragraph implements CharSequence {

    @SafeVarargs
    private static  List list(T head, T... tail) {
        if(tail.length == 0) {
            return Collections.singletonList(head);
        } else {
            ArrayList list = new ArrayList<>(1 + tail.length);
            list.add(head);
            for(T t: tail) list.add(t);
            return list;
        }
    }

    private final List> segments;
    private final TwoLevelNavigator navigator;

    public Paragraph(String text, S style) {
        this(new StyledText(text, style));
    }

    @SafeVarargs
    public Paragraph(StyledText text, StyledText... texts) {
        this(list(text, texts));
    }

    private Paragraph(StyledText text) {
        this(Arrays.asList(text));
    }

    Paragraph(List> segments) {
        assert !segments.isEmpty();
        this.segments = segments;
        navigator = new TwoLevelNavigator(
                () -> segments.size(),
                i -> segments.get(i).length());
    }

    public List> getSegments() {
        return Collections.unmodifiableList(segments);
    }

    private int length = -1;
    @Override
    public int length() {
        if(length == -1) {
            length = segments.stream().mapToInt(s -> s.length()).sum();
        }
        return length;
    }

    @Override
    public char charAt(int index) {
        Position pos = navigator.offsetToPosition(index, Forward);
        return segments.get(pos.getMajor()).charAt(pos.getMinor());
    }

    public String substring(int from, int to) {
        return toString().substring(from, Math.min(to, length()));
    }

    public String substring(int from) {
        return toString().substring(from);
    }

    public Paragraph concat(Paragraph p) {
        if(length() == 0) {
            return p;
        }

        if(p.length() == 0) {
            return this;
        }

        StyledText left = segments.get(segments.size()-1);
        StyledText right = p.segments.get(0);
        if(Objects.equals(left.getStyle(), right.getStyle())) {
            StyledText segment = left.concat(right);
            List> segs = new ArrayList<>(segments.size() + p.segments.size() - 1);
            segs.addAll(segments.subList(0, segments.size()-1));
            segs.add(segment);
            segs.addAll(p.segments.subList(1, p.segments.size()));
            return new Paragraph(segs);
        } else {
            List> segs = new ArrayList<>(segments.size() + p.segments.size());
            segs.addAll(segments);
            segs.addAll(p.segments);
            return new Paragraph(segs);
        }
    }

    public Paragraph concat(CharSequence str) {
        if(str.length() == 0) {
            return this;
        }

        List> segs = new ArrayList<>(segments);
        int lastIdx = segments.size() - 1;
        segs.set(lastIdx, segments.get(lastIdx).concat(str));
        return new Paragraph(segs);
    }

    public Paragraph insert(int offset, CharSequence str) {
        if(offset < 0 || offset > length()) {
            throw new IndexOutOfBoundsException(String.valueOf(offset));
        }

        Position pos = navigator.offsetToPosition(offset, Backward);
        int segIdx = pos.getMajor();
        int segPos = pos.getMinor();
        StyledText seg = segments.get(segIdx);
        StyledText replacement = seg.spliced(segPos, segPos, str);
        List> segs = new ArrayList<>(segments);
        segs.set(segIdx, replacement);
        return new Paragraph(segs);
    }

    @Override
    public Paragraph subSequence(int start, int end) {
        return trim(end).subSequence(start);
    }

    public Paragraph trim(int length) {
        if(length >= length()) {
            return this;
        } else {
            Position pos = navigator.offsetToPosition(length, Backward);
            int segIdx = pos.getMajor();
            List> segs = new ArrayList<>(segIdx + 1);
            segs.addAll(segments.subList(0, segIdx));
            segs.add(segments.get(segIdx).subSequence(0, pos.getMinor()));
            return new Paragraph(segs);
        }
    }

    public Paragraph subSequence(int start) {
        if(start < 0) {
            throw new IllegalArgumentException("start must not be negative (was: " + start + ")");
        } else if(start == 0) {
            return this;
        } else if(start <= length()) {
            Position pos = navigator.offsetToPosition(start, Forward);
            int segIdx = pos.getMajor();
            List> segs = new ArrayList<>(segments.size() - segIdx);
            segs.add(segments.get(segIdx).subSequence(pos.getMinor()));
            segs.addAll(segments.subList(segIdx + 1, segments.size()));
            return new Paragraph(segs);
        } else {
            throw new IndexOutOfBoundsException(start + " not in [0, " + length() + "]");
        }
    }

    public Paragraph delete(int start, int end) {
        return trim(start).concat(subSequence(end));
    }

    public Paragraph restyle(S style) {
        return new Paragraph(toString(), style);
    }

    public Paragraph restyle(int from, int to, S style) {
        if(from >= length()) {
            return this;
        } else {
            to = Math.min(to, length());
            Paragraph left = subSequence(0, from);
            Paragraph middle = new Paragraph(substring(from, to), style);
            Paragraph right = subSequence(to);
            return left.concat(middle).concat(right);
        }
    }

    public Paragraph restyle(int from, StyleSpans styleSpans) {
        int len = styleSpans.length();
        if(styleSpans.equals(getStyleSpans(from, from + len))) {
            return this;
        }

        Paragraph left = trim(from);
        Paragraph right = subSequence(from + len);

        String middleString = substring(from, from + len);
        List> middleSegs = new ArrayList<>(styleSpans.getSpanCount());
        int offset = 0;
        for(StyleSpan span: styleSpans) {
            int end = offset + span.getLength();
            String text = middleString.substring(offset, end);
            middleSegs.add(new StyledText<>(text, span.getStyle()));
            offset = end;
        }
        Paragraph middle = new Paragraph<>(middleSegs);

        return left.concat(middle).concat(right);
    }

    /**
     * Returns the style of character with the given index.
     * If {@code charIdx < 0}, returns the style at the beginning of this paragraph.
     * If {@code charIdx >= this.length()}, returns the style at the end of this paragraph.
     */
    public S getStyleOfChar(int charIdx) {
        if(charIdx < 0) {
            return segments.get(0).getStyle();
        }

        Position pos = navigator.offsetToPosition(charIdx, Forward);
        return segments.get(pos.getMajor()).getStyle();
    }

    /**
     * Returns the style at the given position. That is the style of the
     * character immediately preceding {@code position}. If {@code position}
     * is 0, then the style of the first character (index 0) in this paragraph
     * is returned. If this paragraph is empty, then some style previously used
     * in this paragraph is returned.
     * If {@code position > this.length()}, then it is equivalent to
     * {@code position == this.length()}.
     *
     * 

In other words, {@code getStyleAtPosition(p)} is equivalent to * {@code getStyleOfChar(p-1)}. */ public S getStyleAtPosition(int position) { if(position < 0) { throw new IllegalArgumentException("Paragraph position cannot be negative (" + position + ")"); } Position pos = navigator.offsetToPosition(position, Backward); return segments.get(pos.getMajor()).getStyle(); } /** * Returns the range of homogeneous style that includes the given position. * If {@code position} points to a boundary between two styled ranges, * then the range preceding {@code position} is returned. */ public IndexRange getStyleRangeAtPosition(int position) { Position pos = navigator.offsetToPosition(position, Backward); int start = position - pos.getMinor(); int end = start + segments.get(pos.getMajor()).length(); return new IndexRange(start, end); } public StyleSpans getStyleSpans() { StyleSpansBuilder builder = new StyleSpansBuilder<>(segments.size()); for(StyledText seg: segments) { builder.add(seg.getStyle(), seg.length()); } return builder.create(); } public StyleSpans getStyleSpans(int from, int to) { Position start = navigator.offsetToPosition(from, Forward); Position end = to == from ? start : start.offsetBy(to - from, Backward); int startSegIdx = start.getMajor(); int endSegIdx = end.getMajor(); int n = endSegIdx - startSegIdx + 1; StyleSpansBuilder builder = new StyleSpansBuilder<>(n); if(startSegIdx == endSegIdx) { StyledText seg = segments.get(startSegIdx); builder.add(seg.getStyle(), to - from); } else { StyledText startSeg = segments.get(startSegIdx); builder.add(startSeg.getStyle(), startSeg.length() - start.getMinor()); for(int i = startSegIdx + 1; i < endSegIdx; ++i) { StyledText seg = segments.get(i); builder.add(seg.getStyle(), seg.length()); } StyledText endSeg = segments.get(endSegIdx); builder.add(endSeg.getStyle(), end.getMinor()); } return builder.create(); } private String text = null; /** * Returns the string content of this paragraph, * excluding the line terminator. */ @Override public String toString() { if(text == null) { StringBuilder sb = new StringBuilder(length()); for(StyledText seg: segments) sb.append(seg); text = sb.toString(); } return text; } @Override public boolean equals(Object other) { if(other instanceof Paragraph) { Paragraph that = (Paragraph) other; return Objects.equals(this.segments, that.segments); } else { return false; } } @Override public int hashCode() { return segments.hashCode(); } }