diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/Hyperlink.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/Hyperlink.java index b9aaf5454..450ab351f 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/Hyperlink.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/Hyperlink.java @@ -1,16 +1,14 @@ package org.fxmisc.richtext.demo.hyperlink; -public class Hyperlink { +public class Hyperlink { private final String originalDisplayedText; private final String displayedText; - private final S style; private final String link; - Hyperlink(String originalDisplayedText, String displayedText, S style, String link) { + Hyperlink(String originalDisplayedText, String displayedText, String link) { this.originalDisplayedText = originalDisplayedText; this.displayedText = displayedText; - this.style = style; this.link = link; } @@ -22,7 +20,7 @@ public boolean isReal() { return length() > 0; } - public boolean shareSameAncestor(Hyperlink other) { + public boolean shareSameAncestor(Hyperlink other) { return link.equals(other.link) && originalDisplayedText.equals(other.originalDisplayedText); } @@ -44,32 +42,24 @@ public String getLink() { return link; } - public Hyperlink subSequence(int start, int end) { - return new Hyperlink<>(originalDisplayedText, displayedText.substring(start, end), style, link); + public Hyperlink subSequence(int start, int end) { + return new Hyperlink(originalDisplayedText, displayedText.substring(start, end), link); } - public Hyperlink subSequence(int start) { - return new Hyperlink<>(originalDisplayedText, displayedText.substring(start), style, link); + public Hyperlink subSequence(int start) { + return new Hyperlink(originalDisplayedText, displayedText.substring(start), link); } - public S getStyle() { - return style; - } - - public Hyperlink setStyle(S style) { - return new Hyperlink<>(originalDisplayedText, displayedText, style, link); - } - - public Hyperlink mapDisplayedText(String text) { - return new Hyperlink<>(originalDisplayedText, text, style, link); + public Hyperlink mapDisplayedText(String text) { + return new Hyperlink(originalDisplayedText, text, link); } @Override public String toString() { return isEmpty() - ? String.format("EmptyHyperlink[original=%s style=%s link=%s]", originalDisplayedText, style, link) - : String.format("RealHyperlink[original=%s displayedText=%s, style=%s, link=%s]", - originalDisplayedText, displayedText, style, link); + ? String.format("EmptyHyperlink[original=%s link=%s]", originalDisplayedText, link) + : String.format("RealHyperlink[original=%s displayedText=%s, link=%s]", + originalDisplayedText, displayedText, link); } } diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/HyperlinkDemo.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/HyperlinkDemo.java index e49dbf7d4..81294bf70 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/HyperlinkDemo.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/HyperlinkDemo.java @@ -1,9 +1,7 @@ package org.fxmisc.richtext.demo.hyperlink; import com.sun.deploy.uitoolkit.impl.fx.HostServicesFactory; -import com.sun.javafx.application.HostServicesDelegate; import javafx.application.Application; -import javafx.application.HostServices; import javafx.scene.Scene; import javafx.stage.Stage; import org.fxmisc.flowless.VirtualizedScrollPane; diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/HyperlinkOps.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/HyperlinkOps.java index 954f90883..e07f753f3 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/HyperlinkOps.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/HyperlinkOps.java @@ -1,48 +1,42 @@ package org.fxmisc.richtext.demo.hyperlink; -import org.fxmisc.richtext.model.SegmentOps; +import org.fxmisc.richtext.model.SegmentOpsBase; import java.util.Optional; -public class HyperlinkOps implements SegmentOps, S> { +public class HyperlinkOps extends SegmentOpsBase { + + public HyperlinkOps() { + super(new Hyperlink("", "", "")); + } @Override - public int length(Hyperlink hyperlink) { + public int length(Hyperlink hyperlink) { return hyperlink.length(); } @Override - public char charAt(Hyperlink hyperlink, int index) { + public char realCharAt(Hyperlink hyperlink, int index) { return hyperlink.charAt(index); } @Override - public String getText(Hyperlink hyperlink) { + public String realGetText(Hyperlink hyperlink) { return hyperlink.getDisplayedText(); } @Override - public Hyperlink subSequence(Hyperlink hyperlink, int start, int end) { + public Hyperlink realSubSequence(Hyperlink hyperlink, int start, int end) { return hyperlink.subSequence(start, end); } @Override - public Hyperlink subSequence(Hyperlink hyperlink, int start) { + public Hyperlink realSubSequence(Hyperlink hyperlink, int start) { return hyperlink.subSequence(start); } @Override - public S getStyle(Hyperlink hyperlink) { - return hyperlink.getStyle(); - } - - @Override - public Hyperlink setStyle(Hyperlink hyperlink, S style) { - return hyperlink.setStyle(style); - } - - @Override - public Optional> join(Hyperlink currentSeg, Hyperlink nextSeg) { + public Optional joinSeg(Hyperlink currentSeg, Hyperlink nextSeg) { if (currentSeg.isEmpty()) { if (nextSeg.isEmpty()) { return Optional.empty(); @@ -58,7 +52,7 @@ public Optional> join(Hyperlink currentSeg, Hyperlink nextSeg } } - private Optional> concatHyperlinks(Hyperlink leftSeg, Hyperlink rightSeg) { + private Optional concatHyperlinks(Hyperlink leftSeg, Hyperlink rightSeg) { if (!leftSeg.shareSameAncestor(rightSeg)) { return Optional.empty(); } @@ -90,8 +84,4 @@ private Optional> concatHyperlinks(Hyperlink leftSeg, Hyperlink< } } - @Override - public Hyperlink createEmpty() { - return new Hyperlink<>("", "", null, ""); - } } diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/TextHyperlinkArea.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/TextHyperlinkArea.java index 77b6e6473..b763ce249 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/TextHyperlinkArea.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/hyperlink/TextHyperlinkArea.java @@ -4,17 +4,19 @@ import org.fxmisc.richtext.GenericStyledArea; import org.fxmisc.richtext.TextExt; import org.fxmisc.richtext.model.ReadOnlyStyledDocument; -import org.fxmisc.richtext.model.StyledText; +import org.fxmisc.richtext.model.SegmentOps; import org.fxmisc.richtext.model.TextOps; import org.reactfx.util.Either; +import java.util.Optional; import java.util.function.Consumer; -public class TextHyperlinkArea extends GenericStyledArea, Hyperlink>, TextStyle> { +public class TextHyperlinkArea extends GenericStyledArea, TextStyle> { - private static final TextOps, TextStyle> STYLED_TEXT_OPS = StyledText.textOps(); + private static final TextOps STYLED_TEXT_OPS = SegmentOps.styledTextOps(); private static final HyperlinkOps HYPERLINK_OPS = new HyperlinkOps<>(); - private static final TextOps, Hyperlink>, TextStyle> EITHER_OPS = STYLED_TEXT_OPS._or(HYPERLINK_OPS); + + private static final TextOps, TextStyle> EITHER_OPS = STYLED_TEXT_OPS._or(HYPERLINK_OPS, (s1, s2) -> Optional.empty()); public TextHyperlinkArea(Consumer showLink) { super( @@ -22,11 +24,11 @@ public TextHyperlinkArea(Consumer showLink) { (t, p) -> {}, TextStyle.EMPTY, EITHER_OPS, - e -> e.unify( - styledText -> + e -> e.getSegment().unify( + text -> createStyledTextNode(t -> { - t.setText(styledText.getText()); - t.setStyle(styledText.getStyle().toCss()); + t.setText(text); + t.setStyle(e.getStyle().toCss()); }), hyperlink -> createStyledTextNode(t -> { @@ -49,7 +51,7 @@ public void appendWithLink(String displayedText, String link) { public void replaceWithLink(int start, int end, String displayedText, String link) { replace(start, end, ReadOnlyStyledDocument.fromSegment( - Either.right(new Hyperlink<>(displayedText, displayedText, TextStyle.EMPTY, link)), + Either.right(new Hyperlink(displayedText, displayedText, link)), null, TextStyle.EMPTY, EITHER_OPS diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/EmptyLinkedImage.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/EmptyLinkedImage.java index 3403cda0a..fc46ef2b1 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/EmptyLinkedImage.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/EmptyLinkedImage.java @@ -2,16 +2,11 @@ import javafx.scene.Node; -public class EmptyLinkedImage implements LinkedImage { +public class EmptyLinkedImage implements LinkedImage { @Override - public LinkedImage setStyle(S style) { - return this; - } - - @Override - public S getStyle() { - return null; + public boolean isReal() { + return false; } @Override diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImage.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImage.java index f5857cbc7..88b41f39c 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImage.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImage.java @@ -7,41 +7,39 @@ import java.io.DataOutputStream; import java.io.IOException; -public interface LinkedImage { - static Codec> codec(Codec styleCodec) { - return new Codec>() { - +public interface LinkedImage { + static Codec codec() { + return new Codec() { @Override public String getName() { - return "LinkedImage<" + styleCodec.getName() + ">"; + return "LinkedImage"; } @Override - public void encode(DataOutputStream os, LinkedImage i) throws IOException { - // don't encode EmptyLinkedImage objects - if (i.getStyle() != null) { - // external path rep should use forward slashes only - String externalPath = i.getImagePath().replace("\\", "/"); + public void encode(DataOutputStream os, LinkedImage linkedImage) throws IOException { + if (linkedImage.isReal()) { + os.writeBoolean(true); + String externalPath = linkedImage.getImagePath().replace("\\", "/"); Codec.STRING_CODEC.encode(os, externalPath); - styleCodec.encode(os, i.getStyle()); + } else { + os.writeBoolean(false); } } @Override - public RealLinkedImage decode(DataInputStream is) throws IOException { - // Sanitize path - make sure that forward slashes only are used - String imagePath = Codec.STRING_CODEC.decode(is); - imagePath = imagePath.replace("\\", "/"); - S style = styleCodec.decode(is); - return new RealLinkedImage<>(imagePath, style); + public LinkedImage decode(DataInputStream is) throws IOException { + if (is.readBoolean()) { + String imagePath = Codec.STRING_CODEC.decode(is); + imagePath = imagePath.replace("\\", "/"); + return new RealLinkedImage(imagePath); + } else { + return new EmptyLinkedImage(); + } } - }; } - LinkedImage setStyle(S style); - - S getStyle(); + boolean isReal(); /** * @return The path of the image to render. diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImageOps.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImageOps.java index 3a31a9d05..4b726b9c2 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImageOps.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImageOps.java @@ -2,20 +2,16 @@ import org.fxmisc.richtext.model.NodeSegmentOpsBase; -public class LinkedImageOps extends NodeSegmentOpsBase, S> { - public LinkedImageOps() { - super(new EmptyLinkedImage<>()); - } +public class LinkedImageOps extends NodeSegmentOpsBase { - @Override - public S realGetStyle(LinkedImage linkedImage) { - return linkedImage.getStyle(); + public LinkedImageOps() { + super(new EmptyLinkedImage()); } @Override - public LinkedImage realSetStyle(LinkedImage linkedImage, S style) { - return linkedImage.setStyle(style); + public int length(LinkedImage linkedImage) { + return linkedImage.isReal() ? 1 : 0; } } diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RealLinkedImage.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RealLinkedImage.java index 5b1d6fc75..8f197acce 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RealLinkedImage.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RealLinkedImage.java @@ -12,18 +12,16 @@ * When rendered in the rich text editor, the image is loaded from the * specified file. */ -public class RealLinkedImage implements LinkedImage { +public class RealLinkedImage implements LinkedImage { private final String imagePath; - private final S style; /** * Creates a new linked image object. * * @param imagePath The path to the image file. - * @param style The text style to apply to the corresponding segment. */ - public RealLinkedImage(String imagePath, S style) { + public RealLinkedImage(String imagePath) { // if the image is below the current working directory, // then store as relative path name. @@ -33,26 +31,18 @@ public RealLinkedImage(String imagePath, S style) { } this.imagePath = imagePath; - this.style = style; } @Override - public RealLinkedImage setStyle(S style) { - return new RealLinkedImage<>(imagePath, style); + public boolean isReal() { + return true; } - @Override public String getImagePath() { return imagePath; } - @Override - public S getStyle() { - return style; - } - - @Override public String toString() { return String.format("RealLinkedImage[path=%s]", imagePath); diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RichText.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RichText.java index 4c3a012d4..0c8b0d46f 100644 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RichText.java +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/RichText.java @@ -20,7 +20,6 @@ import java.util.function.Function; import javafx.application.Application; -import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.collections.FXCollections; import javafx.scene.Node; @@ -49,9 +48,10 @@ import org.fxmisc.richtext.model.Codec; import org.fxmisc.richtext.model.Paragraph; import org.fxmisc.richtext.model.ReadOnlyStyledDocument; +import org.fxmisc.richtext.model.SegmentOps; import org.fxmisc.richtext.model.StyleSpans; import org.fxmisc.richtext.model.StyledDocument; -import org.fxmisc.richtext.model.StyledText; +import org.fxmisc.richtext.model.StyledSegment; import org.fxmisc.richtext.model.TextOps; import org.reactfx.SuspendableNo; import org.reactfx.util.Either; @@ -66,22 +66,22 @@ public static void main(String[] args) { launch(args); } - private final TextOps, TextStyle> styledTextOps = StyledText.textOps(); + private final TextOps styledTextOps = SegmentOps.styledTextOps(); private final LinkedImageOps linkedImageOps = new LinkedImageOps<>(); - private final GenericStyledArea, LinkedImage>, TextStyle> area = + private final GenericStyledArea, TextStyle> area = new GenericStyledArea<>( ParStyle.EMPTY, // default paragraph style (paragraph, style) -> paragraph.setStyle(style.toCss()), // paragraph style setter TextStyle.EMPTY.updateFontSize(12).updateFontFamily("Serif").updateTextColor(Color.BLACK), // default segment style - styledTextOps._or(linkedImageOps), // segment operations + styledTextOps._or(linkedImageOps, (s1, s2) -> Optional.empty()), // segment operations seg -> createNode(seg, (text, style) -> text.setStyle(style.toCss()))); // Node creator and segment style setter { area.setWrapText(true); area.setStyleCodecs( ParStyle.CODEC, - Codec.eitherCodec(StyledText.codec(TextStyle.CODEC), LinkedImage.codec(TextStyle.CODEC))); + Codec.styledSegmentCodec(Codec.eitherCodec(Codec.STRING_CODEC, LinkedImage.codec()), TextStyle.CODEC)); } private Stage mainStage; @@ -184,7 +184,7 @@ protected boolean computeValue() { int startPar = area.offsetToPosition(selection.getStart(), Forward).getMajor(); int endPar = area.offsetToPosition(selection.getEnd(), Backward).getMajor(); - List,LinkedImage>, TextStyle>> pars = area.getParagraphs().subList(startPar, endPar + 1); + List, TextStyle>> pars = area.getParagraphs().subList(startPar, endPar + 1); @SuppressWarnings("unchecked") Optional[] alignments = pars.stream().map(p -> p.getParagraphStyle().alignment).distinct().toArray(Optional[]::new); @@ -272,7 +272,7 @@ protected boolean computeValue() { paragraphBackgroundPicker); panel2.getChildren().addAll(sizeCombo, familyCombo, textColorPicker, backgroundColorPicker); - VirtualizedScrollPane,LinkedImage>, TextStyle>> vsPane = new VirtualizedScrollPane<>(area); + VirtualizedScrollPane, TextStyle>> vsPane = new VirtualizedScrollPane<>(area); VBox vbox = new VBox(); VBox.setVgrow(vsPane, Priority.ALWAYS); vbox.getChildren().addAll(panel1, panel2, vsPane); @@ -286,13 +286,12 @@ protected boolean computeValue() { } - private Node createNode(Either, LinkedImage> seg, + private Node createNode(StyledSegment, TextStyle> seg, BiConsumer applyStyle) { - if (seg.isLeft()) { - return StyledTextArea.createStyledTextNode(seg.getLeft(), styledTextOps, applyStyle); - } else { - return seg.getRight().createNode(); - } + return seg.getSegment().unify( + text -> StyledTextArea.createStyledTextNode(text, seg.getStyle(), applyStyle), + LinkedImage::createNode + ); } @Deprecated @@ -374,14 +373,14 @@ private void loadDocument() { private void load(File file) { if(area.getStyleCodecs().isPresent()) { - Tuple2, Codec, LinkedImage>>> codecs = area.getStyleCodecs().get(); - Codec, LinkedImage>, TextStyle>> + Tuple2, Codec, TextStyle>>> codecs = area.getStyleCodecs().get(); + Codec, TextStyle>> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2, area.getSegOps()); try { FileInputStream fis = new FileInputStream(file); DataInputStream dis = new DataInputStream(fis); - StyledDocument, LinkedImage>, TextStyle> doc = codec.decode(dis); + StyledDocument, TextStyle> doc = codec.decode(dis); fis.close(); if(doc != null) { @@ -408,11 +407,11 @@ private void saveDocument() { private void save(File file) { - StyledDocument, LinkedImage>, TextStyle> doc = area.getDocument(); + StyledDocument, TextStyle> doc = area.getDocument(); // Use the Codec to save the document in a binary format area.getStyleCodecs().ifPresent(codecs -> { - Codec, LinkedImage>, TextStyle>> codec = + Codec, TextStyle>> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2, area.getSegOps()); try { FileOutputStream fos = new FileOutputStream(file); @@ -438,8 +437,8 @@ private void insertImage() { if (selectedFile != null) { String imagePath = selectedFile.getAbsolutePath(); imagePath = imagePath.replace('\\', '/'); - ReadOnlyStyledDocument, LinkedImage>, TextStyle> ros = - ReadOnlyStyledDocument.fromSegment(Either.right(new RealLinkedImage<>(imagePath, TextStyle.EMPTY)), + ReadOnlyStyledDocument, TextStyle> ros = + ReadOnlyStyledDocument.fromSegment(Either.right(new RealLinkedImage(imagePath)), ParStyle.EMPTY, TextStyle.EMPTY, area.getSegOps()); area.replaceSelection(ros); } @@ -469,7 +468,7 @@ private void updateParagraphStyleInSelection(Function update int startPar = area.offsetToPosition(selection.getStart(), Forward).getMajor(); int endPar = area.offsetToPosition(selection.getEnd(), Backward).getMajor(); for(int i = startPar; i <= endPar; ++i) { - Paragraph,LinkedImage>, TextStyle> paragraph = area.getParagraph(i); + Paragraph, TextStyle> paragraph = area.getParagraph(i); area.setParagraphStyle(i, updater.apply(paragraph.getParagraphStyle())); } } diff --git a/richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/ClipboardTests.java b/richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/ClipboardTests.java new file mode 100644 index 000000000..127ab3744 --- /dev/null +++ b/richtextfx/src/integrationTest/java/org/fxmisc/richtext/api/ClipboardTests.java @@ -0,0 +1,49 @@ +package org.fxmisc.richtext.api; + +import com.nitorcreations.junit.runners.NestedRunner; +import javafx.scene.Scene; +import javafx.stage.Stage; +import org.fxmisc.flowless.VirtualizedScrollPane; +import org.fxmisc.richtext.CodeArea; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.testfx.framework.junit.ApplicationTest; + +import static javafx.scene.input.KeyCode.*; + +@RunWith(NestedRunner.class) +public class ClipboardTests { + + public class CopyTests extends ApplicationTest { + + CodeArea area; + + @Override + public void start(Stage primaryStage) throws Exception { + area = new CodeArea("abc\ndef\nghi"); + VirtualizedScrollPane vsPane = new VirtualizedScrollPane<>(area); + + Scene scene = new Scene(vsPane, 400, 400); + primaryStage.setScene(scene); + primaryStage.show(); + } + + + public class WhenUserMakesSelectionEndingInNewLineCharacter { + + @Before + public void setup() { + area.selectRange(2, 4); + } + + @Test + public void copyingShouldNotThrowException() { + push(CONTROL, C); + + push(CONTROL, V); + } + } + } + +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java b/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java index da5428de8..d7a62ef47 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java @@ -19,6 +19,7 @@ import org.fxmisc.richtext.model.ReadOnlyStyledDocument; import org.fxmisc.richtext.model.SegmentOps; import org.fxmisc.richtext.model.StyledDocument; +import org.fxmisc.richtext.model.StyledSegment; import org.fxmisc.richtext.model.TextEditingArea; import org.reactfx.util.Tuple2; @@ -31,8 +32,8 @@ public interface ClipboardActions extends EditActions { * Gets codecs to encode/decode style information to/from binary format. * Providing codecs enables clipboard actions to retain the style information. */ - Optional, Codec>> getStyleCodecs(); - void setStyleCodecs(Codec paragraphStyleCodec, Codec textStyleCodec); + Optional, Codec>>> getStyleCodecs(); + void setStyleCodecs(Codec paragraphStyleCodec, Codec> textStyleCodec); SegmentOps getSegOps(); @@ -85,7 +86,7 @@ default void paste() { Clipboard clipboard = Clipboard.getSystemClipboard(); if(getStyleCodecs().isPresent()) { - Tuple2, Codec> codecs = getStyleCodecs().get(); + Tuple2, Codec>> codecs = getStyleCodecs().get(); Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2, getSegOps()); DataFormat format = dataFormat(codec.getName()); if(clipboard.hasContent(format)) { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/CodeArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/CodeArea.java index 1b80badde..c2ea4a45e 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/CodeArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/CodeArea.java @@ -5,7 +5,6 @@ import javafx.beans.NamedArg; import org.fxmisc.richtext.model.EditableStyledDocument; -import org.fxmisc.richtext.model.StyledText; /** * A convenience subclass of {@link StyleClassedTextArea} @@ -24,7 +23,7 @@ public class CodeArea extends StyleClassedTextArea { setUseInitialStyleForInsertion(true); } - public CodeArea(@NamedArg("document") EditableStyledDocument, StyledText>, Collection> document) { + public CodeArea(@NamedArg("document") EditableStyledDocument, String, Collection> document) { super(document, false); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index f1ee1e174..56344975f 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -33,7 +33,6 @@ import javafx.css.StyleConverter; import javafx.css.Styleable; import javafx.css.StyleableObjectProperty; -import javafx.css.StyleableProperty; import javafx.event.Event; import javafx.geometry.BoundingBox; import javafx.geometry.Bounds; @@ -69,6 +68,7 @@ import org.fxmisc.richtext.model.SegmentOps; import org.fxmisc.richtext.model.StyleSpans; import org.fxmisc.richtext.model.StyledDocument; +import org.fxmisc.richtext.model.StyledSegment; import org.fxmisc.richtext.model.TextEditingArea; import org.fxmisc.richtext.model.TextOps; import org.fxmisc.richtext.model.TwoDimensional; @@ -355,13 +355,13 @@ protected void invalidated() { @Override public boolean getUseInitialStyleForInsertion() { return useInitialStyleForInsertion.get(); } - private Optional, Codec>> styleCodecs = Optional.empty(); + private Optional, Codec>>> styleCodecs = Optional.empty(); @Override - public void setStyleCodecs(Codec paragraphStyleCodec, Codec textStyleCodec) { - styleCodecs = Optional.of(t(paragraphStyleCodec, textStyleCodec)); + public void setStyleCodecs(Codec paragraphStyleCodec, Codec> styledSegCodec) { + styleCodecs = Optional.of(t(paragraphStyleCodec, styledSegCodec)); } @Override - public Optional, Codec>> getStyleCodecs() { + public Optional, Codec>>> getStyleCodecs() { return styleCodecs; } @@ -580,7 +580,7 @@ public GenericStyledArea(@NamedArg("initialParagraphStyle") PS initialParagraphS @NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle, @NamedArg("initialTextStyle") S initialTextStyle, @NamedArg("segmentOps") TextOps segmentOps, - @NamedArg("nodeFactory") Function nodeFactory) { + @NamedArg("nodeFactory") Function, Node> nodeFactory) { this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, true, nodeFactory); } @@ -589,7 +589,7 @@ public GenericStyledArea(@NamedArg("initialParagraphStyle") PS initialParagraphS @NamedArg("initialTextStyle") S initialTextStyle, @NamedArg("segmentOps") TextOps segmentOps, @NamedArg("preserveStyle") boolean preserveStyle, - @NamedArg("nodeFactory") Function nodeFactory) { + @NamedArg("nodeFactory") Function, Node> nodeFactory) { this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, new GenericEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), segmentOps, preserveStyle, nodeFactory); } @@ -605,7 +605,7 @@ public GenericStyledArea( @NamedArg("initialTextStyle") S initialTextStyle, @NamedArg("document") EditableStyledDocument document, @NamedArg("segmentOps") TextOps segmentOps, - @NamedArg("nodeFactory") Function nodeFactory) { + @NamedArg("nodeFactory") Function, Node> nodeFactory) { this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, document, segmentOps, true, nodeFactory); } @@ -617,7 +617,7 @@ public GenericStyledArea( @NamedArg("document") EditableStyledDocument document, @NamedArg("segmentOps") TextOps segmentOps, @NamedArg("preserveStyle") boolean preserveStyle, - @NamedArg("nodeFactory") Function nodeFactory) { + @NamedArg("nodeFactory") Function, Node> nodeFactory) { this.initialTextStyle = initialTextStyle; this.initialParagraphStyle = initialParagraphStyle; this.preserveStyle = preserveStyle; @@ -1120,10 +1120,20 @@ public void setStyleSpans(int paragraph, int from, StyleSpans style public void setParagraphStyle(int paragraph, PS paragraphStyle) { content.setParagraphStyle(paragraph, paragraphStyle); } + @Override public void replaceText(int start, int end, String text) { StyledDocument doc = ReadOnlyStyledDocument.fromString( - text, getParagraphStyleForInsertionAt(start), getStyleForInsertionAt(start), segmentOps); + text, getParagraphStyleForInsertionAt(start), getTextStyleForInsertionAt(start), segmentOps + ); + replace(start, end, doc); + } + + @Override + public void replace(int start, int end, SEG seg, S style) { + StyledDocument doc = ReadOnlyStyledDocument.fromSegment( + seg, getParagraphStyleForInsertionAt(start), style, segmentOps + ); replace(start, end, doc); } @@ -1179,7 +1189,7 @@ protected void layoutChildren() { private Cell, ParagraphBox> createCell( Paragraph paragraph, BiConsumer applyParagraphStyle, - Function nodeFactory) { + Function, Node> nodeFactory) { ParagraphBox box = new ParagraphBox<>(paragraph, applyParagraphStyle, nodeFactory); @@ -1375,7 +1385,8 @@ private static Bounds extendLeft(Bounds b, double w) { } } - private S getStyleForInsertionAt(int pos) { + @Override + public final S getTextStyleForInsertionAt(int pos) { if(useInitialStyleForInsertion.get()) { return initialTextStyle; } else { @@ -1383,7 +1394,8 @@ private S getStyleForInsertionAt(int pos) { } } - private PS getParagraphStyleForInsertionAt(int pos) { + @Override + public final PS getParagraphStyleForInsertionAt(int pos) { if(useInitialStyleForInsertion.get()) { return initialParagraphStyle; } else { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java index cff8c9c10..8e787051a 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java @@ -7,7 +7,8 @@ import org.fxmisc.richtext.model.Codec; import org.fxmisc.richtext.model.EditableStyledDocument; import org.fxmisc.richtext.model.SimpleEditableStyledDocument; -import org.fxmisc.richtext.model.StyledText; + +import static org.fxmisc.richtext.model.Codec.styledTextCodec; /** * Text area that uses inline css to define style of text segments and paragraph segments. @@ -18,7 +19,7 @@ public InlineCssTextArea() { this(new SimpleEditableStyledDocument<>("", "")); } - public InlineCssTextArea(@NamedArg("document") EditableStyledDocument, String> document) { + public InlineCssTextArea(@NamedArg("document") EditableStyledDocument document) { super( "", TextFlow::setStyle, "", TextExt::setStyle, @@ -36,11 +37,11 @@ public InlineCssTextArea(@NamedArg("document") EditableStyledDocument> graphicFactoryProperty() { public int getIndex() { return index.getValue(); } ParagraphBox(Paragraph par, BiConsumer applyParagraphStyle, - Function nodeFactory) { + Function, Node> nodeFactory) { this.getStyleClass().add("paragraph-box"); this.text = new ParagraphText<>(par, nodeFactory); applyParagraphStyle.accept(this.text, par.getParagraphStyle()); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java index 2f0abe7a8..3edfce9f8 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java @@ -29,6 +29,7 @@ import javafx.scene.shape.StrokeType; import org.fxmisc.richtext.model.Paragraph; +import org.fxmisc.richtext.model.StyledSegment; import org.reactfx.util.Tuple2; import org.reactfx.util.Tuples; import org.reactfx.value.Val; @@ -70,7 +71,7 @@ public ObjectProperty highlightTextFillProperty() { caretShape.visibleProperty().bind(caretVisible); } - ParagraphText(Paragraph par, Function nodeFactory) { + ParagraphText(Paragraph par, Function, Node> nodeFactory) { this.paragraph = par; getStyleClass().add("paragraph-text"); @@ -111,11 +112,7 @@ public ObjectProperty highlightTextFillProperty() { // }); // populate with text nodes - for(SEG segment: par.getSegments()) { - // create Segment - Node fxNode = nodeFactory.apply(segment); - getChildren().add(fxNode); - } + par.getStyledSegments().stream().map(nodeFactory).forEach(getChildren()::add); // set up custom css shape helpers Supplier createShape = () -> { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java index 0cdaae22c..02d57a5ec 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java @@ -7,14 +7,13 @@ import org.fxmisc.richtext.model.Codec; import org.fxmisc.richtext.model.EditableStyledDocument; import org.fxmisc.richtext.model.SimpleEditableStyledDocument; -import org.fxmisc.richtext.model.StyledText; /** * Text area that uses style classes to define style of text segments and paragraph segments. */ public class StyleClassedTextArea extends StyledTextArea, Collection> { - public StyleClassedTextArea(@NamedArg("document") EditableStyledDocument, StyledText>, Collection> document, + public StyleClassedTextArea(@NamedArg("document") EditableStyledDocument, String, Collection> document, @NamedArg("preserveStyle") boolean preserveStyle) { super(Collections.emptyList(), (paragraph, styleClasses) -> paragraph.getStyleClass().addAll(styleClasses), @@ -25,7 +24,7 @@ public StyleClassedTextArea(@NamedArg("document") EditableStyledDocument type of style that can be applied to text. */ -public class StyledTextArea extends GenericStyledArea, S> { +public class StyledTextArea extends GenericStyledArea { public StyledTextArea(@NamedArg("initialParagraphStyle") PS initialParagraphStyle, @NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle, @NamedArg("initialTextStyle") S initialTextStyle, @NamedArg("applyStyle") BiConsumer applyStyle, - @NamedArg("document") EditableStyledDocument, S> document, + @NamedArg("document") EditableStyledDocument document, + @NamedArg("segmentOps") TextOps segmentOps, @NamedArg("preserveStyle") boolean preserveStyle) { super(initialParagraphStyle, applyParagraphStyle, - initialTextStyle, - document, StyledText.textOps(), preserveStyle, - seg -> createStyledTextNode(seg, StyledText.textOps(), applyStyle)); + initialTextStyle, document, segmentOps, preserveStyle, + seg -> createStyledTextNode(seg, applyStyle) + ); } public StyledTextArea(@NamedArg("initialParagraphStyle") PS initialParagraphStyle, @NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle, @NamedArg("initialTextStyle") S initialTextStyle, @NamedArg("applyStyle") BiConsumer applyStyle, - @NamedArg("document") EditableStyledDocument, S> document) { + @NamedArg("document") EditableStyledDocument document, + @NamedArg("preserveStyle") boolean preserveStyle) { + this(initialParagraphStyle, applyParagraphStyle, + initialTextStyle, applyStyle, + document, SegmentOps.styledTextOps(), preserveStyle); + } + + public StyledTextArea(@NamedArg("initialParagraphStyle") PS initialParagraphStyle, + @NamedArg("applyParagraphStyle") BiConsumer applyParagraphStyle, + @NamedArg("initialTextStyle") S initialTextStyle, + @NamedArg("applyStyle") BiConsumer applyStyle, + @NamedArg("document") EditableStyledDocument document) { this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, applyStyle, document, true); } @@ -105,13 +118,18 @@ public StyledTextArea(@NamedArg("initialParagraphStyle") PS initialParagraphStyl initialTextStyle, applyStyle, true); } - public static Node createStyledTextNode(StyledText seg, SegmentOps, S> segOps, - BiConsumer applyStyle) { + public static Node createStyledTextNode(StyledSegment seg, + BiConsumer applyStyle) { + return createStyledTextNode(seg.getSegment(), seg.getStyle(), applyStyle); + } + + public static Node createStyledTextNode(String text, S style, + BiConsumer applyStyle) { - TextExt t = new TextExt(segOps.getText(seg)); + TextExt t = new TextExt(text); t.setTextOrigin(VPos.TOP); t.getStyleClass().add("text"); - applyStyle.accept(t, segOps.getStyle(seg)); + applyStyle.accept(t, style); // XXX: binding selectionFill to textFill, // see the note at highlightTextFill diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/Codec.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/Codec.java index 48730f031..e132f6685 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Codec.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Codec.java @@ -63,6 +63,57 @@ public Color decode(DataInputStream is) throws IOException { }; + static Codec> styledSegmentCodec(Codec segCodec, Codec styleCodec) { + return new Codec>() { + @Override + public String getName() { + return "styled-segment<" + segCodec.getName() + ", " + styleCodec.getName() + ">"; + } + + @Override + public void encode(DataOutputStream os, StyledSegment styledSegment) throws IOException { + segCodec.encode(os, styledSegment.getSegment()); + styleCodec.encode(os, styledSegment.getStyle()); + } + + @Override + public StyledSegment decode(DataInputStream is) throws IOException { + SEG seg = segCodec.decode(is); + S style = styleCodec.decode(is); + return new StyledSegment<>(seg, style); + } + }; + } + + /** + * A codec which allows serialisation of this class to/from a data stream. + * + * Because S may be any type, you must pass a codec for it. If your style + * is String or Color, you can use {@link Codec#STRING_CODEC}/{@link Codec#COLOR_CODEC} respectively. + */ + public static Codec> styledTextCodec(Codec styleCodec) { + return new Codec>() { + @Override + public String getName() { + return "styled-text"; + } + + @Override + public void encode(DataOutputStream os, StyledSegment styledSeg) throws IOException { + Codec.STRING_CODEC.encode(os, styledSeg.getSegment()); + styleCodec.encode(os, styledSeg.getStyle()); + + } + + @Override + public StyledSegment decode(DataInputStream is) throws IOException { + String text = Codec.STRING_CODEC.decode(is); + S style = styleCodec.decode(is); + return new StyledSegment<>(text, style); + } + }; + } + static Codec> listCodec(Codec elemCodec) { return SuperCodec.collectionListCodec(elemCodec); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocument.java index cddc53323..3baa656aa 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocument.java @@ -2,7 +2,8 @@ /** * Provides a basic implementation of {@link EditableStyledDocument}. See {@link SimpleEditableStyledDocument} for - * a version that is specified for {@link StyledText}. + * a version that is specified for {@link String}. + * * @param type of style that can be applied to paragraphs (e.g. {@link javafx.scene.text.TextFlow}. * @param type of segment used in {@link Paragraph}. Can be only {@link org.fxmisc.richtext.TextExt text} * (plain or styled) or a type that combines text and other {@link javafx.scene.Node}s. @@ -11,7 +12,7 @@ public final class GenericEditableStyledDocument extends GenericEditableStyledDocumentBase implements EditableStyledDocument { - public GenericEditableStyledDocument(PS initialParagraphStyle, S initialStyle, TextOps segmentOps) { + public GenericEditableStyledDocument(PS initialParagraphStyle, S initialStyle, SegmentOps segmentOps) { super(initialParagraphStyle, initialStyle, segmentOps); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java index 7763ea2eb..d059b4f7f 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java @@ -86,7 +86,7 @@ public ReadOnlyStyledDocument snapshot() { @Override public final SuspendableNo beingUpdatedProperty() { return beingUpdated; } @Override public final boolean isBeingUpdated() { return beingUpdated.get(); } - GenericEditableStyledDocumentBase(Paragraph initialParagraph/*, SegmentOps segmentOps*/) { + GenericEditableStyledDocumentBase(Paragraph initialParagraph) { this.doc = new ReadOnlyStyledDocument<>(Collections.singletonList(initialParagraph)); final Suspendable omniSuspendable = Suspendable.combine( @@ -104,8 +104,8 @@ public ReadOnlyStyledDocument snapshot() { /** * Creates an empty {@link EditableStyledDocument} */ - public GenericEditableStyledDocumentBase(PS initialParagraphStyle, S initialStyle, TextOps segmentOps) { - this(new Paragraph<>(initialParagraphStyle, segmentOps, segmentOps.create("", initialStyle))); + public GenericEditableStyledDocumentBase(PS initialParagraphStyle, S initialStyle, SegmentOps segmentOps) { + this(new Paragraph<>(initialParagraphStyle, segmentOps, segmentOps.createEmptySeg(), initialStyle)); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/NodeSegmentOpsBase.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/NodeSegmentOpsBase.java index b9edb1f6d..af73710cb 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/NodeSegmentOpsBase.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/NodeSegmentOpsBase.java @@ -1,9 +1,11 @@ package org.fxmisc.richtext.model; +import java.util.Optional; + /** * Properly implements {@link SegmentOps} when implementing a non-text custom object (e.g. shape, circle, image) - * and reduces boilerplate. Developers only need to override {@link #realGetStyle(Object)} and - * {@link #realSetStyle(Object, Object)}. Developers may also want to override {@link #join(Object, Object)}. + * and reduces boilerplate. Developers may want to override {@link #joinSeg(Object, Object)} and + * {@link #joinStyle(Object, Object)}. * * @param type of segment * @param type of style @@ -14,11 +16,6 @@ public NodeSegmentOpsBase(SEG empty) { super(empty); } - @Override - public int realLength(SEG seg) { - return 1; - } - @Override public char realCharAt(SEG seg, int index) { return '\ufffc'; @@ -38,4 +35,9 @@ public SEG realSubSequence(SEG seg, int start, int end) { public SEG realSubSequence(SEG seg, int start) { return seg; } + + @Override + public Optional joinSeg(SEG currentSeg, SEG nextSeg) { + return Optional.empty(); + } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java index 8fa5c7126..9bd9caf33 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java @@ -5,6 +5,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; +import java.util.LinkedList; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -12,6 +13,8 @@ import javafx.scene.control.IndexRange; import org.fxmisc.richtext.model.TwoDimensional.Position; +import org.reactfx.util.Tuple2; +import org.reactfx.util.Tuples; /** * This is one Paragraph of the document. Depending on whether the text is wrapped, @@ -19,9 +22,7 @@ * contains of a list of SEG objects which make up the individual segments of the * Paragraph. By providing a specific segment object and an associated segment * operations object, all required data and the necessary operations on this data - * for a single segment can be provided. For example, {@link StyledText} is a segment - * type which is used in {@link org.fxmisc.richtext.StyledTextArea}, a text area which can render simple - * text only (which is already sufficient to implement all kinds of code editors). + * for a single segment can be provided. * *

For more complex requirements (for example, when images shall be part of the * document) a different segment type must be provided (which can make use of @@ -32,13 +33,25 @@ * one and returns a new Paragraph.

* * @param The type of the paragraph style. - * @param The type of the content segments in the paragraph (e.g. {@link StyledText}). + * @param The type of the content segments in the paragraph (e.g. {@link String}). * Every paragraph, even an empty paragraph, must have at least one SEG object * (even if that SEG object itself represents an empty segment). * @param The type of the style of individual segments. */ public final class Paragraph { + private static Tuple2, StyleSpans> decompose(List> list, + SegmentOps segmentOps) { + // TODO: optimize this so that join-able segments/styles are joined before returning tuple + List segs = new ArrayList<>(); + StyleSpansBuilder builder = new StyleSpansBuilder<>(); + for (StyledSegment styledSegment : list) { + segs.add(styledSegment.getSegment()); + builder.add(styledSegment.getStyle(), segmentOps.length(styledSegment.getSegment())); + } + return Tuples.t(segs, builder.create()); + } + @SafeVarargs private static List list(T head, T... tail) { if(tail.length == 0) { @@ -52,26 +65,94 @@ private static List list(T head, T... tail) { } private final List segments; + private final StyleSpans styles; private final TwoLevelNavigator navigator; private final PS paragraphStyle; private final SegmentOps segmentOps; - @SafeVarargs - public Paragraph(PS paragraphStyle, SegmentOps segmentOps, SEG text, SEG... texts) { - this(paragraphStyle, segmentOps, list(text, texts)); + public Paragraph(PS paragraphStyle, SegmentOps segmentOps, List> styledSegments) { + this(paragraphStyle, segmentOps, decompose(styledSegments, segmentOps)); + + } + + private Paragraph(PS paragraphStyle, SegmentOps segmentOps, Tuple2, StyleSpans> decomposedList) { + this(paragraphStyle, segmentOps, decomposedList._1, decomposedList._2); + } + + public Paragraph(PS paragraphStyle, SegmentOps segmentOps, SEG segment, S style) { + this(paragraphStyle, segmentOps, segment, + new StyleSpansBuilder().add(style, segmentOps.length(segment)).create() + ); } - Paragraph(PS paragraphStyle, SegmentOps segmentOps, List segments) { + public Paragraph(PS paragraphStyle, SegmentOps segmentOps, SEG segment, StyleSpans styles) { + this(paragraphStyle, segmentOps, Collections.singletonList(segment), styles); + } + + public Paragraph(PS paragraphStyle, SegmentOps segmentOps, List segments, StyleSpans styles) { if (segments.isEmpty()) { throw new IllegalArgumentException("Cannot construct a Paragraph with an empty list of segments"); } + if (styles.getSpanCount() == 0) { + throw new IllegalArgumentException( + "Cannot construct a Paragraph with StyleSpans object that contains no StyleSpan objects" + ); + } this.segmentOps = segmentOps; this.segments = segments; + this.styles = styles; this.paragraphStyle = paragraphStyle; navigator = new TwoLevelNavigator(segments::size, - i -> segmentOps.length(segments.get(i))); + i -> segmentOps.length(segments.get(i)) + ); + } + + public List> getStyledSegments() { + if (segments.size() == 1 && styles.getSpanCount() == 1) { + return Collections.singletonList( + new StyledSegment<>(segments.get(0), styles.getStyleSpan(0).getStyle()) + ); + } + + List> styledSegments = new LinkedList<>(); + Iterator segIterator = segments.iterator(); + Iterator> styleIterator = styles.iterator(); + SEG segCurrent = segIterator.next(); + StyleSpan styleCurrent = styleIterator.next(); + int segOffset = 0, styleOffset = 0; + boolean finished = false; + while (!finished) { + int segLength = segmentOps.length(segCurrent) - segOffset; + int styleLength = styleCurrent.getLength() - styleOffset; + + if (segLength < styleLength) { + SEG splitSeg = segmentOps.subSequence(segCurrent, segOffset); + styledSegments.add(new StyledSegment<>(splitSeg, styleCurrent.getStyle())); + segCurrent = segIterator.next(); + segOffset = 0; + styleOffset += segLength; + } else if (styleLength < segLength) { + SEG splitSeg = segmentOps.subSequence(segCurrent, segOffset, segOffset + styleLength); + styledSegments.add(new StyledSegment<>(splitSeg, styleCurrent.getStyle())); + styleCurrent = styleIterator.next(); + styleOffset = 0; + segOffset += styleLength; + } else { + SEG splitSeg = segmentOps.subSequence(segCurrent, segOffset, segOffset + styleLength); + styledSegments.add(new StyledSegment<>(splitSeg, styleCurrent.getStyle())); + if (segIterator.hasNext() && styleIterator.hasNext()) { + segCurrent = segIterator.next(); + segOffset = 0; + styleCurrent = styleIterator.next(); + styleOffset = 0; + } else { + finished = true; + } + } + } + return styledSegments; } public List getSegments() { @@ -118,22 +199,38 @@ public Paragraph concat(Paragraph p) { return p; } - SEG left = segments.get(segments.size() - 1); - SEG right = p.segments.get(0); - Optional joined = segmentOps.join(left, right); + List updatedSegs; + SEG leftSeg = segments.get(segments.size() - 1); + SEG rightSeg = p.segments.get(0); + Optional joined = segmentOps.joinSeg(leftSeg, rightSeg); if(joined.isPresent()) { SEG segment = joined.get(); - 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<>(paragraphStyle, segmentOps, segs); + updatedSegs = new ArrayList<>(segments.size() + p.segments.size() - 1); + updatedSegs.addAll(segments.subList(0, segments.size()-1)); + updatedSegs.add(segment); + updatedSegs.addAll(p.segments.subList(1, p.segments.size())); } else { - List segs = new ArrayList<>(segments.size() + p.segments.size()); - segs.addAll(segments); - segs.addAll(p.segments); - return new Paragraph<>(paragraphStyle, segmentOps, segs); + updatedSegs = new ArrayList<>(segments.size() + p.segments.size()); + updatedSegs.addAll(segments); + updatedSegs.addAll(p.segments); } + + StyleSpans updatedStyles; + StyleSpan leftSpan = styles.getStyleSpan(styles.getSpanCount() - 1); + StyleSpan rightSpan = p.styles.getStyleSpan(0); + Optional merge = segmentOps.joinStyle(leftSpan.getStyle(), rightSpan.getStyle()); + if (merge.isPresent()) { + int startOfMerge = styles.position(styles.getSpanCount() - 1, 0).toOffset(); + StyleSpans updatedLeftSpan = styles.subView(0, startOfMerge); + int endOfMerge = p.styles.position(1, 0).toOffset(); + StyleSpans updatedRightSpan = p.styles.subView(endOfMerge, p.styles.length()); + updatedStyles = updatedLeftSpan + .append(merge.get(), leftSpan.getLength() + rightSpan.getLength()) + .concat(updatedRightSpan); + } else { + updatedStyles = styles.concat(p.styles); + } + return new Paragraph<>(paragraphStyle, segmentOps, updatedSegs, updatedStyles); } /** @@ -160,9 +257,9 @@ public Paragraph trim(int length) { segs.addAll(segments.subList(0, segIdx)); segs.add(segmentOps.subSequence(segments.get(segIdx), 0, pos.getMinor())); if (segs.isEmpty()) { - segs.add(segmentOps.createEmpty()); + segs.add(segmentOps.createEmptySeg()); } - return new Paragraph<>(paragraphStyle, segmentOps, segs); + return new Paragraph<>(paragraphStyle, segmentOps, segs, styles.subView(0, length)); } } @@ -174,7 +271,7 @@ public Paragraph subSequence(int start) { } else if (start == length()) { // in case one is using EitherOps, force the empty segment // to use the left ops' default empty seg, not the right one's empty seg - return new Paragraph<>(paragraphStyle, segmentOps, segmentOps.createEmpty()); + return new Paragraph<>(paragraphStyle, segmentOps, segmentOps.createEmptySeg(), styles.subView(0, 0)); } else if(start < length()) { Position pos = navigator.offsetToPosition(start, Forward); int segIdx = pos.getMajor(); @@ -182,9 +279,9 @@ public Paragraph subSequence(int start) { segs.add(segmentOps.subSequence(segments.get(segIdx), pos.getMinor())); segs.addAll(segments.subList(segIdx + 1, segments.size())); if (segs.isEmpty()) { - segs.add(segmentOps.createEmpty()); + segs.add(segmentOps.createEmptySeg()); } - return new Paragraph<>(paragraphStyle, segmentOps, segs); + return new Paragraph<>(paragraphStyle, segmentOps, segs, styles.subView(start, styles.length())); } else { throw new IndexOutOfBoundsException(start + " not in [0, " + length() + "]"); } @@ -204,31 +301,18 @@ public Paragraph delete(int start, int end) { * @return The new paragraph with the restyled segments. */ public Paragraph restyle(S style) { - List segs = new ArrayList<>(); - Iterator it = segments.iterator(); - segs.add(segmentOps.setStyle(it.next(), style)); - while (it.hasNext()) { - SEG prev = segs.get(segs.size() - 1); - SEG cur = segmentOps.setStyle(it.next(), style); - Optional joined = segmentOps.join(prev, cur); - if(joined.isPresent()) { - segs.set(segs.size() - 1, joined.get()); - } else { - segs.add(cur); - } - } - return new Paragraph<>(paragraphStyle, segmentOps, segs); + StyleSpans spans = new StyleSpansBuilder().add(new StyleSpan<>(style, length())).create(); + return new Paragraph<>(paragraphStyle, segmentOps, segments, spans); } 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 = subSequence(from, to).restyle(style); - Paragraph right = subSequence(to); - return left.concat(middle).concat(right); + StyleSpans left = styles.subView(0, from); + StyleSpans right = styles.subView(to, length()); + StyleSpans updatedStyles = left.append(style, to - from).concat(right); + return new Paragraph<>(paragraphStyle, segmentOps, segments, updatedStyles); } } @@ -238,21 +322,13 @@ public Paragraph restyle(int from, StyleSpans styleSpan return this; } - Paragraph left = trim(from); - Paragraph right = subSequence(from + len); - - Paragraph middle = subSequence(from, from + len); - List middleSegs = new ArrayList<>(styleSpans.getSpanCount()); - int offset = 0; - for(StyleSpan span: styleSpans) { - int end = offset + span.getLength(); - Paragraph text = middle.subSequence(offset, end); - middleSegs.addAll(text.restyle(span.getStyle()).segments); - offset = end; - } - Paragraph newMiddle = new Paragraph<>(paragraphStyle, segmentOps, middleSegs); + StyleSpans left = styles.subView(0, from); + StyleSpans right = styles.subView(from + len, length()); - return left.concat(newMiddle).concat(right); + // type issue with concat + StyleSpans castedSpans = (StyleSpans) styleSpans; + StyleSpans updatedStyles = left.concat(castedSpans).concat(right); + return new Paragraph<>(paragraphStyle, segmentOps, segments, updatedStyles); } /** @@ -266,7 +342,7 @@ public Paragraph restyle(int from, StyleSpans styleSpan * @return A new paragraph with the same segment contents, but a new paragraph style. */ public Paragraph setParagraphStyle(PS paragraphStyle) { - return new Paragraph<>(paragraphStyle, segmentOps, segments); + return new Paragraph<>(paragraphStyle, segmentOps, segments, styles); } /** @@ -276,11 +352,11 @@ public Paragraph setParagraphStyle(PS paragraphStyle) { */ public S getStyleOfChar(int charIdx) { if(charIdx < 0) { - return segmentOps.getStyle(segments.get(0)); + return styles.getStyleSpan(0).getStyle(); } - Position pos = navigator.offsetToPosition(charIdx, Forward); - return segmentOps.getStyle(segments.get(pos.getMajor())); + Position pos = styles.offsetToPosition(charIdx, Forward); + return styles.getStyleSpan(pos.getMajor()).getStyle(); } /** @@ -300,8 +376,8 @@ public S getStyleAtPosition(int position) { throw new IllegalArgumentException("Paragraph position cannot be negative (" + position + ")"); } - Position pos = navigator.offsetToPosition(position, Backward); - return segmentOps.getStyle(segments.get(pos.getMajor())); + Position pos = styles.offsetToPosition(position, Backward); + return styles.getStyleSpan(pos.getMajor()).getStyle(); } /** @@ -312,48 +388,16 @@ public S getStyleAtPosition(int position) { public IndexRange getStyleRangeAtPosition(int position) { Position pos = navigator.offsetToPosition(position, Backward); int start = position - pos.getMinor(); - int end = start + segmentOps.length(segments.get(pos.getMajor())); + int end = start + styles.getStyleSpan(pos.getMajor()).getLength(); return new IndexRange(start, end); } public StyleSpans getStyleSpans() { - StyleSpansBuilder builder = new StyleSpansBuilder<>(segments.size()); - for(SEG seg: segments) { - builder.add(segmentOps.getStyle(seg), - segmentOps.length(seg)); - } - return builder.create(); + return styles; } 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) { - SEG seg = segments.get(startSegIdx); - builder.add(segmentOps.getStyle(seg), to - from); - } else { - SEG startSeg = segments.get(startSegIdx); - builder.add(segmentOps.getStyle(startSeg), segmentOps.length(startSeg) - start.getMinor()); - - for(int i = startSegIdx + 1; i < endSegIdx; ++i) { - SEG seg = segments.get(i); - builder.add(segmentOps.getStyle(seg), - segmentOps.length(seg)); - } - - SEG endSeg = segments.get(endSegIdx); - builder.add(segmentOps.getStyle(endSeg), end.getMinor()); - } - - return builder.create(); + return styles.subView(from, to); } private String text = null; @@ -375,8 +419,8 @@ public String getText() { public String toString() { return "Par[" + paragraphStyle + "; " + - segments.stream().map(Object::toString) - .reduce((s1, s2) -> s1 + "," + s2).orElse("") + + getStyledSegments().stream().map(Object::toString) + .reduce((s1, s2) -> s1 + ", " + s2).orElse("") + "]"; } @@ -389,7 +433,8 @@ public boolean equals(Object other) { if(other instanceof Paragraph) { Paragraph that = (Paragraph) other; return Objects.equals(this.paragraphStyle, that.paragraphStyle) - && Objects.equals(this.segments, that.segments); + && Objects.equals(this.segments, that.segments) + && Objects.equals(this.styles, that.styles); } else { return false; } @@ -397,7 +442,6 @@ public boolean equals(Object other) { @Override public int hashCode() { - return Objects.hash(paragraphStyle, segments); + return Objects.hash(paragraphStyle, segments, styles); } - } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java index 6eec29924..22f44d180 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java @@ -7,7 +7,7 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; @@ -78,18 +78,18 @@ public static ReadOnlyStyledDocument fromString(String m.reset(); while(m.find()) { String s = str.substring(start, m.start()); - res.add(new Paragraph<>(paragraphStyle, segmentOps, segmentOps.create(s, style))); + res.add(new Paragraph<>(paragraphStyle, segmentOps, segmentOps.create(s), style)); start = m.end(); } String last = str.substring(start); - res.add(new Paragraph<>(paragraphStyle, segmentOps, segmentOps.create(last, style))); + res.add(new Paragraph<>(paragraphStyle, segmentOps, segmentOps.create(last), style)); return new ReadOnlyStyledDocument<>(res); } public static ReadOnlyStyledDocument fromSegment(SEG segment, PS paragraphStyle, S style, SegmentOps segmentOps) { - Paragraph content = new Paragraph(paragraphStyle, segmentOps, Arrays.asList(segment)); - List> res = Arrays.asList(content); + Paragraph content = new Paragraph(paragraphStyle, segmentOps, segment, style); + List> res = Collections.singletonList(content); return new ReadOnlyStyledDocument<>(res); } @@ -102,9 +102,12 @@ public static ReadOnlyStyledDocument from(StyledDocumen } - public static Codec> codec(Codec pCodec, Codec segCodec, SegmentOps segmentOps) { + public static Codec> codec(Codec pCodec, Codec> segCodec, + SegmentOps segmentOps) { return new Codec>() { - private final Codec>> codec = Codec.listCodec(paragraphCodec(pCodec, segCodec, segmentOps)); + private final Codec>> codec = Codec.listCodec( + paragraphCodec(pCodec, segCodec, segmentOps) + ); @Override public String getName() { @@ -124,9 +127,11 @@ public StyledDocument decode(DataInputStream is) throws IOException }; } - private static Codec> paragraphCodec(Codec pCodec, Codec segCodec, SegmentOps segmentOps) { + private static Codec> paragraphCodec(Codec pCodec, + Codec> segCodec, + SegmentOps segmentOps) { return new Codec>() { - private final Codec> segmentsCodec = Codec.listCodec(segCodec); + private final Codec>> segmentsCodec = Codec.listCodec(segCodec); @Override public String getName() { @@ -136,13 +141,13 @@ public String getName() { @Override public void encode(DataOutputStream os, Paragraph p) throws IOException { pCodec.encode(os, p.getParagraphStyle()); - segmentsCodec.encode(os, p.getSegments()); + segmentsCodec.encode(os, p.getStyledSegments()); } @Override public Paragraph decode(DataInputStream is) throws IOException { PS paragraphStyle = pCodec.decode(is); - List segments = segmentsCodec.decode(is); + List> segments = segmentsCodec.decode(is); return new Paragraph<>(paragraphStyle, segmentOps, segments); } }; diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java index 6a6281d04..5355b8e3b 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java @@ -1,6 +1,7 @@ package org.fxmisc.richtext.model; import java.util.Optional; +import java.util.function.BiFunction; import org.reactfx.util.Either; @@ -21,24 +22,80 @@ public interface SegmentOps { public SEG subSequence(SEG seg, int start); - public S getStyle(SEG seg); + public Optional joinSeg(SEG currentSeg, SEG nextSeg); - public SEG setStyle(SEG seg, S style); + default Optional joinStyle(S currentStyle, S nextStyle) { + return Optional.empty(); + } - public Optional join(SEG currentSeg, SEG nextSeg); + public SEG createEmptySeg(); - public SEG createEmpty(); + public static TextOps styledTextOps() { + return styledTextOps((s1, s2) -> Optional.empty()); + } + + public static TextOps styledTextOps(BiFunction> mergeStyle) { + return new TextOpsBase("") { + @Override + public char realCharAt(String s, int index) { + return s.charAt(index); + } + + @Override + public String realGetText(String s) { + return s; + } + + @Override + public String realSubSequence(String s, int start, int end) { + return s.substring(start, end); + } + + @Override + public String create(String text) { + return text; + } + + @Override + public int length(String s) { + return s.length(); + } + + @Override + public Optional joinSeg(String currentSeg, String nextSeg) { + return Optional.of(currentSeg + nextSeg); + } + + @Override + public Optional joinStyle(S currentStyle, S nextStyle) { + return mergeStyle.apply(currentStyle, nextStyle); + } + }; + } public default SegmentOps, S> or(SegmentOps rOps) { return either(this, rOps); } - public default TextOps, S> or_(TextOps rOps) { - return TextOps.eitherR(this, rOps); + public default SegmentOps, Either> orStyled( + SegmentOps rOps + ) { + return eitherStyles(this, rOps); + } + + public static SegmentOps, Either> eitherStyles( + SegmentOps lOps, + SegmentOps rOps) { + return new EitherStyledSegmentOps<>(lOps, rOps); } public static SegmentOps, S> either(SegmentOps lOps, SegmentOps rOps) { - return new EitherSegmentOps<>(lOps, rOps); + return either(lOps, rOps, (leftStyle, rightStyle) -> Optional.empty()); + } + + public static SegmentOps, S> either(SegmentOps lOps, SegmentOps rOps, + BiFunction> mergeStyle) { + return new EitherSegmentOps<>(lOps, rOps, mergeStyle); } } @@ -46,10 +103,12 @@ class EitherSegmentOps implements SegmentOps, S> { private final SegmentOps lOps; private final SegmentOps rOps; + private final BiFunction> mergeStyle; - EitherSegmentOps(SegmentOps lOps, SegmentOps rOps) { + EitherSegmentOps(SegmentOps lOps, SegmentOps rOps, BiFunction> mergeStyle) { this.lOps = lOps; this.rOps = rOps; + this.mergeStyle = mergeStyle; } @@ -82,24 +141,92 @@ public Either subSequence(Either seg, int start) { } @Override - public S getStyle(Either seg) { - return seg.unify(lOps::getStyle, - rOps::getStyle); + public Optional> joinSeg(Either left, Either right) { + return left.unify(ll -> right.unify(rl -> lOps.joinSeg(ll, rl).map(Either::left), rr -> Optional.empty()), + lr -> right.unify(rl -> Optional.empty(), rr -> rOps.joinSeg(lr, rr).map(Either::right))); + } + + @Override + public Optional joinStyle(S currentStyle, S nextStyle) { + return mergeStyle.apply(currentStyle, nextStyle); + } + + public Either createEmptySeg() { + return Either.left(lOps.createEmptySeg()); + } +} + +class EitherStyledSegmentOps implements SegmentOps, Either> { + + private final SegmentOps lOps; + private final SegmentOps rOps; + + EitherStyledSegmentOps(SegmentOps lOps, SegmentOps rOps) { + this.lOps = lOps; + this.rOps = rOps; + } + + + @Override + public int length(Either seg) { + return seg.unify( + lOps::length, + rOps::length + ); + } + + @Override + public char charAt(Either seg, int index) { + return seg.unify( + lSeg -> lOps.charAt(lSeg, index), + rSeg -> rOps.charAt(rSeg, index) + ); + } + + @Override + public String getText(Either seg) { + return seg.unify(lOps::getText, rOps::getText); + } + + @Override + public Either subSequence(Either seg, int start, int end) { + return seg.map( + lSeg -> lOps.subSequence(lSeg, start, end), + rSeg -> rOps.subSequence(rSeg, start, end) + ); + } + + @Override + public Either subSequence(Either seg, int start) { + return seg.map(lSeg -> lOps.subSequence(lSeg, start), + rSeg -> rOps.subSequence(rSeg, start)); } @Override - public Either setStyle(Either seg, S style) { - return seg.map(l -> lOps.setStyle(l, style), - r -> rOps.setStyle(r, style)); + public Optional> joinSeg(Either left, Either right) { + return left.unify( + ll -> right.unify( + rl -> lOps.joinSeg(ll, rl).map(Either::left), + rr -> Optional.empty()), + lr -> right.unify( + rl -> Optional.empty(), + rr -> rOps.joinSeg(lr, rr).map(Either::right)) + ); } @Override - public Optional> join(Either left, Either right) { - return left.unify(ll -> right.unify(rl -> lOps.join(ll, rl).map(Either::left), rr -> Optional.empty()), - lr -> right.unify(rl -> Optional.empty(), rr -> rOps.join(lr, rr).map(Either::right))); + public Optional> joinStyle(Either left, Either right) { + return left.unify( + ll -> right.unify( + rl -> lOps.joinStyle(ll, rl).map(Either::left), + rr -> Optional.empty()), + lr -> right.unify( + rl -> Optional.empty(), + rr -> rOps.joinStyle(lr, rr).map(Either::right)) + ); } - public Either createEmpty() { - return Either.left(lOps.createEmpty()); + public Either createEmptySeg() { + return Either.left(lOps.createEmptySeg()); } } \ No newline at end of file diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOpsBase.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOpsBase.java index cf44425d3..24bf2e43e 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOpsBase.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOpsBase.java @@ -4,8 +4,8 @@ /** * Properly implements the {@link SegmentOps} interface and reduces boilerplate, so that developer only needs to - * implement methods for real segments, not empty ones. Optionally, {@link #join(Object, Object)} can be overridden - * as well. + * implement methods for real segments, not empty ones. Optionally, {@link #joinSeg(Object, Object)} and + * {@link #joinStyle(Object, Object)} can be overridden as well. * * @param the type of segment * @param the type of style @@ -14,25 +14,19 @@ public abstract class SegmentOpsBase implements SegmentOps { private final SEG empty; - public SegmentOpsBase(SEG emptySEg) { - this.empty = emptySEg; + public SegmentOpsBase(SEG emptySeg) { + this.empty = emptySeg; } - @Override - public final int length(SEG seg) { - return seg == empty ? 0 : realLength(seg); - } - public abstract int realLength(SEG seg); - @Override public final char charAt(SEG seg, int index) { - return seg == empty ? '\0' : realCharAt(seg, index); + return length(seg) == 0 ? '\0' : realCharAt(seg, index); } public abstract char realCharAt(SEG seg, int index); @Override public final String getText(SEG seg) { - return seg == empty ? "" : realGetText(seg); + return length(seg) == 0 ? "" : realGetText(seg); } public abstract String realGetText(SEG seg); @@ -46,37 +40,29 @@ public final SEG subSequence(SEG seg, int start, int end) { String.format("End cannot be greater than segment's length. End=%s Length=%s", end, length(seg)) ); } - return seg == empty ? empty : realSubSequence(seg, start, end); + return length(seg) == 0 || start == end + ? empty + : realSubSequence(seg, start, end); } public abstract SEG realSubSequence(SEG seg, int start, int end); @Override public final SEG subSequence(SEG seg, int start) { - return seg == empty ? empty : realSubSequence(seg, start); + return length(seg) == 0 || start == length(seg) + ? empty + : realSubSequence(seg, start); } public SEG realSubSequence(SEG seg, int start) { return realSubSequence(seg, start, length(seg)); } @Override - public final S getStyle(SEG seg) { - return seg == empty ? null : realGetStyle(seg); - } - public abstract S realGetStyle(SEG seg); - - @Override - public final SEG setStyle(SEG seg, S style) { - return seg == empty ? empty : realSetStyle(seg, style); + public final SEG createEmptySeg() { + return empty; } - public abstract SEG realSetStyle(SEG seg, S style); @Override - public Optional join(SEG currentSeg, SEG nextSeg) { + public Optional joinStyle(S currentStyle, S nextStyle) { return Optional.empty(); } - - @Override - public final SEG createEmpty() { - return empty; - } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java index 968dc639a..36e71a422 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java @@ -1,12 +1,24 @@ package org.fxmisc.richtext.model; +import java.util.Optional; +import java.util.function.BiFunction; + /** - * Provides an implementation of {@link EditableStyledDocument} that is specified for {@link StyledText} as its segment. + * Provides an implementation of {@link EditableStyledDocument} that is specified for {@link String} as its segment. * See also {@link GenericEditableStyledDocument}. */ -public final class SimpleEditableStyledDocument extends GenericEditableStyledDocumentBase, S> { +public final class SimpleEditableStyledDocument extends GenericEditableStyledDocumentBase { public SimpleEditableStyledDocument(PS initialParagraphStyle, S initialStyle) { - super(initialParagraphStyle, initialStyle, StyledText.textOps()); + this(initialParagraphStyle, initialStyle, (s1, s2) -> Optional.empty()); + } + + public SimpleEditableStyledDocument(PS initialParagraphStyle, S initialStyle, + BiFunction> mergeStyle) { + this(initialParagraphStyle, initialStyle, SegmentOps.styledTextOps(mergeStyle)); + } + + public SimpleEditableStyledDocument(PS initialParagraphStyle, S initialTextStyle, SegmentOps segOps) { + super(initialParagraphStyle, initialTextStyle, segOps); } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleActions.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleActions.java index 036ebc7d1..724aa1520 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleActions.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleActions.java @@ -24,6 +24,18 @@ public interface StyleActions { */ PS getInitialParagraphStyle(); + /** + * Returns {@link #getInitialTextStyle()} if {@link #getUseInitialStyleForInsertion()} is true; + * otherwise, returns the style at the given position. + */ + S getTextStyleForInsertionAt(int pos); + + /** + * Returns {@link #getInitialParagraphStyle()} if {@link #getUseInitialStyleForInsertion()} is true; + * otherwise, returns the paragraph style at the given position. + */ + PS getParagraphStyleForInsertionAt(int pos); + /** * Indicates whether style should be preserved on undo/redo (and in the future copy/paste and text move). */ diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledSegment.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledSegment.java new file mode 100644 index 000000000..24f0a3bae --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledSegment.java @@ -0,0 +1,37 @@ +package org.fxmisc.richtext.model; + +import java.util.Objects; + +public final class StyledSegment { + + private final SEG segment; + public final SEG getSegment() { return segment; } + + private final S style; + public final S getStyle() { return style; } + + public StyledSegment(SEG segment, S style) { + this.segment = segment; + this.style = style; + } + + @Override + public String toString() { + return String.format("StyledSegment(segment=%s style=%s", segment, style); + } + + @Override + public int hashCode() { + return Objects.hash(segment, style); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof StyledSegment) { + StyledSegment that = (StyledSegment) obj; + return Objects.equals(this.segment, that.segment) && Objects.equals(this.style, that.style); + } else { + return false; + } + } +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java deleted file mode 100644 index ff3cbbdb1..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.fxmisc.richtext.model; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.Objects; -import java.util.Optional; - -/** - * A String with a single style of type S. - * - * This is a simple class suitable for use as the SEG type in the other classes - * such as {@link Paragraph}, {@link org.fxmisc.richtext.GenericStyledArea}, {@link StyledDocument}, etc. - * - * This class is immutable. - * - * @param The type of the style of the text. - */ -public class StyledText { - - /** - * An implementation of TextOps for StyledText. Useful for passing to the constructor - * of {@link org.fxmisc.richtext.GenericStyledArea} and similar classes if you are using this class as the SEG type. - */ - public static TextOps, S> textOps() { - return new TextOpsBase, S>(new StyledText<>("", null)) { - - @Override - public int realLength(StyledText styledText) { - return styledText.getText().length(); - } - - @Override - public char realCharAt(StyledText styledText, int index) { - return styledText.getText().charAt(index); - } - - @Override - public String realGetText(StyledText styledText) { - return styledText.getText(); - } - - @Override - public StyledText realSubSequence(StyledText styledText, int start, int end) { - return new StyledText<>(styledText.getText().substring(start, end), styledText.getStyle()); - } - - @Override - public StyledText realSubSequence(StyledText styledText, int start) { - return new StyledText<>(styledText.getText().substring(start), styledText.getStyle()); - } - - @Override - public S realGetStyle(StyledText styledText) { - return styledText.getStyle(); - } - - @Override - public StyledText realSetStyle(StyledText seg, S style) { - return seg.setStyle(style); - } - - @Override - public Optional> join(StyledText left, StyledText right) { - return Objects.equals(left.getStyle(), right.getStyle()) - ? Optional.of(new StyledText<>(left.getText() + right.getText(), left.getStyle())) - : Optional.empty(); - } - - @Override - public StyledText create(String text, S style) { - return new StyledText<>(text, style); - } - }; - } - - /** - * A codec which allows serialisation of this class to/from a data stream. - * - * Because S may be any type, you must pass a codec for it. If your style - * is String or Color, you can use {@link Codec#STRING_CODEC}/{@link Codec#COLOR_CODEC} respectively. - */ - public static Codec> codec(Codec styleCodec) { - return new Codec>() { - - @Override - public String getName() { - return "styled-text"; - } - - @Override - public void encode(DataOutputStream os, StyledText t) throws IOException { - Codec.STRING_CODEC.encode(os, t.text); - styleCodec.encode(os, t.style); - } - - @Override - public StyledText decode(DataInputStream is) throws IOException { - String text = Codec.STRING_CODEC.decode(is); - S style = styleCodec.decode(is); - return new StyledText<>(text, style); - } - - }; - } - - - private final String text; - - /** - * The text content of this piece of styled text. - */ - public String getText() { return text; } - - private final S style; - - /** - * The style of this piece of styled text. - */ - public S getStyle() { return style; } - - /** - * Creates a new StyledText with the same content but the given style. - */ - public StyledText setStyle(S style) { - return new StyledText<>(text, style); - } - - /** - * Creates a new StyledText with the given text content, and style. - */ - public StyledText(String text, S style) { - this.text = text; - this.style = style; - } - - @Override - public String toString() { - return String.format("StyledText[text=\"%s\", style=%s]", text, style); - } - - @Override - public boolean equals(Object obj) { - if(obj instanceof StyledText) { - StyledText that = (StyledText) obj; - return (this.text.isEmpty() && that.text.isEmpty()) - || (Objects.equals(this.text, that.text) - && Objects.equals(this.style, that.style)); - } else { - return false; - } - } - - @Override - public int hashCode() { - return Objects.hash(text, style); - } - -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java index 1f82753bd..b189e189e 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java @@ -47,6 +47,8 @@ public interface TextEditingArea { */ StyledDocument getDocument(); + SegmentOps getSegOps(); + /** * The current position of the caret, as a character offset in the text. * @@ -267,6 +269,37 @@ default void replaceText(int startParagraph, int startColumn, int endParagraph, */ void replace(int start, int end, StyledDocument replacement); + /** + * Replaces a range of characters with the given segment. + * + * It must hold {@code 0 <= start <= end <= getLength()}. + * + * @param start Start index of the range to replace, inclusive. + * @param end End index of the range to replace, exclusive. + * @param seg The seg to put in place of the deleted range. + * It must not be null. + */ + void replace(int start, int end, SEG seg, S style); + + /** + * Replaces a range of characters with the given segment. + * + * It must hold {@code 0 <= start <= end <= getLength()} where + * {@code start = getAbsolutePosition(startParagraph, startColumn);} and is inclusive, and + * {@code int end = getAbsolutePosition(endParagraph, endColumn);} and is exclusive. + * + *

Caution: see {@link #getAbsolutePosition(int, int)} to know how the column index argument + * can affect the returned position.

+ * + * @param seg The segment to put in place of the deleted range. + * It must not be null. + */ + default void replace(int startParagraph, int startColumn, int endParagraph, int endColumn, SEG seg, S style) { + int start = getAbsolutePosition(startParagraph, startColumn); + int end = getAbsolutePosition(endParagraph, endColumn); + replace(start, end, seg, style); + } + /** * Replaces a range of characters with the given rich-text document. * @@ -292,6 +325,19 @@ default void replaceText(IndexRange range, String text) { replaceText(range.getStart(), range.getEnd(), text); } + /** + * Replaces a range of characters with the given seg. + * + * @param range The range to replace. It must not be null. + * @param seg The segment to put in place of the deleted range. + * It must not be null. + * + * @see #replace(int, int, Object, Object) + */ + default void replace(IndexRange range, SEG seg, S style) { + replace(range.getStart(), range.getEnd(), seg, style); + } + /** * Equivalent to * {@code replace(range.getStart(), range.getEnd(), replacement)}. diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java index 077f23d54..e2dac8a7b 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java @@ -2,19 +2,24 @@ import org.reactfx.util.Either; +import java.util.Optional; +import java.util.function.BiFunction; + public interface TextOps extends SegmentOps { - public SEG create(String text, S style); + public SEG create(String text); - public default TextOps, S> _or(SegmentOps rOps) { - return eitherL(this, rOps); + public default TextOps, S> _or(SegmentOps rOps, BiFunction> mergeStyle) { + return eitherL(this, rOps, mergeStyle); } - public static TextOps, S> eitherL(TextOps lOps, SegmentOps rOps) { - return new LeftTextOps<>(lOps, rOps); + public static TextOps, S> eitherL(TextOps lOps, SegmentOps rOps, + BiFunction> mergeStyle) { + return new LeftTextOps<>(lOps, rOps, mergeStyle); } - public static TextOps, S> eitherR(SegmentOps lOps, TextOps rOps) { - return new RightTextOps<>(lOps, rOps); + public static TextOps, S> eitherR(SegmentOps lOps, TextOps rOps, + BiFunction> mergeStyle) { + return new RightTextOps<>(lOps, rOps, mergeStyle); } } @@ -23,14 +28,14 @@ class LeftTextOps extends EitherSegmentOps implements TextOps< private final TextOps lOps; - LeftTextOps(TextOps lOps, SegmentOps rOps) { - super(lOps, rOps); + LeftTextOps(TextOps lOps, SegmentOps rOps, BiFunction> mergeStyle) { + super(lOps, rOps, mergeStyle); this.lOps = lOps; } @Override - public Either create(String text, S style) { - return Either.left(lOps.create(text, style)); + public Either create(String text) { + return Either.left(lOps.create(text)); } } @@ -39,14 +44,14 @@ class RightTextOps extends EitherSegmentOps implements TextOps private final TextOps rOps; - RightTextOps(SegmentOps lOps, TextOps rOps) { - super(lOps, rOps); + RightTextOps(SegmentOps lOps, TextOps rOps, BiFunction> mergeStyle) { + super(lOps, rOps, mergeStyle); this.rOps = rOps; } @Override - public Either create(String text, S style) { - return Either.right(rOps.create(text, style)); + public Either create(String text) { + return Either.right(rOps.create(text)); } } \ No newline at end of file diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java index 7708ea81f..705fcc0b9 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java @@ -11,11 +11,11 @@ public class ParagraphTest { // This relates to merging text changes and issue #216. @Test public void concatEmptyParagraphsTest() { - TextOps, Boolean> segOps = StyledText.textOps(); - Paragraph, Boolean> p1 = new Paragraph<>(null, segOps, segOps.create("", true)); - Paragraph, Boolean> p2 = new Paragraph<>(null, segOps, segOps.create("", false)); + TextOps segOps = SegmentOps.styledTextOps(); + Paragraph p1 = new Paragraph<>(null, segOps, segOps.create(""), true); + Paragraph p2 = new Paragraph<>(null, segOps, segOps.create(""), false); - Paragraph, Boolean> p = p1.concat(p2); + Paragraph p = p1.concat(p2); assertEquals(Boolean.TRUE, p.getStyleAtPosition(0)); } @@ -24,13 +24,13 @@ public void concatEmptyParagraphsTest() { // would style an empty paragraph would throw an exception @Test public void restylingEmptyParagraphViaStyleSpansWorks() { - TextOps, Boolean> segOps = StyledText.textOps(); - Paragraph, Boolean> p = new Paragraph<>(null, segOps, segOps.createEmpty()); + TextOps segOps = SegmentOps.styledTextOps(); + Paragraph p = new Paragraph<>(null, segOps, segOps.createEmptySeg(), false); StyleSpansBuilder builder = new StyleSpansBuilder<>(); builder.add(true, 2); StyleSpans spans = builder.create(); - Paragraph, Boolean> restyledP = p.restyle(0, spans); + Paragraph restyledP = p.restyle(0, spans); assertEquals(p, restyledP); } diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java index 6f70e41cb..88e5578bc 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java @@ -6,15 +6,22 @@ import static org.junit.Assert.*; import java.util.List; +import java.util.function.BiConsumer; import org.junit.Test; public class ReadOnlyStyledDocumentTest { + private static Void NULL = new Void(); + + /** Short for Void: + * cannot pass in 'null' since compiler will interpret it as a StyleSpans argument to Paragraph's constructor */ + private static class Void { } + @Test public void testUndo() { - TextOps, String> segOps = StyledText.textOps(); - ReadOnlyStyledDocument, String> doc0 = fromString("", "X", "X", segOps); + TextOps segOps = SegmentOps.styledTextOps(); + ReadOnlyStyledDocument doc0 = fromString("", "X", "X", segOps); doc0.replace(0, 0, fromString("abcd", "Y", "Y", segOps)).exec((doc1, chng1, pchng1) -> { // undo chng1 @@ -30,52 +37,59 @@ public void testUndo() { @Test public void deleteNewlineTest() { - TextOps, Void> segOps = StyledText.textOps(); - ReadOnlyStyledDocument, Void> doc0 = fromString("Foo\nBar", null, null, segOps); - doc0.replace(3, 4, fromString("", null, null, segOps)).exec((doc1, ch, pch) -> { - List, Void>> removed = pch.getRemoved(); - List, Void>> added = pch.getAdded(); + TextOps segOps = SegmentOps.styledTextOps(); + ReadOnlyStyledDocument doc0 = fromString("Foo\nBar", NULL, NULL, segOps); + doc0.replace(3, 4, fromString("", NULL, NULL, segOps)).exec((doc1, ch, pch) -> { + List> removed = pch.getRemoved(); + List> added = pch.getAdded(); assertEquals(2, removed.size()); - assertEquals(new Paragraph, Void>(null, segOps, segOps.create("Foo", null)), removed.get(0)); - assertEquals(new Paragraph, Void>(null, segOps, segOps.create("Bar", null)), removed.get(1)); + Paragraph p = new Paragraph<>(NULL, segOps, segOps.create("som"), NULL); + assertEquals(new Paragraph<>(NULL, segOps, "Foo", NULL), removed.get(0)); + assertEquals(new Paragraph<>(NULL, segOps, "Bar", NULL), removed.get(1)); assertEquals(1, added.size()); - assertEquals(new Paragraph, Void>(null, segOps, segOps.create("FooBar", null)), added.get(0)); + assertEquals(new Paragraph<>(NULL, segOps, "FooBar", NULL), added.get(0)); }); } @Test public void testRestyle() { + // texts final String fooBar = "Foo Bar"; final String and = " and "; final String helloWorld = "Hello World"; - TextOps, String> segOps = StyledText.textOps(); - SimpleEditableStyledDocument doc0 = new SimpleEditableStyledDocument<>("", ""); + // styles + final String bold = "bold"; + final String empty = ""; + final String italic = "italic"; + TextOps segOps = SegmentOps.styledTextOps(); - ReadOnlyStyledDocument, String> text = fromString(fooBar, "", "bold", segOps); - doc0.replace(doc0.getLength(), doc0.getLength(), text); + SimpleEditableStyledDocument doc0 = new SimpleEditableStyledDocument<>("", ""); - text = fromString(and, "", "", segOps); - doc0.replace(doc0.getLength(), doc0.getLength(), text); + BiConsumer appendStyledText = (text, style) -> { + ReadOnlyStyledDocument rosDoc = fromString(text, "", style, segOps); + doc0.replace(doc0.getLength(), doc0.getLength(), rosDoc); + }; - text = fromString(helloWorld, "", "bold", segOps); - doc0.replace(doc0.getLength(), doc0.getLength(), text); + appendStyledText.accept(fooBar, bold); + appendStyledText.accept(and, empty); + appendStyledText.accept(helloWorld, bold); StyleSpans styles = doc0.getStyleSpans(4, 17); assertThat("Invalid number of Spans", styles.getSpanCount(), equalTo(3)); - StyleSpans newStyles = styles.mapStyles(style -> "italic"); + StyleSpans newStyles = styles.mapStyles(style -> italic); doc0.setStyleSpans(4, newStyles); // assert the new segment structure: // StyledText[text="Foo ", style=bold] // StyledText[text="Bar and Hello", style=italic] // StyledText[text=" World", style=bold] - List> result = doc0.getParagraphs().get(0).getSegments(); - assertThat(result.size(), equalTo(3)); - assertThat(result.get(0).getText(), equalTo("Foo ")); - assertThat(result.get(1).getText(), equalTo("Bar and Hello")); - assertThat(result.get(2).getText(), equalTo(" World")); + StyleSpans spans = doc0.getParagraphs().get(0).getStyleSpans(); + assertThat(spans.getSpanCount(), equalTo(3)); + assertThat(spans.getStyleSpan(0).getStyle(), equalTo(bold)); + assertThat(spans.getStyleSpan(1).getStyle(), equalTo(italic)); + assertThat(spans.getStyleSpan(2).getStyle(), equalTo(bold)); } } diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java index a870b2624..c70977ebf 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java @@ -5,14 +5,14 @@ public class SimpleEditableStyledDocumentTest { - private final TextOps, String> segOps = StyledText.textOps(); + private final TextOps segOps = SegmentOps.styledTextOps(); /** * The style of the inserted text will be the style at position * {@code start} in the current document. */ - private void replaceText(EditableStyledDocument, String> doc, int start, int end, String text) { - StyledDocument, String> styledDoc = ReadOnlyStyledDocument.fromString( + private void replaceText(EditableStyledDocument doc, int start, int end, String text) { + StyledDocument styledDoc = ReadOnlyStyledDocument.fromString( text, doc.getParagraphStyleAtPosition(start), doc.getStyleAtPosition(start), segOps); doc.replace(start, end, styledDoc); } diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/UndoManagerTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/UndoManagerTest.java index f81485e8e..065bd8db3 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/UndoManagerTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/UndoManagerTest.java @@ -14,7 +14,7 @@ public class UndoManagerTest { @Test public void testUndoWithWinNewlines() { - final TextOps>, Collection> segOps = StyledText.textOps(); + final TextOps> segOps = SegmentOps.styledTextOps(); String text1 = "abc\r\ndef"; String text2 = "A\r\nB\r\nC"; @@ -30,7 +30,7 @@ public void testUndoWithWinNewlines() { @Test public void testForBug216() { - final TextOps, Boolean> segOps = StyledText.textOps(); + final TextOps segOps = SegmentOps.styledTextOps(); // set up area with some styled text content boolean initialStyle = false;