From 3b443bf1588500be5cb8acba6448279142972e01 Mon Sep 17 00:00:00 2001 From: Andreas Fester Date: Tue, 17 May 2016 11:21:00 +0200 Subject: [PATCH 1/9] Initial support for custom objects --- richtextfx-demos/sample.png | Bin 0 -> 1085 bytes richtextfx-demos/sample.rtfx | Bin 0 -> 2129 bytes .../demo/customobject/CircleObject.java | 58 +++++++++ .../demo/customobject/CustomObjectDemo.java | 61 ++++++++++ .../demo/customobject/RectangleObject.java | 65 ++++++++++ .../richtext/demo/richtext/RichText.java | 114 +++++++++++++++++- .../richtext/demo/richtext/insertimage.png | Bin 0 -> 1085 bytes .../richtext/demo/richtext/loadfile.png | Bin 0 -> 1148 bytes .../richtext/demo/richtext/rich-text.css | 3 + .../richtext/demo/richtext/savefile.png | Bin 0 -> 1184 bytes .../org/fxmisc/richtext/ParagraphText.java | 17 +-- .../org/fxmisc/richtext/StyledTextArea.java | 35 +++++- .../fxmisc/richtext/model/CustomObject.java | 105 ++++++++++++++++ .../model/EditableStyledDocument.java | 4 +- .../fxmisc/richtext/model/LinkedImage.java | 81 +++++++++++++ .../org/fxmisc/richtext/model/Paragraph.java | 102 ++++++++++------ .../model/ReadOnlyStyledDocument.java | 47 ++++++-- .../org/fxmisc/richtext/model/Segment.java | 40 ++++++ .../model/SimpleEditableStyledDocument.java | 6 +- .../org/fxmisc/richtext/model/StyleSpan.java | 5 + .../org/fxmisc/richtext/model/StyledText.java | 67 +++++++++- .../richtext/model/StyledTextAreaModel.java | 4 +- .../richtext/model/CustomObjectTest.java | 103 ++++++++++++++++ .../model/ReadOnlyStyledDocumentTest.java | 36 ++++++ 24 files changed, 879 insertions(+), 74 deletions(-) create mode 100644 richtextfx-demos/sample.png create mode 100644 richtextfx-demos/sample.rtfx create mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java create mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java create mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java create mode 100644 richtextfx-demos/src/main/resources/org/fxmisc/richtext/demo/richtext/insertimage.png create mode 100644 richtextfx-demos/src/main/resources/org/fxmisc/richtext/demo/richtext/loadfile.png create mode 100644 richtextfx-demos/src/main/resources/org/fxmisc/richtext/demo/richtext/savefile.png create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java create mode 100644 richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java diff --git a/richtextfx-demos/sample.png b/richtextfx-demos/sample.png new file mode 100644 index 0000000000000000000000000000000000000000..5cdf307df24a093ca491ae9777fc112ef1c28ad9 GIT binary patch literal 1085 zcmV-D1j74?P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000B5NklpZ3-!sv!}5^9}6v^Z=Co3{ey?O49J6+`wDP+tY=t^w-ddZ^BQ zh+3Bib(Co-t03XbomfJTA3wT+&|X=JpHtzpWTExi9n`yl2KOSWjR~ke9S61f z7#gOQprgEUlneieEo`_PK>bt*`Wc|n3+QNX_5e*&w{iS@E);E>pg0weGJ^!#u`_7@ z1*jsS*B_(d7zy>`0kn8R=)3_K=wc3^o5qJw*suY``b{X-ZAOuPBg#!5q3ag?mbTw< zBbHDgVbesABq4PBfU^%^^v*$H+=HTu4XFD1B@{GnMzdhT#oqvpa|yTZ{)tps`+cE= zgpL_MPPqiMT)~ZMXRX*sxRVDJywI}U!Ngp*2%mXYZhk8)ImkX|iPDJh?`0sg0$~7W1T7Bs zJd5+5!*q&XJUepkc}Qd|i=J{_mW#eJ-M1oqU6^b^w&`&k8DPlkW3c+PB78nd*?uEi zO3a~#DN<#oRMqE`Y0a~;6D!d_`=zRF_7nB`VDTVB5ixkPM1;>g`&h{)H@?eW=MVa6 z;uYf6NAOzw?$z0ub;48+&5UM=ZTM&++>zNIkNBc zpm}-`6M5iHiU%8AOdy;AJ>?eaLytzy$&3;&M|JnNHT`ioeg1Ek7aTs4J@>ex=1kCc zjwW4LH9vLL>;0mA4JL*SI>!R0Bq;*Xa)h*w!5lUoMX&|OtK#st3$!B1D3j`LKvp&H zZSJFxWVAAjviu#_B!EfY_NF>$PT4NayWDsPnr%3f4mQU+H{rG=1hfkUQbRi_(rirf;5 z;e{HN)_N2uU{+o1jGa=966RyrUg7WEK+fK~to3CQsMr2KZ62nQQ}M$dA5FjC+mK*yH_6%Z{oU|AKavQ-qAKo#V)Tgs&##uP9SbWsZF zR}G#*E?XoGbvcUKQr2FD20Ug(udRAiQ5FRoLJW7Etzzu?Xfz-6)ac`LtBrG^?FS?p zRyt`Y!%E*YJw^NSDA5XTXvz1cM^BG0j`CK|v;T95giPkz_lyn&u3SFojVmo=X2V}yXU)qy* HD1&YRCna5Y literal 0 HcmV?d00001 diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java new file mode 100644 index 000000000..596f05c9e --- /dev/null +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java @@ -0,0 +1,58 @@ +package org.fxmisc.richtext.demo.customobject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; + +import org.fxmisc.richtext.model.Codec; +import org.fxmisc.richtext.model.CustomObject; + +import javafx.scene.Node; +import javafx.scene.shape.Circle; + +/** + * A custom object which represents a circle. + */ +public class CircleObject extends CustomObject> { + + private double radius; + + public CircleObject() {} + + public CircleObject(double radius) { + super(new ArrayList()); + this.radius = radius; + } + + public double getRadius() { + return radius; + } + + @Override + public void encode(DataOutputStream os) throws IOException { + Codec.STRING_CODEC.encode(os, Double.toString(radius)); + } + + @Override + public void decode(DataInputStream is) throws IOException { + try { + radius = Double.parseDouble(Codec.STRING_CODEC.decode(is)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + @Override + public Node createNode() { + Circle result = new Circle(getRadius()); + return result; + } + + @Override + public String toString() { + return String.format("CircleObject[radius=%s]", radius); + } + +} \ No newline at end of file diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java new file mode 100644 index 000000000..3be7b5c49 --- /dev/null +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java @@ -0,0 +1,61 @@ +package org.fxmisc.richtext.demo.customobject; + +import java.util.ArrayList; +import java.util.Collection; + +import org.fxmisc.flowless.VirtualizedScrollPane; +import org.fxmisc.richtext.StyleClassedTextArea; +import org.fxmisc.richtext.model.LinkedImage; +import org.fxmisc.richtext.model.ReadOnlyStyledDocument; + +import javafx.application.Application; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + + +/** + * This demo shows how to register custom objects with the RichTextFX editor. + * It creates a sample document with some text, a custom node with a circle, a custom node + * with a rectangle and also adds an image to show that images are supported without + * explicitly implementing and registering them as custom objects. + */ +public class CustomObjectDemo extends Application { + + public static void main(String[] args) { + launch(args); + } + + + @Override + public void start(Stage primaryStage) { + StyleClassedTextArea textArea = new StyleClassedTextArea(); + textArea.setWrapText(true); + + // create the sample document + textArea.replaceText(0, 0, "This example shows how to add custom nodes, for example Rectangles "); + ReadOnlyStyledDocument, Collection> d1 = + ReadOnlyStyledDocument.from(new RectangleObject(20, 10), + new ArrayList()); + textArea.append(d1); + textArea.appendText(" or Circles "); + + ReadOnlyStyledDocument, Collection> d2 = + ReadOnlyStyledDocument.from(new CircleObject(5), + new ArrayList()); + textArea.append(d2); + + textArea.appendText("\nImages are supported by default: "); + ReadOnlyStyledDocument, Collection> d3 = + ReadOnlyStyledDocument.from(new LinkedImage>("sample.png", new ArrayList()), + new ArrayList()); + textArea.append(d3); + + textArea.appendText("\nNow, select some text from above (including one or more of the custom objects) using CTRL-C, and paste it somewhere in the document with CTRL-V."); + + Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(textArea)), 600, 400); + primaryStage.setScene(scene); + primaryStage.setTitle("Custom Object demo"); + primaryStage.show(); + } +} diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java new file mode 100644 index 000000000..220a365c4 --- /dev/null +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java @@ -0,0 +1,65 @@ +package org.fxmisc.richtext.demo.customobject; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; + +import org.fxmisc.richtext.model.Codec; +import org.fxmisc.richtext.model.CustomObject; + +import javafx.scene.Node; +import javafx.scene.shape.Rectangle; + +/** + * A custom object which represents a rectangle. + */ +public class RectangleObject extends CustomObject> { + + private double width; + private double height; + + public RectangleObject() {} + + public RectangleObject(double width, double height) { + super(new ArrayList()); + this.width = width; + this.height = height; + } + + public double getWidth() { + return width; + } + + public double getHeight() { + return height; + } + + @Override + public void encode(DataOutputStream os) throws IOException { + Codec.STRING_CODEC.encode(os, Double.toString(width)); + Codec.STRING_CODEC.encode(os, Double.toString(height)); + } + + @Override + public void decode(DataInputStream is) throws IOException { + try { + width = Double.parseDouble(Codec.STRING_CODEC.decode(is)); + height = Double.parseDouble(Codec.STRING_CODEC.decode(is)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + @Override + public Node createNode() { + Rectangle result = new Rectangle(getWidth(), getHeight()); + return result; + } + + @Override + public String toString() { + return String.format("RectangleObject[width=%s, height=%s]", width, height); + } +} 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 7c07f46cd..7905916fd 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 @@ -8,10 +8,22 @@ import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; import java.util.List; import java.util.Optional; import java.util.function.Function; +import org.fxmisc.richtext.model.Codec; +import org.fxmisc.richtext.model.LinkedImage; +import org.fxmisc.richtext.model.ReadOnlyStyledDocument; +import org.fxmisc.richtext.model.StyledDocument; +import org.reactfx.util.Tuple2; + import javafx.application.Application; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; @@ -31,6 +43,7 @@ import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.TextAlignment; +import javafx.stage.FileChooser; import javafx.stage.Stage; import org.fxmisc.flowless.VirtualizedScrollPane; @@ -54,10 +67,16 @@ public static void main(String[] args) { area.setStyleCodecs(ParStyle.CODEC, TextStyle.CODEC); } + private Stage mainStage; + private final SuspendableNo updatingToolbar = new SuspendableNo(); @Override public void start(Stage primaryStage) { + mainStage = primaryStage; + + Button loadBtn = createButton("loadfile", this::loadDocument, "Load document"); + Button saveBtn = createButton("savefile", this::saveDocument, "Save document"); CheckBox wrapToggle = new CheckBox("Wrap"); wrapToggle.setSelected(true); area.wrapTextProperty().bind(wrapToggle.selectedProperty()); @@ -70,6 +89,7 @@ public void start(Stage primaryStage) { Button italicBtn = createButton("italic", this::toggleItalic); Button underlineBtn = createButton("underline", this::toggleUnderline); Button strikeBtn = createButton("strikethrough", this::toggleStrikethrough); + Button insertImageBtn = createButton("insertimage", this::insertImage, "Insert Image"); ToggleGroup alignmentGrp = new ToggleGroup(); ToggleButton alignLeftBtn = createToggleButton(alignmentGrp, "align-left", this::alignLeft); ToggleButton alignCenterBtn = createToggleButton(alignmentGrp, "align-center", this::alignCenter); @@ -228,9 +248,10 @@ protected boolean computeValue() { HBox panel1 = new HBox(3.0); HBox panel2 = new HBox(3.0); panel1.getChildren().addAll( + loadBtn, saveBtn, wrapToggle, undoBtn, redoBtn, cutBtn, copyBtn, pasteBtn, boldBtn, italicBtn, underlineBtn, strikeBtn, - alignLeftBtn, alignCenterBtn, alignRightBtn, alignJustifyBtn, + alignLeftBtn, alignCenterBtn, alignRightBtn, alignJustifyBtn, insertImageBtn, paragraphBackgroundPicker); panel2.getChildren().addAll(sizeCombo, familyCombo, textColorPicker, backgroundColorPicker); @@ -247,7 +268,12 @@ protected boolean computeValue() { primaryStage.show(); } + @Deprecated private Button createButton(String styleClass, Runnable action) { + return createButton(styleClass, action, null); + } + + private Button createButton(String styleClass, Runnable action, String toolTip) { Button button = new Button(); button.getStyleClass().add(styleClass); button.setOnAction(evt -> { @@ -256,6 +282,9 @@ private Button createButton(String styleClass, Runnable action) { }); button.setPrefWidth(20); button.setPrefHeight(20); + if (toolTip != null) { + button.setTooltip(new Tooltip(toolTip)); + } return button; } @@ -304,6 +333,89 @@ private void alignJustify() { updateParagraphStyleInSelection(ParStyle.alignJustify()); } + private void loadDocument() { + String initialDir = System.getProperty("user.dir"); + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Load document"); + fileChooser.setInitialDirectory(new File(initialDir)); + File selectedFile = fileChooser.showOpenDialog(mainStage); + if (selectedFile != null) { + area.clear(); + load(selectedFile); + } + } + + private void load(File file) { + if(area.getStyleCodecs().isPresent()) { + Tuple2, Codec> codecs = area.getStyleCodecs().get(); + Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2); + + try { + FileInputStream fis = new FileInputStream(file); + DataInputStream dis = new DataInputStream(fis); + StyledDocument doc = codec.decode(dis); + fis.close(); + + if(doc != null) { + area.replaceSelection(doc); + return; + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + + private void saveDocument() { + String initialDir = System.getProperty("user.dir"); + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Save document"); + fileChooser.setInitialDirectory(new File(initialDir)); + File selectedFile = fileChooser.showSaveDialog(mainStage); + if (selectedFile != null) { + save(selectedFile); + } + } + + + private void save(File file) { + StyledDocument doc = area.getDocument(); + + // Use the Codec to save the document in a binary format + area.getStyleCodecs().ifPresent(codecs -> { + Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2); + try { + FileOutputStream fos = new FileOutputStream(file); + DataOutputStream dos = new DataOutputStream(fos); + codec.encode(dos, doc); + fos.close(); + } catch (IOException fnfe) { + fnfe.printStackTrace(); + } + }); + } + + + /** + * Action listener which inserts a new image at the current caret position. + */ + private void insertImage() { + + String initialDir = System.getProperty("user.dir"); + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Insert image"); + fileChooser.setInitialDirectory(new File(initialDir)); + File selectedFile = fileChooser.showOpenDialog(mainStage); + if (selectedFile != null) { + String imagePath = selectedFile.getAbsolutePath(); + ReadOnlyStyledDocument ros = + ReadOnlyStyledDocument.from(new LinkedImage<>(imagePath, TextStyle.EMPTY), + ParStyle.EMPTY); + area.replaceSelection(ros); + } + } + private void updateStyleInSelection(Function, TextStyle> mixinGetter) { IndexRange selection = area.getSelection(); if(selection.getLength() != 0) { diff --git a/richtextfx-demos/src/main/resources/org/fxmisc/richtext/demo/richtext/insertimage.png b/richtextfx-demos/src/main/resources/org/fxmisc/richtext/demo/richtext/insertimage.png new file mode 100644 index 0000000000000000000000000000000000000000..5cdf307df24a093ca491ae9777fc112ef1c28ad9 GIT binary patch literal 1085 zcmV-D1j74?P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000B5NklpZ3-!sv!}5^9}6v^Z=Co3{ey?O49J6+`wDP+tY=t^w-ddZ^BQ zh+3Bib(Co-t03XbomfJTA3wT+&|X=JpHtzpWTExi9n`yl2KOSWjR~ke9S61f z7#gOQprgEUlneieEo`_PK>bt*`Wc|n3+QNX_5e*&w{iS@E);E>pg0weGJ^!#u`_7@ z1*jsS*B_(d7zy>`0kn8R=)3_K=wc3^o5qJw*suY``b{X-ZAOuPBg#!5q3ag?mbTw< zBbHDgVbesABq4PBfU^%^^v*$H+=HTu4XFD1B@{GnMzdhT#oqvpa|yTZ{)tps`+cE= zgpL_MPPqiMT)~ZMXRX*sxRVDJywI}U!Ngp*2%mXYZhk8)ImkX|iPDJh?`0sg0$~7W1T7Bs zJd5+5!*q&XJUepkc}Qd|i=J{_mW#eJ-M1oqU6^b^w&`&k8DPlkW3c+PB78nd*?uEi zO3a~#DN<#oRMqE`Y0a~;6D!d_`=zRF_7nB`VDTVB5ixkPM1;>g`&h{)H@?eW=MVa6 z;uYf6NAOzw?$z0ubPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000B)Nklged6P1`M}v z`enq7smXM4W@f`r|2^*sTh^qFJIOES%{lM;JJ0jH&wIF2Vo?aOo!AkO4AEOc;n5K8 z|6a6c`{^vPyP!yh;?jIS*fU91rZy$@i=A83Uw^SE*utrs1DKg}`@x=-lqrxQ7U^i- ziJ)YN4iazvD`e5!oEd<=x5*FpkOAszmr+$yjtkZ20-`K0Mt1J$H4;Yq2`8tWXi?Xo zQB{fRi)E;|P|VY)x`eAObpcVgHlW9#K~aeU>3XGEQVr>}}IFA=TcyJ$g?kog_W$=$MU=2sdtf;Iiq-M^AcVZk*pZYqdTg;JO-o%BKqsayhUpBWvUZIpfiKbd`yWx+~Q zR3b-VF+)Zlmu}rvmwZ@Rox$Q=FK#b* zFn?4`y9?0|a{!Bk5`hd&I$^2!RkHUl7 zi^C)s#N50avvd6vi3`&{2gb(D7#=Z_#DIQx51g)U*z8@Dg$C|HBedOW}eBJ4vX^e=7*vTC~aV)d6tl+WJWq`w}6RuX-M?w%It z^vxtwp-bCD!UnXp*V6`8LXs_hcHqMU%FxizaBlzpeea~Ces)DF&!xiVLZ#zn^` z9EpyLkKG#+E7-?iWA;2DDSl4^-H*h@?iIuwet&n=uJ^WW3k%~oF63`a27Qand;+lm O0000Px#1ZP1_K>z@;j|==^1poj532;bRa{vGXbpQYZbpg<9qoDu*02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qQF!XYv000CJNkl8 zvJ5l8SQtQ-VHpby{r5YUE2HUSdy`+jJ2~h7yWctY%oPBL{BIYRsa0yt<>X|RpI^XO z&(G}9!00xQ2lT9e(7*$5CZk|R`3WDN; zM0Pqj1i87_pdFdO=%jfYy@X-Pj4{&^#;2DsF|&*(v&+yKjL82iAD?{mF|MSln4}W~ zWuk&5#GfZ&9)|QrEUv6!f#8*6Ufp8JvW8{LI#!>p!}8Y#^kYU`qb5F7CLtyH^2_OG_&u>HRqN&^HmsXi->_m>KaQNMx!_sq(k8rg;UXiT(PUGH z=2grtu3~0k1*Z8GOwBFh_W>O;(lf!c{5?oIL5PoyV+7LD(($Wy1cT#GiCMA!L6tKw zwuRO(gMQ+%eg;229E4i^DR`EzQql=R?AddSfJ&uC--AJD$7To$i);x*3GqljjfbPt zc%U<(`xh-#saL?W332f@;n}EYMj$a!iEnD|p!T~K;`?oC=y#1&tM9hp+q?I1`_6rQ zeY=4&3h-=POsq|KCOm=>c#)`xNRjBsGymfG*o*VeQB#sm5W+*l7=ix+J-{Xm4Gv)h zp7X_x8~C#1CTL>gsCv%4Cpvdn3TlU(AIB2HAxEa$iP(!v{M$I{3MQ+}j7S zF)`@r=|OjQH@dpI#IcHs3b8x6kBr1}F?Y)Uq)q5^`~)Mw@g+%HTN|CNSa`a7Ad)t` zy}cc+t*vNoZWcS{TOoVT8{SfeugYI0w+W9O_3R|T;S*X~S`ZZ(31=rK1P2A7si_GK z4GpNRtwm*JB|g7?o#q}DANa1P7XoNMX-{oJcNe#-?yg7HJ=~7qI8CUpuLphKpj0U7 z1!z!JRV6O7tgH;BrKR+r*TD6#GhCbw;~f`QobWmZFL#e0NcAfBy|U-^J^S{*xv$}n zgX5&To15vYTer;R<>f0Ru#jM_q@-lMxVU(&sHo^!VPWB_Dm8V{@ojsP!~VBS2kjlE z4mlo}ayoFZo?N@QkYBIu+_A%L_pV)G4)*q8NlImGYD!9cMtb^1IwFbL+1UzSvobR; y(bqX4B0N0$jlFwAciY*8ON80k**Ob>u=5|Kv0*7qs%sqp0000 par, BiConsumer applyS // } // }); - // populate with text nodes - for(StyledText segment: par.getSegments()) { - TextExt t = new TextExt(segment.getText()); - t.setTextOrigin(VPos.TOP); - t.getStyleClass().add("text"); - applyStyle.accept(t, segment.getStyle()); - - // XXX: binding selectionFill to textFill, - // see the note at highlightTextFill - t.impl_selectionFillProperty().bind(t.fillProperty()); + // populate with nodes + for(Segment segment: par.getSegments()) { + // Create the object node + Node t = segment.createNode(); getChildren().add(t); // add corresponding background node (empty) diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java index e1c1fa16a..7628d6491 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java @@ -35,8 +35,11 @@ import javafx.geometry.Bounds; import javafx.geometry.Insets; import javafx.geometry.Point2D; +import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.control.IndexRange; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.Background; import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.CornerRadii; @@ -56,6 +59,7 @@ import org.fxmisc.richtext.model.Codec; import org.fxmisc.richtext.model.EditActions; import org.fxmisc.richtext.model.EditableStyledDocument; +import org.fxmisc.richtext.model.LinkedImage; import org.fxmisc.richtext.model.NavigationActions; import org.fxmisc.richtext.model.Paragraph; import org.fxmisc.richtext.model.PlainTextChange; @@ -63,6 +67,7 @@ import org.fxmisc.richtext.model.SimpleEditableStyledDocument; import org.fxmisc.richtext.model.StyleSpans; import org.fxmisc.richtext.model.StyledDocument; +import org.fxmisc.richtext.model.StyledText; import org.fxmisc.richtext.model.StyledTextAreaModel; import org.fxmisc.richtext.model.TextEditingArea; import org.fxmisc.richtext.model.TwoDimensional; @@ -578,12 +583,34 @@ public StyledTextArea(PS initialParagraphStyle, BiConsumer applyPa public StyledTextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, S initialTextStyle, BiConsumer applyStyle, - EditableStyledDocument document, boolean preserveStyle - ) { + EditableStyledDocument document, boolean preserveStyle) { this.model = new StyledTextAreaModel<>(initialParagraphStyle, initialTextStyle, document, preserveStyle); this.applyStyle = applyStyle; this.applyParagraphStyle = applyParagraphStyle; + // Inject node factories into model layer + StyledText.setNodeFactory(segment -> { + TextExt t = new TextExt(segment.getText()); + t.setTextOrigin(VPos.TOP); + t.getStyleClass().add("text"); + this.applyStyle.accept(t, segment.getStyle()); + + // XXX: binding selectionFill to textFill, + // see the note at highlightTextFill + t.impl_selectionFillProperty().bind(t.fillProperty()); + + return t; + }); + + LinkedImage.setNodeFactory(segment -> { + String imagePath = segment.getImagePath(); + Image image = new Image("file:" + imagePath); // XXX: No need to create new Image objects each time - + // could be cached in the model layer + + ImageView result = new ImageView(image); + return result; + }); + // allow tab traversal into area setFocusTraversable(true); @@ -1043,7 +1070,7 @@ public void setStyle(int paragraph, int from, int to, S style) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int from, StyleSpans styleSpans) { + public void setStyleSpans(int from, StyleSpans styleSpans) { model.setStyleSpans(from, styleSpans); } @@ -1057,7 +1084,7 @@ public void setStyleSpans(int from, StyleSpans styleSpans) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { model.setStyleSpans(paragraph, from, styleSpans); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java new file mode 100644 index 000000000..c300c31ca --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java @@ -0,0 +1,105 @@ +package org.fxmisc.richtext.model; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * This is the base class for custom objects in the model layer. + * Its String representation is always one character long and contains + * the "object replacement character" (\ufffc). + */ +public abstract class CustomObject implements Segment { + + protected S style; + + protected CustomObject() {} + + public CustomObject(S style) { + this.style = style; + } + + + @Override + public Segment subSequence(int start, int end) { + if (start == 0 && end == 1) { + return this; + } + return new StyledText<>("", style); + } + + + @Override + public Segment subSequence(int start) { + if (start == 1) { + return new StyledText<>("", style); + } + return this; + } + + + @Override + public CustomObject append(String str) { + throw new UnsupportedOperationException(); + // return new StyledText<>(text + str, style); + } + + + @Override + public CustomObject spliced(int from, int to, CharSequence replacement) { + throw new UnsupportedOperationException(); +/* String left = text.substring(0, from); + String right = text.substring(to); + return new StyledText<>(left + replacement + right, style);*/ + } + + + @Override + public int length() { + return 1; + } + + + @Override + public char charAt(int index) { + return getText().charAt(0); + } + + + @Override + public String getText() { + return "\ufffc"; + } + + + @Override + public S getStyle() { + return style; + } + + @Override + public void setStyle(S style) { + this.style = style; + } + + public abstract void encode(DataOutputStream os) throws IOException; + + @Override + public final void encode(DataOutputStream os, Codec styleCodec) throws IOException { + encode(os); + styleCodec.encode(os, style); + } + + public abstract void decode(DataInputStream is) throws IOException; + + @Override + public final void decode(DataInputStream is, Codec styleCodec) throws IOException { + decode(is); + style = styleCodec.decode(is); + } + + @Override + public boolean canJoin(Segment right) { + return false; + } +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java index 99512a299..e478242eb 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java @@ -72,9 +72,9 @@ default EventStream plainChanges() { void setStyle(int paragraph, int fromCol, int toCol, S style); - void setStyleSpans(int from, StyleSpans styleSpens); + void setStyleSpans(int from, StyleSpans styleSpens); - void setStyleSpans(int paragraph, int from, StyleSpans styleSpens); + void setStyleSpans(int paragraph, int from, StyleSpans styleSpens); void setParagraphStyle(int parIdx, PS style); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java new file mode 100644 index 000000000..7d4887add --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java @@ -0,0 +1,81 @@ +package org.fxmisc.richtext.model; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; +import java.util.function.Function; + +import javafx.scene.Node; + + +/** + * A custom object which contains a file path to an image file. + * When rendered in the rich text editor, the image is loaded from the + * specified file. + */ +public class LinkedImage extends CustomObject { + + private String imagePath; + + LinkedImage() {} + + /** + * 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 LinkedImage(String imagePath, S style) { + super(style); + + // if the image is below the current working directory, + // then store as relative path name. + String currentDir = System.getProperty("user.dir") + File.separatorChar; + if (imagePath.startsWith(currentDir)) { + imagePath = imagePath.substring(currentDir.length()); + } + + this.imagePath = imagePath; + } + + + /** + * @return The path of the image to render. + */ + public String getImagePath() { + return imagePath; + } + + + @Override + public void encode(DataOutputStream os) throws IOException { + Codec.STRING_CODEC.encode(os, imagePath); + } + + + @Override + public void decode(DataInputStream is) throws IOException { + imagePath = Codec.STRING_CODEC.decode(is); + } + + + @Override + public String toString() { + return String.format("LinkedImage[path=%s]", imagePath); + } + + + @SuppressWarnings("rawtypes") + private static Function nodeFactory; + + @Override + public Node createNode() { + return nodeFactory.apply(this); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static void setNodeFactory(Function, Node> nodeFactory) { + LinkedImage.nodeFactory = (Function) (Object) nodeFactory; + } +} 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 093c7447f..acca8bb16 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -25,7 +26,7 @@ private static List list(T head, T... tail) { } } - private final List> segments; + private final List> segments; private final TwoLevelNavigator navigator; private final PS paragraphStyle; @@ -34,11 +35,11 @@ public Paragraph(PS paragraphStyle, String text, S style) { } @SafeVarargs - public Paragraph(PS paragraphStyle, StyledText text, StyledText... texts) { + public Paragraph(PS paragraphStyle, Segment text, Segment... texts) { this(paragraphStyle, list(text, texts)); } - Paragraph(PS paragraphStyle, List> segments) { + Paragraph(PS paragraphStyle, List> segments) { assert !segments.isEmpty(); this.segments = segments; this.paragraphStyle = paragraphStyle; @@ -46,7 +47,7 @@ public Paragraph(PS paragraphStyle, StyledText text, StyledText... texts) i -> segments.get(i).length()); } - public List> getSegments() { + public List> getSegments() { return Collections.unmodifiableList(segments); } @@ -57,7 +58,7 @@ public PS getParagraphStyle() { private int length = -1; public int length() { if(length == -1) { - length = segments.stream().mapToInt(StyledText::length).sum(); + length = segments.stream().mapToInt(Segment::length).sum(); } return length; } @@ -90,17 +91,17 @@ public Paragraph concat(Paragraph p) { return p; } - StyledText left = segments.get(segments.size() - 1); - StyledText right = p.segments.get(0); - if(Objects.equals(left.getStyle(), right.getStyle())) { - StyledText segment = left.append(right.getText()); - List> segs = new ArrayList<>(segments.size() + p.segments.size() - 1); + Segment left = segments.get(segments.size() - 1); + Segment right = p.segments.get(0); + if(left.canJoin(right)) { + Segment segment = left.append(right.getText()); + 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, segs); } else { - List> segs = new ArrayList<>(segments.size() + p.segments.size()); + List> segs = new ArrayList<>(segments.size() + p.segments.size()); segs.addAll(segments); segs.addAll(p.segments); return new Paragraph<>(paragraphStyle, segs); @@ -122,7 +123,7 @@ public Paragraph append(String str) { return this; } - List> segs = new ArrayList<>(segments); + List> segs = new ArrayList<>(segments); int lastIdx = segments.size() - 1; segs.set(lastIdx, segments.get(lastIdx).append(str)); return new Paragraph<>(paragraphStyle, segs); @@ -136,9 +137,9 @@ public Paragraph insert(int offset, CharSequence str) { 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); + Segment seg = segments.get(segIdx); + Segment replacement = seg.spliced(segPos, segPos, str); + List> segs = new ArrayList<>(segments); segs.set(segIdx, replacement); return new Paragraph<>(paragraphStyle, segs); } @@ -153,7 +154,7 @@ public Paragraph trim(int length) { } else { Position pos = navigator.offsetToPosition(length, Backward); int segIdx = pos.getMajor(); - List> segs = new ArrayList<>(segIdx + 1); + List> segs = new ArrayList<>(segIdx + 1); segs.addAll(segments.subList(0, segIdx)); segs.add(segments.get(segIdx).subSequence(0, pos.getMinor())); return new Paragraph<>(paragraphStyle, segs); @@ -168,7 +169,7 @@ public Paragraph subSequence(int start) { } else if(start <= length()) { Position pos = navigator.offsetToPosition(start, Forward); int segIdx = pos.getMajor(); - List> segs = new ArrayList<>(segments.size() - segIdx); + 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<>(paragraphStyle, segs); @@ -197,26 +198,57 @@ public Paragraph restyle(int from, int to, S style) { } } - public Paragraph restyle(int from, StyleSpans styleSpans) { + 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); + // Step 1: Restyle all segments according to the corresponding style span + // + // Assumptions: + // * borders of segments are aligned to borders of spans + // * One style span can span multiple segments + // * One segment can (obviously) not span multiple StyleSpans + List> intermediateSegs = new ArrayList<>(segments.size()); + int segOff = 0; + Iterator> spans = styleSpans.iterator(); + StyleSpan span = spans.next(); + int spanEnd = span.getLength(); + for (Segment seg : segments) { + seg.setStyle(span.getStyle()); + + intermediateSegs.add(seg); + + segOff = segOff + seg.length(); + if (segOff >= spanEnd && spans.hasNext()) { + span = (StyleSpan) spans.next(); + spanEnd += span.getLength(); + } + } - 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; + // Step 2: Join segments with the same style + // + // Assumptions: + // * There is at least one segment available + List> middleSegs = new ArrayList<>(intermediateSegs.size()); + Iterator> segs = intermediateSegs.iterator(); + Segment currentSeg = (Segment) segs.next(); + while(segs.hasNext()) { + Segment nextSeg = (Segment) segs.next(); + if (currentSeg.canJoin(nextSeg)) { + currentSeg = currentSeg.append(nextSeg.getText()); + } else { + middleSegs.add(currentSeg); + currentSeg = nextSeg; + } } + middleSegs.add(currentSeg); + Paragraph middle = new Paragraph<>(paragraphStyle, middleSegs); + Paragraph left = trim(from); + Paragraph right = subSequence(from + len); return left.concat(middle).concat(right); } @@ -273,7 +305,7 @@ public IndexRange getStyleRangeAtPosition(int position) { public StyleSpans getStyleSpans() { StyleSpansBuilder builder = new StyleSpansBuilder<>(segments.size()); - for(StyledText seg: segments) { + for(Segment seg: segments) { builder.add(seg.getStyle(), seg.length()); } return builder.create(); @@ -291,18 +323,18 @@ public StyleSpans getStyleSpans(int from, int to) { StyleSpansBuilder builder = new StyleSpansBuilder<>(n); if(startSegIdx == endSegIdx) { - StyledText seg = segments.get(startSegIdx); + Segment seg = segments.get(startSegIdx); builder.add(seg.getStyle(), to - from); } else { - StyledText startSeg = segments.get(startSegIdx); + Segment 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); + Segment seg = segments.get(i); builder.add(seg.getStyle(), seg.length()); } - StyledText endSeg = segments.get(endSegIdx); + Segment endSeg = segments.get(endSegIdx); builder.add(endSeg.getStyle(), end.getMinor()); } @@ -317,7 +349,7 @@ public StyleSpans getStyleSpans(int from, int to) { public String getText() { if(text == null) { StringBuilder sb = new StringBuilder(length()); - for(StyledText seg: segments) + for(Segment seg: segments) sb.append(seg.getText()); text = sb.toString(); } @@ -328,7 +360,7 @@ public String getText() { public String toString() { return "Par[" + paragraphStyle + "; " + - segments.stream().map(StyledText::toString) + segments.stream().map(Segment::toString) .reduce((s1, s2) -> s1 + "," + s2).orElse("") + "]"; } 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 1093cec5c..562b52bd6 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java @@ -7,6 +7,7 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.function.BiFunction; @@ -94,6 +95,22 @@ public static ReadOnlyStyledDocument from(StyledDocument d } } + /** + * Creates a new ReadOnlyStyledDocument with one Segment. + * The resulting ReadOnlyStyledDocument can be inserted or appended to a StyledTextArea. + * + * @param seg The segment which shall be contained in the document. + * @param paragraphStyle The paragraph style to use for the paragraph which contains the segment. + * + * @return A ReadOnlyStyledDocument with the given segment. + */ + public static ReadOnlyStyledDocument from(Segment seg, PS paragraphStyle) { + List> res = new ArrayList<>(1); + Paragraph content = new Paragraph(paragraphStyle, Arrays.asList(seg)); + res.add(content); + return new ReadOnlyStyledDocument<>(res); + } + public static Codec> codec(Codec pCodec, Codec tCodec) { return new Codec>() { private final Codec>> codec = Codec.listCodec(paragraphCodec(pCodec, tCodec)); @@ -118,7 +135,7 @@ public StyledDocument decode(DataInputStream is) throws IOException { private static Codec> paragraphCodec(Codec pCodec, Codec tCodec) { return new Codec>() { - private final Codec>> segmentsCodec = Codec.listCodec(styledTextCodec(tCodec)); + private final Codec>> segmentsCodec = Codec.listCodec(styledTextCodec(tCodec)); @Override public String getName() { @@ -134,14 +151,14 @@ public void encode(DataOutputStream os, Paragraph p) throws IOException { @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, segments); } }; } - private static Codec> styledTextCodec(Codec styleCodec) { - return new Codec>() { + private static Codec> styledTextCodec(Codec styleCodec) { + return new Codec>() { @Override public String getName() { @@ -149,16 +166,24 @@ public String getName() { } @Override - public void encode(DataOutputStream os, StyledText t) throws IOException { - STRING_CODEC.encode(os, t.getText()); - styleCodec.encode(os, t.getStyle()); + public void encode(DataOutputStream os, Segment t) throws IOException { + // encode the segment type and content + STRING_CODEC.encode(os, t.getClass().getName()); + t.encode(os, styleCodec); } @Override - public StyledText decode(DataInputStream is) throws IOException { - String text = STRING_CODEC.decode(is); - S style = styleCodec.decode(is); - return new StyledText<>(text, style); + public Segment decode(DataInputStream is) throws IOException { + String segmentType = is.readUTF(); + try { + @SuppressWarnings("unchecked") + Class> clazz = (Class>) Class.forName(segmentType); + Segment result = (Segment) clazz.newInstance(); + result.decode(is, styleCodec); + return result; + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + throw new IOException("Could not create Segment for " + segmentType, e); + } } }; diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java new file mode 100644 index 000000000..46d71c85a --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java @@ -0,0 +1,40 @@ +package org.fxmisc.richtext.model; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import javafx.scene.Node; + +/** + * An interface to segment types, like StyledText or LinkedImage. + */ +public interface Segment { + + int length(); + + char charAt(int index); + + String getText(); // each segment has a string associated with it - for custom objects + // this is the replacement character \ufffc + + Segment subSequence(int start, int end); + + Segment subSequence(int start); + + Segment append(String str); + + Segment spliced(int from, int to, CharSequence replacement); + + S getStyle(); + + void encode(DataOutputStream os, Codec styleCodec) throws IOException; + + void decode(DataInputStream is, Codec styleCodec) throws IOException; + + Node createNode(); + + boolean canJoin(Segment right); + + void setStyle(S style); +} 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 5b7e5422a..988b7714b 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java @@ -135,7 +135,7 @@ public void setStyle(int paragraph, int fromCol, int toCol, S style) { } @Override - public void setStyleSpans(int from, StyleSpans styleSpans) { + public void setStyleSpans(int from, StyleSpans styleSpans) { int len = styleSpans.length(); ensureValidRange(from, from + len); doc.replace(from, from + len, d -> { @@ -143,7 +143,7 @@ public void setStyleSpans(int from, StyleSpans styleSpans) { List> pars = new ArrayList<>(d.getParagraphs().size()); for(Paragraph p: d.getParagraphs()) { Position j = i.offsetBy(p.length(), Backward); - StyleSpans spans = styleSpans.subView(i, j); + StyleSpans spans = styleSpans.subView(i, j); pars.add(p.restyle(0, spans)); i = j.offsetBy(1, Forward); // skip the newline } @@ -152,7 +152,7 @@ public void setStyleSpans(int from, StyleSpans styleSpans) { } @Override - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { setStyleSpans(doc.position(paragraph, from).toOffset(), styleSpans); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpan.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpan.java index 3eb9cd9fa..d70cd398c 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpan.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyleSpan.java @@ -43,4 +43,9 @@ public boolean equals(Object other) { public int hashCode() { return Objects.hash(style, length); } + + @Override + public String toString() { + return String.format("StyleSpan[length=%s, style=%s]", length, style); + } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java index 418138e7f..918c81abd 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java @@ -1,53 +1,74 @@ package org.fxmisc.richtext.model; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; import java.util.Objects; +import java.util.function.Function; -public class StyledText { - private final String text; - private final S style; +import javafx.scene.Node; + +public class StyledText implements Segment { + private String text; + private S style; + + StyledText() {} public StyledText(String text, S style) { this.text = text; this.style = style; } + @Override public int length() { return text.length(); } + @Override public char charAt(int index) { return text.charAt(index); } + @Override public String getText() { return text; } + @Override public StyledText subSequence(int start, int end) { return new StyledText<>(text.substring(start, end), style); } + @Override public StyledText subSequence(int start) { return new StyledText<>(text.substring(start), style); } + @Override public StyledText append(String str) { return new StyledText<>(text + str, style); } + @Override public StyledText spliced(int from, int to, CharSequence replacement) { String left = text.substring(0, from); String right = text.substring(to); return new StyledText<>(left + replacement + right, style); } + @Override public S getStyle() { return style; } + @Override + public void setStyle(S style) { + this.style = style; + } + @Override public String toString() { - return '"' + text + '"' + ":" + style; + return String.format("StyledText[text=\"%s\", style=%s]", text, style); } @Override @@ -65,4 +86,42 @@ public boolean equals(Object obj) { public int hashCode() { return Objects.hash(text, style); } + + + @Override + public boolean canJoin(Segment right) { + + if (right instanceof StyledText) { + return Objects.equals(getStyle(), right.getStyle()); + } + + return false; + } + + + @Override + public void encode(DataOutputStream os, Codec styleCodec) throws IOException { + Codec.STRING_CODEC.encode(os, getText()); + styleCodec.encode(os, style); + } + + @Override + public void decode(DataInputStream is, Codec styleCodec) throws IOException { + text = Codec.STRING_CODEC.decode(is); + style = styleCodec.decode(is); + } + + @SuppressWarnings("rawtypes") + private static Function nodeFactory; + + @Override + public Node createNode() { + return nodeFactory.apply(this); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static void setNodeFactory(Function, Node> nodeFactory) { + StyledText.nodeFactory = (Function) (Object) nodeFactory; + } + } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index 301abadb9..3f0afc401 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -562,7 +562,7 @@ public void setStyle(int paragraph, int from, int to, S style) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int from, StyleSpans styleSpans) { + public void setStyleSpans(int from, StyleSpans styleSpans) { content.setStyleSpans(from, styleSpans); } @@ -576,7 +576,7 @@ public void setStyleSpans(int from, StyleSpans styleSpans) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { content.setStyleSpans(paragraph, from, styleSpans); } diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java new file mode 100644 index 000000000..d383e16d1 --- /dev/null +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java @@ -0,0 +1,103 @@ +package org.fxmisc.richtext.model; + +import static org.junit.Assert.*; + +import java.util.List; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; + +public class CustomObjectTest { + + @Test + public void testLinkedImageCreation() { + SimpleEditableStyledDocument doc = + new SimpleEditableStyledDocument<>(true, ""); + StyledDocument customObj = + ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); + doc.replace(0, 0, customObj); + assertEquals(1, doc.getLength()); + + Paragraph para = doc.getParagraphs().get(0); + Object x = para.getSegments().get(0); + assertThat(x, instanceOf(LinkedImage.class)); + } + + @Test + public void testMultipleSegments() { + SimpleEditableStyledDocument doc = new SimpleEditableStyledDocument<>(true, ""); + + final String helloWorld = "Hello World"; + final String helloMoon = "Hello Moon"; + + StyledDocument text = ReadOnlyStyledDocument.fromString(helloWorld, true, ""); + doc.replace(0, 0, text); + + StyledDocument customObj = ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); + doc.replace(doc.getLength(), doc.getLength(), customObj); + + StyledDocument text2 = ReadOnlyStyledDocument.fromString(helloMoon, true, ""); + doc.replace(doc.getLength(), doc.getLength(), text2); + + // Assert that the document now contains one paragraph with three segments + + assertThat(doc.getParagraphs().size(), equalTo(1)); + + Paragraph p = doc.getParagraphs().get(0); + List> segs = p.getSegments(); + + assertThat(segs.size(), equalTo(3)); + assertThat(segs.get(0).getText(), equalTo(helloWorld)); + assertThat(segs.get(1).getText(), equalTo("\ufffc")); + assertThat(segs.get(2).getText(), equalTo(helloMoon)); + } + + + @Test + public void testRestyle() { + SimpleEditableStyledDocument doc = new SimpleEditableStyledDocument<>(true, ""); + + final String helloWorld = "Hello World"; + final String helloMoon = "Hello Moon"; + + StyledDocument text = ReadOnlyStyledDocument.fromString(helloWorld, true, "bold"); + doc.replace(0, 0, text); + + StyledDocument customObj = ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); + doc.replace(doc.getLength(), doc.getLength(), customObj); + + StyledDocument text2 = ReadOnlyStyledDocument.fromString(helloMoon, true, "bold"); + doc.replace(doc.getLength(), doc.getLength(), text2); + + // The document now contains one paragraph with three segments + // Restyle part of the document: + + StyleSpans styles = doc.getStyleSpans(6, 17); + assertThat("Invalid number of Spans", styles.getSpanCount(), equalTo(3)); + + StyleSpans newStyles = styles.mapStyles(style -> "italic"); + doc.setStyleSpans(6, newStyles); + + // Assert that the document now contains one paragraph with five segments + // StyledText[text="Hello ", style=bold] + // StyledText[text="World", style=italic] + // LinkedImage[path=sample.png] + // StyledText[text="Hello", style=italic] + // StyledText[text=" Moon", style=bold] + + assertThat(doc.getParagraphs().size(), equalTo(1)); + List> segs = doc.getParagraphs().get(0).getSegments(); + + assertThat(segs.size(), equalTo(5)); + assertThat(segs.get(0).getText(), equalTo("Hello ")); + assertThat(segs.get(1).getText(), equalTo("World")); + assertThat(segs.get(2).getText(), equalTo("\ufffc")); + assertThat(segs.get(2), instanceOf(LinkedImage.class)); + assertThat(segs.get(3).getText(), equalTo("Hello")); + assertThat(segs.get(4).getText(), equalTo(" Moon")); + } + +} 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 71f2de0f2..bbb60175b 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java @@ -1,6 +1,8 @@ package org.fxmisc.richtext.model; import static org.fxmisc.richtext.model.ReadOnlyStyledDocument.*; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.*; import java.util.List; @@ -38,4 +40,38 @@ public void deleteNewlineTest() { }); } + @Test + public void testRestyle() { + final String fooBar = "Foo Bar"; + final String and = " and "; + final String helloWorld = "Hello World"; + + SimpleEditableStyledDocument doc0 = new SimpleEditableStyledDocument<>("", ""); + + ReadOnlyStyledDocument text = fromString(fooBar, "", "bold"); + doc0.replace(doc0.getLength(), doc0.getLength(), text); + + text = fromString(and, "", ""); + doc0.replace(doc0.getLength(), doc0.getLength(), text); + + text = fromString(helloWorld, "", "bold"); + doc0.replace(doc0.getLength(), doc0.getLength(), text); + + StyleSpans styles = doc0.getStyleSpans(4, 17); + assertThat("Invalid number of Spans", styles.getSpanCount(), equalTo(3)); + + 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")); + } + } From a9c067bb860fa6e05315b8497cf063beba998300 Mon Sep 17 00:00:00 2001 From: Andreas Fester Date: Thu, 15 Sep 2016 13:12:22 +0200 Subject: [PATCH 2/9] Replaced StyledText with SEG type parameter --- richtextfx-demos/sample.rtfx | Bin 2129 -> 1695 bytes .../richtext/demo/richtext/LinkedImage.java | 100 ++ .../demo/richtext/LinkedImageOps.java | 49 + .../richtext/demo/richtext/RichText.java | 75 +- .../org/fxmisc/richtext/ClipboardActions.java | 18 +- .../java/org/fxmisc/richtext/CodeArea.java | 3 +- .../org/fxmisc/richtext/CssProperties.java | 2 +- .../fxmisc/richtext/GenericRichtextArea.java | 1295 ++++++++++++++++ .../fxmisc/richtext/GenericStyledArea.java | 1362 +++++++++++++++++ .../fxmisc/richtext/InlineCssTextArea.java | 11 +- .../org/fxmisc/richtext/ParagraphBox.java | 12 +- .../org/fxmisc/richtext/ParagraphText.java | 24 +- .../fxmisc/richtext/StyleClassedTextArea.java | 15 +- .../org/fxmisc/richtext/StyledTextArea.java | 1339 +--------------- .../richtext/StyledTextAreaBehavior.java | 19 +- .../java/org/fxmisc/richtext/model/Codec.java | 31 + .../fxmisc/richtext/model/EditActions.java | 14 +- .../model/EditableStyledDocument.java | 14 +- .../richtext/model/NavigationActions.java | 2 +- .../org/fxmisc/richtext/model/Paragraph.java | 218 ++- .../model/ReadOnlyStyledDocument.java | 200 +-- .../fxmisc/richtext/model/RichTextChange.java | 10 +- .../org/fxmisc/richtext/model/SegmentOps.java | 99 ++ .../model/SimpleEditableStyledDocument.java | 65 +- .../fxmisc/richtext/model/StyledDocument.java | 24 +- .../org/fxmisc/richtext/model/StyledText.java | 158 +- .../fxmisc/richtext/model/StyledText.java.bck | 127 ++ .../richtext/model/StyledTextAreaModel.java | 72 +- .../richtext/model/TextEditingArea.java | 20 +- .../org/fxmisc/richtext/model/TextOps.java | 51 + .../fxmisc/richtext/model/ParagraphTest.java | 8 +- .../model/ReadOnlyStyledDocumentTest.java | 32 +- .../SimpleEditableStyledDocumentTest.java | 44 +- .../model/StyledTextAreaModelTest.java | 14 +- 34 files changed, 3682 insertions(+), 1845 deletions(-) create mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImage.java create mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImageOps.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java diff --git a/richtextfx-demos/sample.rtfx b/richtextfx-demos/sample.rtfx index 40082d6ff25d6ef7bc5e9564cf12025a38807b07..0404f763bc67140c9ba9a8005deda4875a0bb124 100644 GIT binary patch literal 1695 zcmchXJ#rK=5QVh`CZGt(Am{@Oo}9!16a`fTfH6fXYkJpB>1V8#)>~YHkaEU#I0_jz zfT!7AgIy#;U3{++n9_q-`DKK2>ig6?wjWZ;mr*X;UW zCH9#$)$CaNlvn+76GYwB_P%ZYGm?lvMS@-N;TTuu7uuA_b8;!Jt@onjYSyZh21%69 zQgr)4u&TBrFRSY9Uk7-FysaL#f0}i5Vl_#fn%wYtd7T$6l=MlPOPI~3#9BCS-`78d5S4sRaFx_ z$bU(D*O-T|2GrsyuqRDUU>PFJ6ASfsaf`63UGq}>HC7iU-(m)?mg^7SSI? S)BA!FD{QxMd57a%CVm5JDAr>D literal 2129 zcmb_dyN**a5WP`Yv|3S8P*6+(-BrE-DFRWj(uyWPm1bg3k`aE~_1M|vD=5)+{0^T& z#~-lcJQi4ibZ>;48+&5UM=ZTM&++>zNIkNBc zpm}-`6M5iHiU%8AOdy;AJ>?eaLytzy$&3;&M|JnNHT`ioeg1Ek7aTs4J@>ex=1kCc zjwW4LH9vLL>;0mA4JL*SI>!R0Bq;*Xa)h*w!5lUoMX&|OtK#st3$!B1D3j`LKvp&H zZSJFxWVAAjviu#_B!EfY_NF>$PT4NayWDsPnr%3f4mQU+H{rG=1hfkUQbRi_(rirf;5 z;e{HN)_N2uU{+o1jGa=966RyrUg7WEK+fK~to3CQsMr2KZ62nQQ}M$dA5FjC+mK*yH_6%Z{oU|AKavQ-qAKo#V)Tgs&##uP9SbWsZF zR}G#*E?XoGbvcUKQr2FD20Ug(udRAiQ5FRoLJW7Etzzu?Xfz-6)ac`LtBrG^?FS?p zRyt`Y!%E*YJw^NSDA5XTXvz1cM^BG0j`CK|v;T95giPkz_lyn&u3SFojVmo=X2V}yXU)qy* HD1&YRCna5Y 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 new file mode 100644 index 000000000..4a3155a98 --- /dev/null +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImage.java @@ -0,0 +1,100 @@ +package org.fxmisc.richtext.demo.richtext; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.IOException; + +import javafx.scene.Node; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; + +import org.fxmisc.richtext.model.Codec; + + +/** + * A custom object which contains a file path to an image file. + * When rendered in the rich text editor, the image is loaded from the + * specified file. + */ +public class LinkedImage { + + public static Codec> codec(Codec styleCodec) { + return new Codec>() { + + @Override + public String getName() { + return "LinkedImage<" + styleCodec.getName() + ">"; + } + + @Override + public void encode(DataOutputStream os, LinkedImage i) throws IOException { + // external path rep should use forward slashes only + String externalPath = i.imagePath.replace("\\", "/"); + Codec.STRING_CODEC.encode(os, externalPath); + styleCodec.encode(os, i.style); + } + + @Override + public LinkedImage 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 LinkedImage<>(imagePath, style); + } + + }; + } + + 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 LinkedImage(String imagePath, S style) { + + // if the image is below the current working directory, + // then store as relative path name. + String currentDir = System.getProperty("user.dir") + File.separatorChar; + if (imagePath.startsWith(currentDir)) { + imagePath = imagePath.substring(currentDir.length()); + } + + this.imagePath = imagePath; + this.style = style; + } + + public LinkedImage setStyle(S style) { + return new LinkedImage<>(imagePath, style); + } + + + /** + * @return The path of the image to render. + */ + public String getImagePath() { + return imagePath; + } + + public S getStyle() { + return style; + } + + + @Override + public String toString() { + return String.format("LinkedImage[path=%s]", imagePath); + } + + public Node createNode() { + Image image = new Image("file:" + imagePath); // XXX: No need to create new Image objects each time - + // could be cached in the model layer + ImageView result = new ImageView(image); + return result; + } +} 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 new file mode 100644 index 000000000..a8c7869f9 --- /dev/null +++ b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/richtext/LinkedImageOps.java @@ -0,0 +1,49 @@ +package org.fxmisc.richtext.demo.richtext; + +import java.util.Optional; + +import org.fxmisc.richtext.model.SegmentOps; + +public class LinkedImageOps implements SegmentOps, S> { + + @Override + public int length(LinkedImage seg) { + return 1; + } + + @Override + public char charAt(LinkedImage seg, int index) { + return '\ufffc'; + } + + @Override + public String getText(LinkedImage seg) { + return "\ufffc"; + } + + @Override + public LinkedImage subSequence(LinkedImage seg, int start, int end) { + return seg; + } + + @Override + public LinkedImage subSequence(LinkedImage seg, int start) { + return seg; + } + + @Override + public S getStyle(LinkedImage seg) { + return seg.getStyle(); + } + + @Override + public LinkedImage setStyle(LinkedImage seg, S style) { + return seg.setStyle(style); + } + + @Override + public Optional> join(LinkedImage currentSeg, LinkedImage nextSeg) { + return Optional.empty(); + } + +} 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 7905916fd..3b913fbcf 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 @@ -16,18 +16,14 @@ import java.io.IOException; import java.util.List; import java.util.Optional; +import java.util.function.BiConsumer; import java.util.function.Function; -import org.fxmisc.richtext.model.Codec; -import org.fxmisc.richtext.model.LinkedImage; -import org.fxmisc.richtext.model.ReadOnlyStyledDocument; -import org.fxmisc.richtext.model.StyledDocument; -import org.reactfx.util.Tuple2; - import javafx.application.Application; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; import javafx.collections.FXCollections; +import javafx.scene.Node; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; @@ -47,24 +43,45 @@ import javafx.stage.Stage; import org.fxmisc.flowless.VirtualizedScrollPane; +import org.fxmisc.richtext.GenericStyledArea; import org.fxmisc.richtext.StyledTextArea; +import org.fxmisc.richtext.TextExt; +import org.fxmisc.richtext.model.Codec; import org.fxmisc.richtext.model.Paragraph; +import org.fxmisc.richtext.model.ReadOnlyStyledDocument; import org.fxmisc.richtext.model.StyleSpans; +import org.fxmisc.richtext.model.StyledDocument; +import org.fxmisc.richtext.model.StyledText; +import org.fxmisc.richtext.model.TextOps; import org.reactfx.SuspendableNo; +import org.reactfx.util.Either; +import org.reactfx.util.Tuple2; public class RichText extends Application { public static void main(String[] args) { + // The following properties are required on Linux for improved text rendering + //System.setProperty("prism.lcdtext", "false"); + //System.setProperty("prism.text", "t2k"); launch(args); } - private final StyledTextArea area = new StyledTextArea<>( - ParStyle.EMPTY, ( paragraph, style) -> paragraph.setStyle(style.toCss()), - TextStyle.EMPTY.updateFontSize(12).updateFontFamily("Serif").updateTextColor(Color.BLACK), - ( text, style) -> text.setStyle(style.toCss())); + private final TextOps, TextStyle> styledTextOps = StyledText.textOps(); + private final LinkedImageOps linkedImageOps = new LinkedImageOps<>(); + + private final GenericStyledArea, LinkedImage>, 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 + seg -> createNode(seg, (text, style) -> text.setStyle(style.toCss()))); // Node creator and segment style setter { area.setWrapText(true); - area.setStyleCodecs(ParStyle.CODEC, TextStyle.CODEC); + area.setStyleCodecs( + ParStyle.CODEC, + Codec.eitherCodec(StyledText.codec(TextStyle.CODEC), LinkedImage.codec(TextStyle.CODEC))); } private Stage mainStage; @@ -167,7 +184,7 @@ protected boolean computeValue() { int startPar = area.offsetToPosition(selection.getStart(), Forward).getMajor(); int endPar = area.offsetToPosition(selection.getEnd(), Backward).getMajor(); - List> pars = area.getParagraphs().subList(startPar, endPar + 1); + List,LinkedImage>, TextStyle>> pars = area.getParagraphs().subList(startPar, endPar + 1); @SuppressWarnings("unchecked") Optional[] alignments = pars.stream().map(p -> p.getParagraphStyle().alignment).distinct().toArray(Optional[]::new); @@ -255,7 +272,7 @@ protected boolean computeValue() { paragraphBackgroundPicker); panel2.getChildren().addAll(sizeCombo, familyCombo, textColorPicker, backgroundColorPicker); - VirtualizedScrollPane> vsPane = new VirtualizedScrollPane<>(area); + VirtualizedScrollPane,LinkedImage>, TextStyle>> vsPane = new VirtualizedScrollPane<>(area); VBox vbox = new VBox(); VBox.setVgrow(vsPane, Priority.ALWAYS); vbox.getChildren().addAll(panel1, panel2, vsPane); @@ -268,6 +285,16 @@ protected boolean computeValue() { primaryStage.show(); } + + private Node createNode(Either, LinkedImage> seg, + BiConsumer applyStyle) { + if (seg.isLeft()) { + return StyledTextArea.createStyledTextNode(seg.getLeft(), styledTextOps, applyStyle); + } else { + return seg.getRight().createNode(); + } + } + @Deprecated private Button createButton(String styleClass, Runnable action) { return createButton(styleClass, action, null); @@ -347,13 +374,14 @@ private void loadDocument() { private void load(File file) { if(area.getStyleCodecs().isPresent()) { - Tuple2, Codec> codecs = area.getStyleCodecs().get(); - Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2); + Tuple2, Codec, LinkedImage>>> codecs = area.getStyleCodecs().get(); + Codec, LinkedImage>, TextStyle>> + codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2, area.getSegOps()); try { FileInputStream fis = new FileInputStream(file); DataInputStream dis = new DataInputStream(fis); - StyledDocument doc = codec.decode(dis); + StyledDocument, LinkedImage>, TextStyle> doc = codec.decode(dis); fis.close(); if(doc != null) { @@ -380,11 +408,12 @@ private void saveDocument() { private void save(File file) { - StyledDocument doc = area.getDocument(); + StyledDocument, LinkedImage>, TextStyle> doc = area.getDocument(); // Use the Codec to save the document in a binary format area.getStyleCodecs().ifPresent(codecs -> { - Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2); + Codec, LinkedImage>, TextStyle>> codec = + ReadOnlyStyledDocument.codec(codecs._1, codecs._2, doc.getSegOps()); try { FileOutputStream fos = new FileOutputStream(file); DataOutputStream dos = new DataOutputStream(fos); @@ -401,7 +430,6 @@ private void save(File file) { * Action listener which inserts a new image at the current caret position. */ private void insertImage() { - String initialDir = System.getProperty("user.dir"); FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Insert image"); @@ -409,9 +437,10 @@ private void insertImage() { File selectedFile = fileChooser.showOpenDialog(mainStage); if (selectedFile != null) { String imagePath = selectedFile.getAbsolutePath(); - ReadOnlyStyledDocument ros = - ReadOnlyStyledDocument.from(new LinkedImage<>(imagePath, TextStyle.EMPTY), - ParStyle.EMPTY); + imagePath = imagePath.replace('\\', '/'); + ReadOnlyStyledDocument, LinkedImage>, TextStyle> ros = + ReadOnlyStyledDocument.fromSegment(Either.right(new LinkedImage<>(imagePath, TextStyle.EMPTY)), + ParStyle.EMPTY, TextStyle.EMPTY, area.getSegOps()); area.replaceSelection(ros); } } @@ -440,7 +469,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 paragraph = area.getParagraph(i); + Paragraph,LinkedImage>, TextStyle> paragraph = area.getParagraph(i); area.setParagraphStyle(i, updater.apply(paragraph.getParagraphStyle())); } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java b/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java index f6f90f48b..1c852fca6 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java @@ -17,6 +17,7 @@ import org.fxmisc.richtext.model.Codec; import org.fxmisc.richtext.model.EditActions; import org.fxmisc.richtext.model.ReadOnlyStyledDocument; +import org.fxmisc.richtext.model.SegmentOps; import org.fxmisc.richtext.model.StyledDocument; import org.fxmisc.richtext.model.TextEditingArea; import org.reactfx.util.Tuple2; @@ -24,9 +25,10 @@ /** * Clipboard actions for {@link TextEditingArea}. */ -public interface ClipboardActions extends EditActions { +public interface ClipboardActions extends EditActions { - Optional, Codec>> getStyleCodecs(); + Optional, Codec>> getStyleCodecs(); + SegmentOps getSegOps(); /** * Transfers the currently selected text to the clipboard, @@ -50,9 +52,9 @@ default void copy() { content.putString(getSelectedText()); getStyleCodecs().ifPresent(codecs -> { - Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2); + Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2, getSegOps()); DataFormat format = dataFormat(codec.getName()); - StyledDocument doc = subDocument(selection.getStart(), selection.getEnd()); + StyledDocument doc = subDocument(selection.getStart(), selection.getEnd()); ByteArrayOutputStream os = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(os); try { @@ -77,14 +79,14 @@ default void paste() { Clipboard clipboard = Clipboard.getSystemClipboard(); if(getStyleCodecs().isPresent()) { - Tuple2, Codec> codecs = getStyleCodecs().get(); - Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2); + Tuple2, Codec> codecs = getStyleCodecs().get(); + Codec> codec = ReadOnlyStyledDocument.codec(codecs._1, codecs._2, getSegOps()); DataFormat format = dataFormat(codec.getName()); if(clipboard.hasContent(format)) { byte[] bytes = (byte[]) clipboard.getContent(format); ByteArrayInputStream is = new ByteArrayInputStream(bytes); DataInputStream dis = new DataInputStream(is); - StyledDocument doc = null; + StyledDocument doc = null; try { doc = codec.decode(dis); } catch (IOException e) { @@ -116,4 +118,4 @@ static DataFormat dataFormat(String name) { return new DataFormat(name); } } -} \ No newline at end of file +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/CodeArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/CodeArea.java index 6d59f0bf7..7badd7f08 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/CodeArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/CodeArea.java @@ -4,6 +4,7 @@ import java.util.Collection; import org.fxmisc.richtext.model.EditableStyledDocument; +import org.fxmisc.richtext.model.StyledText; /** * A convenience subclass of {@link StyleClassedTextArea} @@ -22,7 +23,7 @@ public class CodeArea extends StyleClassedTextArea { setUseInitialStyleForInsertion(true); } - public CodeArea(EditableStyledDocument, Collection> document) { + public CodeArea(EditableStyledDocument, StyledText>, Collection> document) { super(document, false); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java b/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java index 6cb7505a4..5219f7df5 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java @@ -12,7 +12,7 @@ /** - * CSS stuff related to {@link StyledTextArea}. + * CSS stuff related to {@link GenericRichtextArea}. */ class CssProperties { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java new file mode 100644 index 000000000..a1f473aff --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java @@ -0,0 +1,1295 @@ +package org.fxmisc.richtext; + +import static javafx.util.Duration.*; +import static org.fxmisc.richtext.PopupAlignment.*; +import static org.reactfx.EventStreams.*; +import static org.reactfx.util.Tuples.*; + +import java.time.Duration; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.IntConsumer; +import java.util.function.IntFunction; +import java.util.function.IntSupplier; +import java.util.function.IntUnaryOperator; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import javafx.beans.binding.Binding; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; +import javafx.css.PseudoClass; +import javafx.css.StyleableObjectProperty; +import javafx.event.Event; +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.IndexRange; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import javafx.stage.PopupWindow; + +import org.fxmisc.flowless.Cell; +import org.fxmisc.flowless.VirtualFlow; +import org.fxmisc.flowless.VirtualFlowHit; +import org.fxmisc.flowless.Virtualized; +import org.fxmisc.flowless.VirtualizedScrollPane; +import org.fxmisc.richtext.CssProperties.EditableProperty; +import org.fxmisc.richtext.model.Codec; +import org.fxmisc.richtext.model.EditActions; +import org.fxmisc.richtext.model.EditableStyledDocument; +import org.fxmisc.richtext.model.NavigationActions; +import org.fxmisc.richtext.model.Paragraph; +import org.fxmisc.richtext.model.PlainTextChange; +import org.fxmisc.richtext.model.RichTextChange; +import org.fxmisc.richtext.model.SegmentOps; +import org.fxmisc.richtext.model.SimpleEditableStyledDocument; +import org.fxmisc.richtext.model.StyleSpans; +import org.fxmisc.richtext.model.StyledDocument; +import org.fxmisc.richtext.model.StyledTextAreaModel; +import org.fxmisc.richtext.model.TextEditingArea; +import org.fxmisc.richtext.model.TwoDimensional; +import org.fxmisc.richtext.model.TwoLevelNavigator; +import org.fxmisc.richtext.model.UndoActions; +import org.fxmisc.undo.UndoManager; +import org.fxmisc.undo.UndoManagerFactory; +import org.reactfx.EventStream; +import org.reactfx.EventStreams; +import org.reactfx.StateMachine; +import org.reactfx.Subscription; +import org.reactfx.collection.LiveList; +import org.reactfx.util.Tuple2; +import org.reactfx.value.Val; +import org.reactfx.value.Var; + +/** + * Text editing control. Accepts user input (keyboard, mouse) and + * provides API to assign style to text ranges. It is suitable for + * syntax highlighting and rich-text editors. + * + *

Subclassing is allowed to define the type of style, e.g. inline + * style or style classes.

+ * + *

Note: Scroll bars no longer appear when the content spans outside + * of the viewport. To add scroll bars, the area needs to be wrapped in + * a {@link VirtualizedScrollPane}. For example,

+ *
+ * {@code
+ * // shows area without scroll bars
+ * InlineCssTextArea area = new InlineCssTextArea();
+ *
+ * // add scroll bars that will display as needed
+ * VirtualizedScrollPane vsPane = new VirtualizedScrollPane(area);
+ *
+ * Parent parent = //;
+ * parent.getChildren().add(vsPane)
+ * }
+ * 
+ * + *

Overriding keyboard shortcuts

+ * + * {@code StyledTextArea} uses {@code KEY_TYPED} handler to handle ordinary + * character input and {@code KEY_PRESSED} handler to handle control key + * combinations (including Enter and Tab). To add or override some keyboard + * shortcuts, while keeping the rest in place, you would combine the default + * event handler with a new one that adds or overrides some of the default + * key combinations. This is how to bind {@code Ctrl+S} to the {@code save()} + * operation: + *
+ * {@code
+ * import static javafx.scene.input.KeyCode.*;
+ * import static javafx.scene.input.KeyCombination.*;
+ * import static org.fxmisc.wellbehaved.event.EventPattern.*;
+ * import static org.fxmisc.wellbehaved.event.InputMap.*;
+ *
+ * import org.fxmisc.wellbehaved.event.Nodes;
+ *
+ * Nodes.addInputMap(area, consume(keyPressed(S, CONTROL_DOWN), event -> save()));
+ * }
+ * 
+ * + * @param type of style that can be applied to text. + */ +public class GenericRichtextArea extends Region + implements + TextEditingArea, + EditActions, + ClipboardActions, + NavigationActions, + UndoActions, + TwoDimensional, + Virtualized { + + /** + * Index range [0, 0). + */ + public static final IndexRange EMPTY_RANGE = new IndexRange(0, 0); + + private static final PseudoClass HAS_CARET = PseudoClass.getPseudoClass("has-caret"); + private static final PseudoClass FIRST_PAR = PseudoClass.getPseudoClass("first-paragraph"); + private static final PseudoClass LAST_PAR = PseudoClass.getPseudoClass("last-paragraph"); + + + /* ********************************************************************** * + * * + * Properties * + * * + * Properties affect behavior and/or appearance of this control. * + * * + * They are readable and writable by the client code and never change by * + * other means, i.e. they contain either the default value or the value * + * set by the client code. * + * * + * ********************************************************************** */ + + /** + * Background fill for highlighted text. + */ + private final StyleableObjectProperty highlightFill + = new CssProperties.HighlightFillProperty(this, Color.DODGERBLUE); + + /** + * Text color for highlighted text. + */ + private final StyleableObjectProperty highlightTextFill + = new CssProperties.HighlightTextFillProperty(this, Color.WHITE); + + /** + * Controls the blink rate of the caret, when one is displayed. Setting + * the duration to zero disables blinking. + */ + private final StyleableObjectProperty caretBlinkRate + = new CssProperties.CaretBlinkRateProperty(this, javafx.util.Duration.millis(500)); + + // editable property + /** + * Indicates whether this text area can be edited by the user. + * Note that this property doesn't affect editing through the API. + */ + private final BooleanProperty editable = new EditableProperty<>(this); + public final boolean isEditable() { return editable.get(); } + public final void setEditable(boolean value) { editable.set(value); } + public final BooleanProperty editableProperty() { return editable; } + + // wrapText property + /** + * When a run of text exceeds the width of the text region, + * then this property indicates whether the text should wrap + * onto another line. + */ + private final BooleanProperty wrapText = new SimpleBooleanProperty(this, "wrapText"); + public final boolean isWrapText() { return wrapText.get(); } + public final void setWrapText(boolean value) { wrapText.set(value); } + public final BooleanProperty wrapTextProperty() { return wrapText; } + + // showCaret property + /** + * Indicates when this text area should display a caret. + */ + private final Var showCaret = Var.newSimpleVar(CaretVisibility.AUTO); + public final CaretVisibility getShowCaret() { return showCaret.getValue(); } + public final void setShowCaret(CaretVisibility value) { showCaret.setValue(value); } + public final Var showCaretProperty() { return showCaret; } + + public static enum CaretVisibility { + /** Caret is displayed. */ + ON, + /** Caret is displayed when area is focused, enabled, and editable. */ + AUTO, + /** Caret is not displayed. */ + OFF + } + + // undo manager + @Override public UndoManager getUndoManager() { return model.getUndoManager(); } + @Override public void setUndoManager(UndoManagerFactory undoManagerFactory) { + model.setUndoManager(undoManagerFactory); + } + + /** + * Popup window that will be positioned by this text area relative to the + * caret or selection. Use {@link #popupAlignmentProperty()} to specify + * how the popup should be positioned relative to the caret or selection. + * Use {@link #popupAnchorOffsetProperty()} or + * {@link #popupAnchorAdjustmentProperty()} to further adjust the position. + */ + private final ObjectProperty popupWindow = new SimpleObjectProperty<>(); + public void setPopupWindow(PopupWindow popup) { popupWindow.set(popup); } + public PopupWindow getPopupWindow() { return popupWindow.get(); } + public ObjectProperty popupWindowProperty() { return popupWindow; } + + /** @deprecated Use {@link #setPopupWindow(PopupWindow)}. */ + @Deprecated + public void setPopupAtCaret(PopupWindow popup) { popupWindow.set(popup); } + /** @deprecated Use {@link #getPopupWindow()}. */ + @Deprecated + public PopupWindow getPopupAtCaret() { return popupWindow.get(); } + /** @deprecated Use {@link #popupWindowProperty()}. */ + @Deprecated + public ObjectProperty popupAtCaretProperty() { return popupWindow; } + + /** + * Specifies further offset (in pixels) of the popup window from the + * position specified by {@link #popupAlignmentProperty()}. + * + *

If {@link #popupAnchorAdjustmentProperty()} is also specified, then + * it overrides the offset set by this property. + */ + private final ObjectProperty popupAnchorOffset = new SimpleObjectProperty<>(); + public void setPopupAnchorOffset(Point2D offset) { popupAnchorOffset.set(offset); } + public Point2D getPopupAnchorOffset() { return popupAnchorOffset.get(); } + public ObjectProperty popupAnchorOffsetProperty() { return popupAnchorOffset; } + + /** + * Specifies how to adjust the popup window's anchor point. The given + * operator is invoked with the screen position calculated according to + * {@link #popupAlignmentProperty()} and should return a new screen + * position. This position will be used as the popup window's anchor point. + * + *

Setting this property overrides {@link #popupAnchorOffsetProperty()}. + */ + private final ObjectProperty> popupAnchorAdjustment = new SimpleObjectProperty<>(); + public void setPopupAnchorAdjustment(UnaryOperator f) { popupAnchorAdjustment.set(f); } + public UnaryOperator getPopupAnchorAdjustment() { return popupAnchorAdjustment.get(); } + public ObjectProperty> popupAnchorAdjustmentProperty() { return popupAnchorAdjustment; } + + /** + * Defines where the popup window given in {@link #popupWindowProperty()} + * is anchored, i.e. where its anchor point is positioned. This position + * can further be adjusted by {@link #popupAnchorOffsetProperty()} or + * {@link #popupAnchorAdjustmentProperty()}. + */ + private final ObjectProperty popupAlignment = new SimpleObjectProperty<>(CARET_TOP); + public void setPopupAlignment(PopupAlignment pos) { popupAlignment.set(pos); } + public PopupAlignment getPopupAlignment() { return popupAlignment.get(); } + public ObjectProperty popupAlignmentProperty() { return popupAlignment; } + + /** + * Defines how long the mouse has to stay still over the text before a + * {@link MouseOverTextEvent} of type {@code MOUSE_OVER_TEXT_BEGIN} is + * fired on this text area. When set to {@code null}, no + * {@code MouseOverTextEvent}s are fired on this text area. + * + *

Default value is {@code null}. + */ + private final ObjectProperty mouseOverTextDelay = new SimpleObjectProperty<>(null); + public void setMouseOverTextDelay(Duration delay) { mouseOverTextDelay.set(delay); } + public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); } + public ObjectProperty mouseOverTextDelayProperty() { return mouseOverTextDelay; } + + /** + * Defines how to handle an event in which the user has selected some text, dragged it to a + * new location within the area, and released the mouse at some character {@code index} + * within the area. + * + *

By default, this will relocate the selected text to the character index where the mouse + * was released. To override it, use {@link #setOnSelectionDrop(IntConsumer)}. + */ + private Property onSelectionDrop = new SimpleObjectProperty<>(this::moveSelectedText); + public final void setOnSelectionDrop(IntConsumer consumer) { onSelectionDrop.setValue(consumer); } + public final IntConsumer getOnSelectionDrop() { return onSelectionDrop.getValue(); } + + private final ObjectProperty> paragraphGraphicFactory = new SimpleObjectProperty<>(null); + public void setParagraphGraphicFactory(IntFunction factory) { paragraphGraphicFactory.set(factory); } + public IntFunction getParagraphGraphicFactory() { return paragraphGraphicFactory.get(); } + public ObjectProperty> paragraphGraphicFactoryProperty() { return paragraphGraphicFactory; } + + /** + * Indicates whether the initial style should also be used for plain text + * inserted into this text area. When {@code false}, the style immediately + * preceding the insertion position is used. Default value is {@code false}. + */ + public BooleanProperty useInitialStyleForInsertionProperty() { return model.useInitialStyleForInsertionProperty(); } + public void setUseInitialStyleForInsertion(boolean value) { model.setUseInitialStyleForInsertion(value); } + public boolean getUseInitialStyleForInsertion() { return model.getUseInitialStyleForInsertion(); } + + private Optional, Codec>> styleCodecs = Optional.empty(); + /** + * Sets codecs to encode/decode style information to/from binary format. + * Providing codecs enables clipboard actions to retain the style information. + */ + public void setStyleCodecs(Codec paragraphStyleCodec, Codec textStyleCodec) { + styleCodecs = Optional.of(t(paragraphStyleCodec, textStyleCodec)); + } + @Override + public Optional, Codec>> getStyleCodecs() { + return styleCodecs; + } + + /** + * The estimated scrollX value. This can be set in order to scroll the content. + * Value is only accurate when area does not wrap lines and uses the same font size + * throughout the entire area. + */ + @Override + public Var estimatedScrollXProperty() { return virtualFlow.estimatedScrollXProperty(); } + public double getEstimatedScrollX() { return virtualFlow.estimatedScrollXProperty().getValue(); } + public void setEstimatedScrollX(double value) { virtualFlow.estimatedScrollXProperty().setValue(value); } + + /** + * The estimated scrollY value. This can be set in order to scroll the content. + * Value is only accurate when area does not wrap lines and uses the same font size + * throughout the entire area. + */ + @Override + public Var estimatedScrollYProperty() { return virtualFlow.estimatedScrollYProperty(); } + public double getEstimatedScrollY() { return virtualFlow.estimatedScrollYProperty().getValue(); } + public void setEstimatedScrollY(double value) { virtualFlow.estimatedScrollYProperty().setValue(value); } + + + /* ********************************************************************** * + * * + * Observables * + * * + * Observables are "dynamic" (i.e. changing) characteristics of this * + * control. They are not directly settable by the client code, but change * + * in response to user input and/or API actions. * + * * + * ********************************************************************** */ + + // text + @Override public final String getText() { return model.getText(); } + @Override public final ObservableValue textProperty() { return model.textProperty(); } + + // rich text + @Override public final StyledDocument getDocument() { return model.getDocument(); } + + // length + @Override public final int getLength() { return model.getLength(); } + @Override public final ObservableValue lengthProperty() { return model.lengthProperty(); } + + // caret position + @Override public final int getCaretPosition() { return model.getCaretPosition(); } + @Override public final ObservableValue caretPositionProperty() { return model.caretPositionProperty(); } + + // selection anchor + @Override public final int getAnchor() { return model.getAnchor(); } + @Override public final ObservableValue anchorProperty() { return model.anchorProperty(); } + + // selection + @Override public final IndexRange getSelection() { return model.getSelection(); } + @Override public final ObservableValue selectionProperty() { return model.selectionProperty(); } + + // selected text + @Override public final String getSelectedText() { return model.getSelectedText(); } + @Override public final ObservableValue selectedTextProperty() { return model.selectedTextProperty(); } + + // current paragraph index + @Override public final int getCurrentParagraph() { return model.getCurrentParagraph(); } + @Override public final ObservableValue currentParagraphProperty() { return model.currentParagraphProperty(); } + + // caret column + @Override public final int getCaretColumn() { return model.getCaretColumn(); } + @Override public final ObservableValue caretColumnProperty() { return model.caretColumnProperty(); } + + // paragraphs + @Override public LiveList> getParagraphs() { return model.getParagraphs(); } + + // beingUpdated + public ObservableBooleanValue beingUpdatedProperty() { return model.beingUpdatedProperty(); } + public boolean isBeingUpdated() { return model.isBeingUpdated(); } + + // total width estimate + /** + * The estimated width of the entire document. Accurate when area does not wrap lines and + * uses the same font size throughout the entire area. Value is only supposed to be set by + * the skin, not the user. + */ + @Override + public Val totalWidthEstimateProperty() { return virtualFlow.totalWidthEstimateProperty(); } + public double getTotalWidthEstimate() { return virtualFlow.totalWidthEstimateProperty().getValue(); } + + // total height estimate + /** + * The estimated height of the entire document. Accurate when area does not wrap lines and + * uses the same font size throughout the entire area. Value is only supposed to be set by + * the skin, not the user. + */ + @Override + public Val totalHeightEstimateProperty() { return virtualFlow.totalHeightEstimateProperty(); } + public double getTotalHeightEstimate() { return virtualFlow.totalHeightEstimateProperty().getValue(); } + + /* ********************************************************************** * + * * + * Event streams * + * * + * ********************************************************************** */ + + // text changes + @Override public final EventStream plainTextChanges() { return model.plainTextChanges(); } + + // rich text changes + @Override public final EventStream> richChanges() { return model.richChanges(); } + + /* ********************************************************************** * + * * + * Private fields * + * * + * ********************************************************************** */ + + private final StyledTextAreaBehavior behavior; + + private Subscription subscriptions = () -> {}; + + private final Binding caretVisible; + + private final Val> _popupAnchorAdjustment; + + private final VirtualFlow, Cell, ParagraphBox>> virtualFlow; + + // used for two-level navigation, where on the higher level are + // paragraphs and on the lower level are lines within a paragraph + private final TwoLevelNavigator navigator; + + private boolean followCaretRequested = false; + + /** + * model + */ + private final StyledTextAreaModel model; + + /** + * @return this area's {@link StyledTextAreaModel} + */ + final StyledTextAreaModel getModel() { + return model; + } + + /* ********************************************************************** * + * * + * Fields necessary for Cloning * + * * + * ********************************************************************** */ + + /** + * The underlying document that can be displayed by multiple {@code StyledTextArea}s. + */ + public final EditableStyledDocument getContent() { return model.getContent(); } + + /** + * Style used by default when no other style is provided. + */ + public final S getInitialTextStyle() { return model.getInitialTextStyle(); } + + /** + * Style used by default when no other style is provided. + */ + public final PS getInitialParagraphStyle() { return model.getInitialParagraphStyle(); } + + /** + * Style applicator used by the default skin. + */ + private final BiConsumer applyStyle; + public final BiConsumer getApplyStyle() { return applyStyle; } + + /** + * Style applicator used by the default skin. + */ + private final BiConsumer applyParagraphStyle; + public final BiConsumer getApplyParagraphStyle() { return applyParagraphStyle; } + + /** + * Indicates whether style should be preserved on undo/redo, + * copy/paste and text move. + * TODO: Currently, only undo/redo respect this flag. + */ + public final boolean isPreserveStyle() { return model.isPreserveStyle(); } + + private final SegmentOps segmentOps; + public final SegmentOps getSegmentOps() { return segmentOps; } + + + /* ********************************************************************** * + * * + * Constructors * + * * + * ********************************************************************** */ + + /** + * Creates a text area with empty text content. + * + * @param initialTextStyle style to use in places where no other style is + * specified (yet). + * @param applyStyle function that, given a {@link Text} node and + * a style, applies the style to the text node. This function is + * used by the default skin to apply style to text nodes. + * @param initialParagraphStyle style to use in places where no other style is + * specified (yet). + * @param applyParagraphStyle function that, given a {@link TextFlow} node and + * a style, applies the style to the paragraph node. This function is + * used by the default skin to apply style to paragraph nodes. + */ + public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, + S initialTextStyle, SegmentOps segmentOps, BiConsumer applyStyle + ) { + this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, true); + } + + public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, + S initialTextStyle, SegmentOps segmentOps, BiConsumer applyStyle, + boolean preserveStyle + ) { + this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, + new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), preserveStyle); + } + + /** + * The same as {@link #StyledTextArea(Object, BiConsumer, Object, BiConsumer)} except that + * this constructor can be used to create another {@code StyledTextArea} object that + * shares the same {@link EditableStyledDocument}. + */ + public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, + S initialTextStyle, + SegmentOps segmentOps, BiConsumer applyStyle, + EditableStyledDocument document) { + this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, document, true); + } + + public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, + S initialTextStyle, + SegmentOps segmentOps, + BiConsumer applyStyle, + EditableStyledDocument document, boolean preserveStyle) { + this.model = new StyledTextAreaModel<>(initialParagraphStyle, initialTextStyle, segmentOps, document, preserveStyle); + this.applyStyle = applyStyle; + this.applyParagraphStyle = applyParagraphStyle; + this.segmentOps = segmentOps; + + // allow tab traversal into area + setFocusTraversable(true); + + this.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); + getStyleClass().add("styled-text-area"); + getStylesheets().add(GenericRichtextArea.class.getResource("styled-text-area.css").toExternalForm()); + + // keeps track of currently used non-empty cells + @SuppressWarnings("unchecked") + ObservableSet> nonEmptyCells = FXCollections.observableSet(); + + // Initialize content + virtualFlow = VirtualFlow.createVertical( + getParagraphs(), + par -> { + Cell, ParagraphBox> cell = createCell( + par, + applyStyle, + applyParagraphStyle); + nonEmptyCells.add(cell.getNode()); + return cell.beforeReset(() -> nonEmptyCells.remove(cell.getNode())) + .afterUpdateItem(p -> nonEmptyCells.add(cell.getNode())); + }); + getChildren().add(virtualFlow); + + // initialize navigator + IntSupplier cellCount = () -> getParagraphs().size(); + IntUnaryOperator cellLength = i -> virtualFlow.getCell(i).getNode().getLineCount(); + navigator = new TwoLevelNavigator(cellCount, cellLength); + + // follow the caret every time the caret position or paragraphs change + EventStream caretPosDirty = invalidationsOf(caretPositionProperty()); + EventStream paragraphsDirty = invalidationsOf(getParagraphs()); + EventStream selectionDirty = invalidationsOf(selectionProperty()); + // need to reposition popup even when caret hasn't moved, but selection has changed (been deselected) + EventStream caretDirty = merge(caretPosDirty, paragraphsDirty, selectionDirty); + subscribeTo(caretDirty, x -> requestFollowCaret()); + + // whether or not to display the caret + EventStream blinkCaret = EventStreams.valuesOf(showCaretProperty()) + .flatMap(mode -> { + switch (mode) { + case ON: + return EventStreams.valuesOf(Val.constant(true)); + case OFF: + return EventStreams.valuesOf(Val.constant(false)); + default: + case AUTO: + return EventStreams.valuesOf(focusedProperty() + .and(editableProperty()) + .and(disabledProperty().not())); + } + }); + + // the rate at which to display the caret + EventStream blinkRate = EventStreams.valuesOf(caretBlinkRate); + + // The caret is visible in periodic intervals, + // but only when blinkCaret is true. + caretVisible = EventStreams.combine(blinkCaret, blinkRate) + .flatMap(tuple -> { + Boolean blink = tuple.get1(); + javafx.util.Duration rate = tuple.get2(); + if(blink) { + return rate.lessThanOrEqualTo(ZERO) + ? EventStreams.valuesOf(Val.constant(true)) + : booleanPulse(rate, caretDirty); + } else { + return EventStreams.valuesOf(Val.constant(false)); + } + }) + .toBinding(false); + manageBinding(caretVisible); + + // Adjust popup anchor by either a user-provided function, + // or user-provided offset, or don't adjust at all. + Val> userOffset = Val.map( + popupAnchorOffsetProperty(), + offset -> anchor -> anchor.add(offset)); + _popupAnchorAdjustment = + Val.orElse( + popupAnchorAdjustmentProperty(), + userOffset) + .orElseConst(UnaryOperator.identity()); + + // dispatch MouseOverTextEvents when mouseOverTextDelay is not null + EventStreams.valuesOf(mouseOverTextDelayProperty()) + .flatMap(delay -> delay != null + ? mouseOverTextEvents(nonEmptyCells, delay) + : EventStreams.never()) + .subscribe(evt -> Event.fireEvent(this, evt)); + + behavior = new StyledTextAreaBehavior(this); + } + + + /* ********************************************************************** * + * * + * Queries * + * * + * Queries are parameterized observables. * + * * + * ********************************************************************** */ + + /** + * Returns caret bounds relative to the viewport, i.e. the visual bounds + * of the embedded VirtualFlow. + */ + Optional getCaretBounds() { + return virtualFlow.getCellIfVisible(getCurrentParagraph()) + .map(c -> { + Bounds cellBounds = c.getNode().getCaretBounds(); + return virtualFlow.cellToViewport(c, cellBounds); + }); + } + + /** + * Returns x coordinate of the caret in the current paragraph. + */ + ParagraphBox.CaretOffsetX getCaretOffsetX() { + int idx = getCurrentParagraph(); + return getCell(idx).getCaretOffsetX(); + } + + double getViewportHeight() { + return virtualFlow.getHeight(); + } + + CharacterHit hit(ParagraphBox.CaretOffsetX x, TwoDimensional.Position targetLine) { + int parIdx = targetLine.getMajor(); + ParagraphBox cell = virtualFlow.getCell(parIdx).getNode(); + CharacterHit parHit = cell.hitTextLine(x, targetLine.getMinor()); + return parHit.offset(getParagraphOffset(parIdx)); + } + + CharacterHit hit(ParagraphBox.CaretOffsetX x, double y) { + VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(0.0, y); + if(hit.isBeforeCells()) { + return CharacterHit.insertionAt(0); + } else if(hit.isAfterCells()) { + return CharacterHit.insertionAt(getLength()); + } else { + int parIdx = hit.getCellIndex(); + int parOffset = getParagraphOffset(parIdx); + ParagraphBox cell = hit.getCell().getNode(); + Point2D cellOffset = hit.getCellOffset(); + CharacterHit parHit = cell.hitText(x, cellOffset.getY()); + return parHit.offset(parOffset); + } + } + + /** + * Helpful for determining which letter is at point x, y: + *

+     *     {@code
+     *     StyledTextArea area = // creation code
+     *     area.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent e) -> {
+     *         CharacterHit hit = area.hit(e.getX(), e.getY());
+     *         int characterPosition = hit.getInsertionIndex();
+     *
+     *         // move the caret to that character's position
+     *         area.moveTo(characterPosition, SelectionPolicy.CLEAR);
+     *     }}
+     * 
+ */ + public CharacterHit hit(double x, double y) { + VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(x, y); + if(hit.isBeforeCells()) { + return CharacterHit.insertionAt(0); + } else if(hit.isAfterCells()) { + return CharacterHit.insertionAt(getLength()); + } else { + int parIdx = hit.getCellIndex(); + int parOffset = getParagraphOffset(parIdx); + ParagraphBox cell = hit.getCell().getNode(); + Point2D cellOffset = hit.getCellOffset(); + CharacterHit parHit = cell.hit(cellOffset); + return parHit.offset(parOffset); + } + } + + /** + * Returns the current line as a two-level index. + * The major number is the paragraph index, the minor + * number is the line number within the paragraph. + * + *

This method has a side-effect of bringing the current + * paragraph to the viewport if it is not already visible. + */ + TwoDimensional.Position currentLine() { + int parIdx = getCurrentParagraph(); + Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); + int lineIdx = cell.getNode().getCurrentLineIndex(); + return _position(parIdx, lineIdx); + } + + TwoDimensional.Position _position(int par, int line) { + return navigator.position(par, line); + } + + @Override + public final String getText(int start, int end) { + return model.getText(start, end); + } + + @Override + public String getText(int paragraph) { + return model.getText(paragraph); + } + + public Paragraph getParagraph(int index) { + return model.getParagraph(index); + } + + @Override + public StyledDocument subDocument(int start, int end) { + return model.subDocument(start, end); + } + + @Override + public StyledDocument subDocument(int paragraphIndex) { + return model.subDocument(paragraphIndex); + } + + /** + * Returns the selection range in the given paragraph. + */ + public IndexRange getParagraphSelection(int paragraph) { + return model.getParagraphSelection(paragraph); + } + + /** + * Returns the style of the character with the given index. + * If {@code index} points to a line terminator character, + * the last style used in the paragraph terminated by that + * line terminator is returned. + */ + public S getStyleOfChar(int index) { + return model.getStyleOfChar(index); + } + + /** + * Returns the style at the given position. That is the style of the + * character immediately preceding {@code position}, except when + * {@code position} points to a paragraph boundary, in which case it + * is the style at the beginning of the latter paragraph. + * + *

In other words, most of the time {@code getStyleAtPosition(p)} + * is equivalent to {@code getStyleOfChar(p-1)}, except when {@code p} + * points to a paragraph boundary, in which case it is equivalent to + * {@code getStyleOfChar(p)}. + */ + public S getStyleAtPosition(int position) { + return model.getStyleAtPosition(position); + } + + /** + * 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. If {@code position} + * points to a boundary between two paragraphs, then the first styled range + * of the latter paragraph is returned. + */ + public IndexRange getStyleRangeAtPosition(int position) { + return model.getStyleRangeAtPosition(position); + } + + /** + * Returns the styles in the given character range. + */ + public StyleSpans getStyleSpans(int from, int to) { + return model.getStyleSpans(from, to); + } + + /** + * Returns the styles in the given character range. + */ + public StyleSpans getStyleSpans(IndexRange range) { + return getStyleSpans(range.getStart(), range.getEnd()); + } + + /** + * Returns the style of the character with the given index in the given + * paragraph. If {@code index} is beyond the end of the paragraph, the + * style at the end of line is returned. If {@code index} is negative, it + * is the same as if it was 0. + */ + public S getStyleOfChar(int paragraph, int index) { + return model.getStyleOfChar(paragraph, index); + } + + /** + * Returns the style at the given position in the given paragraph. + * This is equivalent to {@code getStyleOfChar(paragraph, position-1)}. + */ + public S getStyleAtPosition(int paragraph, int position) { + return model.getStyleAtPosition(paragraph, position); + } + + /** + * Returns the range of homogeneous style that includes the given position + * in the given paragraph. If {@code position} points to a boundary between + * two styled ranges, then the range preceding {@code position} is returned. + */ + public IndexRange getStyleRangeAtPosition(int paragraph, int position) { + return model.getStyleRangeAtPosition(paragraph, position); + } + + /** + * Returns styles of the whole paragraph. + */ + public StyleSpans getStyleSpans(int paragraph) { + return model.getStyleSpans(paragraph); + } + + /** + * Returns the styles in the given character range of the given paragraph. + */ + public StyleSpans getStyleSpans(int paragraph, int from, int to) { + return model.getStyleSpans(paragraph, from, to); + } + + /** + * Returns the styles in the given character range of the given paragraph. + */ + public StyleSpans getStyleSpans(int paragraph, IndexRange range) { + return getStyleSpans(paragraph, range.getStart(), range.getEnd()); + } + + @Override + public int getAbsolutePosition(int paragraphIndex, int columnIndex) { + return model.getAbsolutePosition(paragraphIndex, columnIndex); + } + + @Override + public Position position(int row, int col) { + return model.position(row, col); + } + + @Override + public Position offsetToPosition(int charOffset, Bias bias) { + return model.offsetToPosition(charOffset, bias); + } + + + /* ********************************************************************** * + * * + * Actions * + * * + * Actions change the state of this control. They typically cause a * + * change of one or more observables and/or produce an event. * + * * + * ********************************************************************** */ + + void scrollBy(Point2D deltas) { + virtualFlow.scrollXBy(deltas.getX()); + virtualFlow.scrollYBy(deltas.getY()); + } + + void show(double y) { + virtualFlow.show(y); + } + + void showCaretAtBottom() { + int parIdx = getCurrentParagraph(); + Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); + Bounds caretBounds = cell.getNode().getCaretBounds(); + double y = caretBounds.getMaxY(); + virtualFlow.showAtOffset(parIdx, getViewportHeight() - y); + } + + void showCaretAtTop() { + int parIdx = getCurrentParagraph(); + Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); + Bounds caretBounds = cell.getNode().getCaretBounds(); + double y = caretBounds.getMinY(); + virtualFlow.showAtOffset(parIdx, -y); + } + + void requestFollowCaret() { + followCaretRequested = true; + requestLayout(); + } + + private void followCaret() { + int parIdx = getCurrentParagraph(); + Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); + Bounds caretBounds = cell.getNode().getCaretBounds(); + double graphicWidth = cell.getNode().getGraphicPrefWidth(); + Bounds region = extendLeft(caretBounds, graphicWidth); + virtualFlow.show(parIdx, region); + } + + /** + * Sets style for the given character range. + */ + public void setStyle(int from, int to, S style) { + model.setStyle(from, to, style); + } + + /** + * Sets style for the whole paragraph. + */ + public void setStyle(int paragraph, S style) { + model.setStyle(paragraph, style); + } + + /** + * Sets style for the given range relative in the given paragraph. + */ + public void setStyle(int paragraph, int from, int to, S style) { + model.setStyle(paragraph, from, to, style); + } + + /** + * Set multiple style ranges at once. This is equivalent to + *

+     * for(StyleSpan{@code } span: styleSpans) {
+     *     setStyle(from, from + span.getLength(), span.getStyle());
+     *     from += span.getLength();
+     * }
+     * 
+ * but the actual implementation is more efficient. + */ + public void setStyleSpans(int from, StyleSpans styleSpans) { + model.setStyleSpans(from, styleSpans); + } + + /** + * Set multiple style ranges of a paragraph at once. This is equivalent to + *
+     * for(StyleSpan{@code } span: styleSpans) {
+     *     setStyle(paragraph, from, from + span.getLength(), span.getStyle());
+     *     from += span.getLength();
+     * }
+     * 
+ * but the actual implementation is more efficient. + */ + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + model.setStyleSpans(paragraph, from, styleSpans); + } + + /** + * Sets style for the whole paragraph. + */ + public void setParagraphStyle(int paragraph, PS paragraphStyle) { + model.setParagraphStyle(paragraph, paragraphStyle); + } + + /** + * Resets the style of the given range to the initial style. + */ + public void clearStyle(int from, int to) { + model.clearStyle(from, to); + } + + /** + * Resets the style of the given paragraph to the initial style. + */ + public void clearStyle(int paragraph) { + model.clearStyle(paragraph); + } + + /** + * Resets the style of the given range in the given paragraph + * to the initial style. + */ + public void clearStyle(int paragraph, int from, int to) { + model.clearStyle(paragraph, from, to); + } + + /** + * Resets the style of the given paragraph to the initial style. + */ + public void clearParagraphStyle(int paragraph) { + model.clearParagraphStyle(paragraph); + } + + @Override + public void replaceText(int start, int end, String text) { + model.replaceText(start, end, text); + } + + @Override + public void replace(int start, int end, StyledDocument replacement) { + model.replace(start, end, replacement); + } + + @Override + public void selectRange(int anchor, int caretPosition) { + model.selectRange(anchor, caretPosition); + } + + /** + * {@inheritDoc} + * @deprecated You probably meant to use {@link #moveTo(int)}. This method will be made + * package-private in the future + */ + @Deprecated + @Override + public void positionCaret(int pos) { + model.positionCaret(pos); + } + + /* ********************************************************************** * + * * + * Public API * + * * + * ********************************************************************** */ + + public void dispose() { + subscriptions.unsubscribe(); + model.dispose(); + virtualFlow.dispose(); + } + + /* ********************************************************************** * + * * + * Layout * + * * + * ********************************************************************** */ + + @Override + protected void layoutChildren() { + virtualFlow.resize(getWidth(), getHeight()); + if(followCaretRequested) { + followCaretRequested = false; + followCaret(); + } + + // position popup + PopupWindow popup = getPopupWindow(); + PopupAlignment alignment = getPopupAlignment(); + UnaryOperator adjustment = _popupAnchorAdjustment.getValue(); + if(popup != null) { + positionPopup(popup, alignment, adjustment); + } + } + + /* ********************************************************************** * + * * + * Private methods * + * * + * ********************************************************************** */ + + private Cell, ParagraphBox> createCell( + Paragraph paragraph, + BiConsumer applyStyle, + BiConsumer applyParagraphStyle) { + + ParagraphBox box = new ParagraphBox<>(paragraph, applyParagraphStyle, applyStyle, segmentOps); + + box.highlightFillProperty().bind(highlightFill); + box.highlightTextFillProperty().bind(highlightTextFill); + box.wrapTextProperty().bind(wrapTextProperty()); + box.graphicFactoryProperty().bind(paragraphGraphicFactoryProperty()); + box.graphicOffset.bind(virtualFlow.breadthOffsetProperty()); + + Val hasCaret = Val.combine( + box.indexProperty(), + currentParagraphProperty(), + (bi, cp) -> bi.intValue() == cp.intValue()); + + Subscription hasCaretPseudoClass = hasCaret.values().subscribe(value -> box.pseudoClassStateChanged(HAS_CARET, value)); + Subscription firstParPseudoClass = box.indexProperty().values().subscribe(idx -> box.pseudoClassStateChanged(FIRST_PAR, idx == 0)); + Subscription lastParPseudoClass = EventStreams.combine( + box.indexProperty().values(), + getParagraphs().sizeProperty().values() + ).subscribe(in -> in.exec((i, n) -> box.pseudoClassStateChanged(LAST_PAR, i == n-1))); + + // caret is visible only in the paragraph with the caret + Val cellCaretVisible = hasCaret.flatMap(x -> x ? caretVisible : Val.constant(false)); + box.caretVisibleProperty().bind(cellCaretVisible); + + // bind cell's caret position to area's caret column, + // when the cell is the one with the caret + box.caretPositionProperty().bind(hasCaret.flatMap(has -> has + ? caretColumnProperty() + : Val.constant(0))); + + // keep paragraph selection updated + ObjectBinding cellSelection = Bindings.createObjectBinding(() -> { + int idx = box.getIndex(); + return idx != -1 + ? getParagraphSelection(idx) + : GenericRichtextArea.EMPTY_RANGE; + }, selectionProperty(), box.indexProperty()); + box.selectionProperty().bind(cellSelection); + + return new Cell, ParagraphBox>() { + @Override + public ParagraphBox getNode() { + return box; + } + + @Override + public void updateIndex(int index) { + box.setIndex(index); + } + + @Override + public void dispose() { + box.highlightFillProperty().unbind(); + box.highlightTextFillProperty().unbind(); + box.wrapTextProperty().unbind(); + box.graphicFactoryProperty().unbind(); + box.graphicOffset.unbind(); + + hasCaretPseudoClass.unsubscribe(); + firstParPseudoClass.unsubscribe(); + lastParPseudoClass.unsubscribe(); + + box.caretVisibleProperty().unbind(); + box.caretPositionProperty().unbind(); + + box.selectionProperty().unbind(); + cellSelection.dispose(); + } + }; + } + + private ParagraphBox getCell(int index) { + return virtualFlow.getCell(index).getNode(); + } + + private EventStream mouseOverTextEvents(ObservableSet> cells, Duration delay) { + return merge(cells, c -> c.stationaryIndices(delay).map(e -> e.unify( + l -> l.map((pos, charIdx) -> MouseOverTextEvent.beginAt(c.localToScreen(pos), getParagraphOffset(c.getIndex()) + charIdx)), + r -> MouseOverTextEvent.end()))); + } + + private int getParagraphOffset(int parIdx) { + return position(parIdx, 0).toOffset(); + } + + private void positionPopup( + PopupWindow popup, + PopupAlignment alignment, + UnaryOperator adjustment) { + Optional bounds = null; + switch(alignment.getAnchorObject()) { + case CARET: bounds = getCaretBoundsOnScreen(); break; + case SELECTION: bounds = getSelectionBoundsOnScreen(); break; + } + bounds.ifPresent(b -> { + double x = 0, y = 0; + switch(alignment.getHorizontalAlignment()) { + case LEFT: x = b.getMinX(); break; + case H_CENTER: x = (b.getMinX() + b.getMaxX()) / 2; break; + case RIGHT: x = b.getMaxX(); break; + } + switch(alignment.getVerticalAlignment()) { + case TOP: y = b.getMinY(); + case V_CENTER: y = (b.getMinY() + b.getMaxY()) / 2; break; + case BOTTOM: y = b.getMaxY(); break; + } + Point2D anchor = adjustment.apply(new Point2D(x, y)); + popup.setAnchorX(anchor.getX()); + popup.setAnchorY(anchor.getY()); + }); + } + + private Optional getCaretBoundsOnScreen() { + return virtualFlow.getCellIfVisible(getCurrentParagraph()) + .map(c -> c.getNode().getCaretBoundsOnScreen()); + } + + private Optional getSelectionBoundsOnScreen() { + IndexRange selection = getSelection(); + if(selection.getLength() == 0) { + return getCaretBoundsOnScreen(); + } + + Bounds[] bounds = virtualFlow.visibleCells().stream() + .map(c -> c.getNode().getSelectionBoundsOnScreen()) + .filter(Optional::isPresent) + .map(Optional::get) + .toArray(Bounds[]::new); + + if(bounds.length == 0) { + return Optional.empty(); + } + double minX = Stream.of(bounds).mapToDouble(Bounds::getMinX).min().getAsDouble(); + double maxX = Stream.of(bounds).mapToDouble(Bounds::getMaxX).max().getAsDouble(); + double minY = Stream.of(bounds).mapToDouble(Bounds::getMinY).min().getAsDouble(); + double maxY = Stream.of(bounds).mapToDouble(Bounds::getMaxY).max().getAsDouble(); + return Optional.of(new BoundingBox(minX, minY, maxX-minX, maxY-minY)); + } + + private void subscribeTo(EventStream src, Consumer cOnsumer) { + manageSubscription(src.subscribe(cOnsumer)); + } + + private void manageSubscription(Subscription subscription) { + subscriptions = subscriptions.and(subscription); + } + + private void manageBinding(Binding binding) { + subscriptions = subscriptions.and(binding::dispose); + } + + private static Bounds extendLeft(Bounds b, double w) { + if(w == 0) { + return b; + } else { + return new BoundingBox( + b.getMinX() - w, b.getMinY(), + b.getWidth() + w, b.getHeight()); + } + } + + private static EventStream booleanPulse(javafx.util.Duration javafxDuration, EventStream restartImpulse) { + Duration duration = Duration.ofMillis(Math.round(javafxDuration.toMillis())); + EventStream ticks = EventStreams.restartableTicks(duration, restartImpulse); + return StateMachine.init(false) + .on(restartImpulse.withDefaultEvent(null)).transition((state, impulse) -> true) + .on(ticks).transition((state, tick) -> !state) + .toStateStream(); + } +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java new file mode 100644 index 000000000..c7c2171c6 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -0,0 +1,1362 @@ +package org.fxmisc.richtext; + +import static javafx.util.Duration.*; +import static org.fxmisc.richtext.PopupAlignment.*; +import static org.reactfx.EventStreams.*; +import static org.reactfx.util.Tuples.*; + +import java.time.Duration; +import java.util.Optional; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntConsumer; +import java.util.function.IntFunction; +import java.util.function.IntSupplier; +import java.util.function.IntUnaryOperator; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; + +import javafx.beans.binding.Binding; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.ObjectBinding; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.Property; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.beans.value.ObservableBooleanValue; +import javafx.beans.value.ObservableValue; +import javafx.collections.FXCollections; +import javafx.collections.ObservableSet; +import javafx.css.PseudoClass; +import javafx.css.StyleableObjectProperty; +import javafx.event.Event; +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.geometry.Insets; +import javafx.geometry.Point2D; +import javafx.scene.Node; +import javafx.scene.control.IndexRange; +import javafx.scene.layout.Background; +import javafx.scene.layout.BackgroundFill; +import javafx.scene.layout.CornerRadii; +import javafx.scene.layout.Region; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.text.TextFlow; +import javafx.stage.PopupWindow; + +import org.fxmisc.flowless.Cell; +import org.fxmisc.flowless.VirtualFlow; +import org.fxmisc.flowless.VirtualFlowHit; +import org.fxmisc.flowless.Virtualized; +import org.fxmisc.flowless.VirtualizedScrollPane; +import org.fxmisc.richtext.CssProperties.EditableProperty; +import org.fxmisc.richtext.model.Codec; +import org.fxmisc.richtext.model.EditActions; +import org.fxmisc.richtext.model.EditableStyledDocument; +import org.fxmisc.richtext.model.NavigationActions; +import org.fxmisc.richtext.model.Paragraph; +import org.fxmisc.richtext.model.PlainTextChange; +import org.fxmisc.richtext.model.RichTextChange; +import org.fxmisc.richtext.model.SegmentOps; +import org.fxmisc.richtext.model.SimpleEditableStyledDocument; +import org.fxmisc.richtext.model.StyleSpans; +import org.fxmisc.richtext.model.StyledDocument; +import org.fxmisc.richtext.model.StyledTextAreaModel; +import org.fxmisc.richtext.model.TextEditingArea; +import org.fxmisc.richtext.model.TextOps; +import org.fxmisc.richtext.model.TwoDimensional; +import org.fxmisc.richtext.model.TwoLevelNavigator; +import org.fxmisc.richtext.model.UndoActions; +import org.fxmisc.undo.UndoManager; +import org.fxmisc.undo.UndoManagerFactory; +import org.reactfx.EventStream; +import org.reactfx.EventStreams; +import org.reactfx.StateMachine; +import org.reactfx.Subscription; +import org.reactfx.collection.LiveList; +import org.reactfx.util.Tuple2; +import org.reactfx.value.Val; +import org.reactfx.value.Var; + +/** + * Text editing control. Accepts user input (keyboard, mouse) and + * provides API to assign style to text ranges. It is suitable for + * syntax highlighting and rich-text editors. + * + *

Subclassing is allowed to define the type of style, e.g. inline + * style or style classes.

+ * + *

Note: Scroll bars no longer appear when the content spans outside + * of the viewport. To add scroll bars, the area needs to be wrapped in + * a {@link VirtualizedScrollPane}. For example,

+ *
+ * {@code
+ * // shows area without scroll bars
+ * InlineCssTextArea area = new InlineCssTextArea();
+ *
+ * // add scroll bars that will display as needed
+ * VirtualizedScrollPane vsPane = new VirtualizedScrollPane(area);
+ *
+ * Parent parent = //;
+ * parent.getChildren().add(vsPane)
+ * }
+ * 
+ * + *

Auto-Scrolling to the Caret

+ * + *

Every time the underlying {@link EditableStyledDocument} changes via user interaction (e.g. typing) through + * the {@code StyledTextArea}, the area will scroll to insure the caret is kept in view. However, this does not + * occur if changes are done programmatically. For example, let's say the area is displaying the bottom part + * of the area's {@link EditableStyledDocument} and some code changes something in the top part of the document + * that is not currently visible. If there is no call to {@link #requestFollowCaret()} at the end of that code, + * the area will not auto-scroll to that section of the document. The change will occur, and the user will continue + * to see the bottom part of the document as before. If such a call is there, then the area will scroll + * to the top of the document and no longer display the bottom part of it.

+ * + *

Additionally, when overriding the default user-interaction behavior, remember to include a call + * to {@link #requestFollowCaret()}.

+ * + *

Overriding keyboard shortcuts

+ * + * {@code StyledTextArea} uses {@code KEY_TYPED} handler to handle ordinary + * character input and {@code KEY_PRESSED} handler to handle control key + * combinations (including Enter and Tab). To add or override some keyboard + * shortcuts, while keeping the rest in place, you would combine the default + * event handler with a new one that adds or overrides some of the default + * key combinations. This is how to bind {@code Ctrl+S} to the {@code save()} + * operation: + *
+ * {@code
+ * import static javafx.scene.input.KeyCode.*;
+ * import static javafx.scene.input.KeyCombination.*;
+ * import static org.fxmisc.wellbehaved.event.EventPattern.*;
+ * import static org.fxmisc.wellbehaved.event.InputMap.*;
+ *
+ * import org.fxmisc.wellbehaved.event.Nodes;
+ *
+ * Nodes.addInputMap(area, consume(keyPressed(S, CONTROL_DOWN), event -> save()));
+ * }
+ * 
+ * + * @param type of style that can be applied to text. + */ +public class GenericStyledArea extends Region + implements + TextEditingArea, + EditActions, + ClipboardActions, + NavigationActions, + UndoActions, + TwoDimensional, + Virtualized { + + /** + * Index range [0, 0). + */ + public static final IndexRange EMPTY_RANGE = new IndexRange(0, 0); + + private static final PseudoClass HAS_CARET = PseudoClass.getPseudoClass("has-caret"); + private static final PseudoClass FIRST_PAR = PseudoClass.getPseudoClass("first-paragraph"); + private static final PseudoClass LAST_PAR = PseudoClass.getPseudoClass("last-paragraph"); + + + /* ********************************************************************** * + * * + * Properties * + * * + * Properties affect behavior and/or appearance of this control. * + * * + * They are readable and writable by the client code and never change by * + * other means, i.e. they contain either the default value or the value * + * set by the client code. * + * * + * ********************************************************************** */ + + /** + * Background fill for highlighted text. + */ + private final StyleableObjectProperty highlightFill + = new CssProperties.HighlightFillProperty(this, Color.DODGERBLUE); + + /** + * Text color for highlighted text. + */ + private final StyleableObjectProperty highlightTextFill + = new CssProperties.HighlightTextFillProperty(this, Color.WHITE); + + /** + * Controls the blink rate of the caret, when one is displayed. Setting + * the duration to zero disables blinking. + */ + private final StyleableObjectProperty caretBlinkRate + = new CssProperties.CaretBlinkRateProperty(this, javafx.util.Duration.millis(500)); + + // editable property + /** + * Indicates whether this text area can be edited by the user. + * Note that this property doesn't affect editing through the API. + */ + private final BooleanProperty editable = new EditableProperty<>(this); + public final boolean isEditable() { return editable.get(); } + public final void setEditable(boolean value) { editable.set(value); } + public final BooleanProperty editableProperty() { return editable; } + + // wrapText property + /** + * When a run of text exceeds the width of the text region, + * then this property indicates whether the text should wrap + * onto another line. + */ + private final BooleanProperty wrapText = new SimpleBooleanProperty(this, "wrapText"); + public final boolean isWrapText() { return wrapText.get(); } + public final void setWrapText(boolean value) { wrapText.set(value); } + public final BooleanProperty wrapTextProperty() { return wrapText; } + + // showCaret property + /** + * Indicates when this text area should display a caret. + */ + private final Var showCaret = Var.newSimpleVar(CaretVisibility.AUTO); + public final CaretVisibility getShowCaret() { return showCaret.getValue(); } + public final void setShowCaret(CaretVisibility value) { showCaret.setValue(value); } + public final Var showCaretProperty() { return showCaret; } + + public static enum CaretVisibility { + /** Caret is displayed. */ + ON, + /** Caret is displayed when area is focused, enabled, and editable. */ + AUTO, + /** Caret is not displayed. */ + OFF + } + + // undo manager + @Override public UndoManager getUndoManager() { return model.getUndoManager(); } + @Override public void setUndoManager(UndoManagerFactory undoManagerFactory) { + model.setUndoManager(undoManagerFactory); + } + + /** + * Popup window that will be positioned by this text area relative to the + * caret or selection. Use {@link #popupAlignmentProperty()} to specify + * how the popup should be positioned relative to the caret or selection. + * Use {@link #popupAnchorOffsetProperty()} or + * {@link #popupAnchorAdjustmentProperty()} to further adjust the position. + */ + private final ObjectProperty popupWindow = new SimpleObjectProperty<>(); + public void setPopupWindow(PopupWindow popup) { popupWindow.set(popup); } + public PopupWindow getPopupWindow() { return popupWindow.get(); } + public ObjectProperty popupWindowProperty() { return popupWindow; } + + /** @deprecated Use {@link #setPopupWindow(PopupWindow)}. */ + @Deprecated + public void setPopupAtCaret(PopupWindow popup) { popupWindow.set(popup); } + /** @deprecated Use {@link #getPopupWindow()}. */ + @Deprecated + public PopupWindow getPopupAtCaret() { return popupWindow.get(); } + /** @deprecated Use {@link #popupWindowProperty()}. */ + @Deprecated + public ObjectProperty popupAtCaretProperty() { return popupWindow; } + + /** + * Specifies further offset (in pixels) of the popup window from the + * position specified by {@link #popupAlignmentProperty()}. + * + *

If {@link #popupAnchorAdjustmentProperty()} is also specified, then + * it overrides the offset set by this property. + */ + private final ObjectProperty popupAnchorOffset = new SimpleObjectProperty<>(); + public void setPopupAnchorOffset(Point2D offset) { popupAnchorOffset.set(offset); } + public Point2D getPopupAnchorOffset() { return popupAnchorOffset.get(); } + public ObjectProperty popupAnchorOffsetProperty() { return popupAnchorOffset; } + + /** + * Specifies how to adjust the popup window's anchor point. The given + * operator is invoked with the screen position calculated according to + * {@link #popupAlignmentProperty()} and should return a new screen + * position. This position will be used as the popup window's anchor point. + * + *

Setting this property overrides {@link #popupAnchorOffsetProperty()}. + */ + private final ObjectProperty> popupAnchorAdjustment = new SimpleObjectProperty<>(); + public void setPopupAnchorAdjustment(UnaryOperator f) { popupAnchorAdjustment.set(f); } + public UnaryOperator getPopupAnchorAdjustment() { return popupAnchorAdjustment.get(); } + public ObjectProperty> popupAnchorAdjustmentProperty() { return popupAnchorAdjustment; } + + /** + * Defines where the popup window given in {@link #popupWindowProperty()} + * is anchored, i.e. where its anchor point is positioned. This position + * can further be adjusted by {@link #popupAnchorOffsetProperty()} or + * {@link #popupAnchorAdjustmentProperty()}. + */ + private final ObjectProperty popupAlignment = new SimpleObjectProperty<>(CARET_TOP); + public void setPopupAlignment(PopupAlignment pos) { popupAlignment.set(pos); } + public PopupAlignment getPopupAlignment() { return popupAlignment.get(); } + public ObjectProperty popupAlignmentProperty() { return popupAlignment; } + + /** + * Defines how long the mouse has to stay still over the text before a + * {@link MouseOverTextEvent} of type {@code MOUSE_OVER_TEXT_BEGIN} is + * fired on this text area. When set to {@code null}, no + * {@code MouseOverTextEvent}s are fired on this text area. + * + *

Default value is {@code null}. + */ + private final ObjectProperty mouseOverTextDelay = new SimpleObjectProperty<>(null); + public void setMouseOverTextDelay(Duration delay) { mouseOverTextDelay.set(delay); } + public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); } + public ObjectProperty mouseOverTextDelayProperty() { return mouseOverTextDelay; } + + /** + * Defines how to handle an event in which the user has selected some text, dragged it to a + * new location within the area, and released the mouse at some character {@code index} + * within the area. + * + *

By default, this will relocate the selected text to the character index where the mouse + * was released. To override it, use {@link #setOnSelectionDrop(IntConsumer)}. + */ + private Property onSelectionDrop = new SimpleObjectProperty<>(this::moveSelectedText); + public final void setOnSelectionDrop(IntConsumer consumer) { onSelectionDrop.setValue(consumer); } + public final IntConsumer getOnSelectionDrop() { return onSelectionDrop.getValue(); } + + private final ObjectProperty> paragraphGraphicFactory = new SimpleObjectProperty<>(null); + public void setParagraphGraphicFactory(IntFunction factory) { paragraphGraphicFactory.set(factory); } + public IntFunction getParagraphGraphicFactory() { return paragraphGraphicFactory.get(); } + public ObjectProperty> paragraphGraphicFactoryProperty() { return paragraphGraphicFactory; } + + /** + * Indicates whether the initial style should also be used for plain text + * inserted into this text area. When {@code false}, the style immediately + * preceding the insertion position is used. Default value is {@code false}. + */ + public BooleanProperty useInitialStyleForInsertionProperty() { return model.useInitialStyleForInsertionProperty(); } + public void setUseInitialStyleForInsertion(boolean value) { model.setUseInitialStyleForInsertion(value); } + public boolean getUseInitialStyleForInsertion() { return model.getUseInitialStyleForInsertion(); } + + private Optional, Codec>> styleCodecs = Optional.empty(); + /** + * Sets codecs to encode/decode style information to/from binary format. + * Providing codecs enables clipboard actions to retain the style information. + */ + public void setStyleCodecs(Codec paragraphStyleCodec, Codec textStyleCodec) { + styleCodecs = Optional.of(t(paragraphStyleCodec, textStyleCodec)); + } + @Override + public Optional, Codec>> getStyleCodecs() { + return styleCodecs; + } + + /** + * The estimated scrollX value. This can be set in order to scroll the content. + * Value is only accurate when area does not wrap lines and uses the same font size + * throughout the entire area. + */ + @Override + public Var estimatedScrollXProperty() { return virtualFlow.estimatedScrollXProperty(); } + public double getEstimatedScrollX() { return virtualFlow.estimatedScrollXProperty().getValue(); } + public void setEstimatedScrollX(double value) { virtualFlow.estimatedScrollXProperty().setValue(value); } + + /** + * The estimated scrollY value. This can be set in order to scroll the content. + * Value is only accurate when area does not wrap lines and uses the same font size + * throughout the entire area. + */ + @Override + public Var estimatedScrollYProperty() { return virtualFlow.estimatedScrollYProperty(); } + public double getEstimatedScrollY() { return virtualFlow.estimatedScrollYProperty().getValue(); } + public void setEstimatedScrollY(double value) { virtualFlow.estimatedScrollYProperty().setValue(value); } + + + /* ********************************************************************** * + * * + * Observables * + * * + * Observables are "dynamic" (i.e. changing) characteristics of this * + * control. They are not directly settable by the client code, but change * + * in response to user input and/or API actions. * + * * + * ********************************************************************** */ + + // text + @Override public final String getText() { return model.getText(); } + @Override public final ObservableValue textProperty() { return model.textProperty(); } + + // rich text + @Override public final StyledDocument getDocument() { return model.getDocument(); } + + // length + @Override public final int getLength() { return model.getLength(); } + @Override public final ObservableValue lengthProperty() { return model.lengthProperty(); } + + // caret position + @Override public final int getCaretPosition() { return model.getCaretPosition(); } + @Override public final ObservableValue caretPositionProperty() { return model.caretPositionProperty(); } + + // selection anchor + @Override public final int getAnchor() { return model.getAnchor(); } + @Override public final ObservableValue anchorProperty() { return model.anchorProperty(); } + + // selection + @Override public final IndexRange getSelection() { return model.getSelection(); } + @Override public final ObservableValue selectionProperty() { return model.selectionProperty(); } + + // selected text + @Override public final String getSelectedText() { return model.getSelectedText(); } + @Override public final ObservableValue selectedTextProperty() { return model.selectedTextProperty(); } + + // current paragraph index + @Override public final int getCurrentParagraph() { return model.getCurrentParagraph(); } + @Override public final ObservableValue currentParagraphProperty() { return model.currentParagraphProperty(); } + + // caret column + @Override public final int getCaretColumn() { return model.getCaretColumn(); } + @Override public final ObservableValue caretColumnProperty() { return model.caretColumnProperty(); } + + // paragraphs + @Override public LiveList> getParagraphs() { return model.getParagraphs(); } + + // beingUpdated + public ObservableBooleanValue beingUpdatedProperty() { return model.beingUpdatedProperty(); } + public boolean isBeingUpdated() { return model.isBeingUpdated(); } + + // total width estimate + /** + * The estimated width of the entire document. Accurate when area does not wrap lines and + * uses the same font size throughout the entire area. Value is only supposed to be set by + * the skin, not the user. + */ + @Override + public Val totalWidthEstimateProperty() { return virtualFlow.totalWidthEstimateProperty(); } + public double getTotalWidthEstimate() { return virtualFlow.totalWidthEstimateProperty().getValue(); } + + // total height estimate + /** + * The estimated height of the entire document. Accurate when area does not wrap lines and + * uses the same font size throughout the entire area. Value is only supposed to be set by + * the skin, not the user. + */ + @Override + public Val totalHeightEstimateProperty() { return virtualFlow.totalHeightEstimateProperty(); } + public double getTotalHeightEstimate() { return virtualFlow.totalHeightEstimateProperty().getValue(); } + + /* ********************************************************************** * + * * + * Event streams * + * * + * ********************************************************************** */ + + // text changes + @Override public final EventStream plainTextChanges() { return model.plainTextChanges(); } + + // rich text changes + @Override public final EventStream> richChanges() { return model.richChanges(); } + + /* ********************************************************************** * + * * + * Private fields * + * * + * ********************************************************************** */ + + private Subscription subscriptions = () -> {}; + + // Remembers horizontal position when traversing up / down. + private Optional targetCaretOffset = Optional.empty(); + + private final Binding caretVisible; + + private final Val> _popupAnchorAdjustment; + + private final VirtualFlow, Cell, ParagraphBox>> virtualFlow; + + // used for two-level navigation, where on the higher level are + // paragraphs and on the lower level are lines within a paragraph + private final TwoLevelNavigator navigator; + + private boolean followCaretRequested = false; + + /** + * model + */ + private final StyledTextAreaModel model; + + /** + * @return this area's {@link StyledTextAreaModel} + */ + final StyledTextAreaModel getModel() { + return model; + } + + /* ********************************************************************** * + * * + * Fields necessary for Cloning * + * * + * ********************************************************************** */ + + /** + * The underlying document that can be displayed by multiple {@code StyledTextArea}s. + */ + public final EditableStyledDocument getContent() { return model.getContent(); } + + /** + * Style used by default when no other style is provided. + */ + public final S getInitialTextStyle() { return model.getInitialTextStyle(); } + + /** + * Style used by default when no other style is provided. + */ + public final PS getInitialParagraphStyle() { return model.getInitialParagraphStyle(); } + + /** + * Style applicator used by the default skin. + */ + private final BiConsumer applyParagraphStyle; + public final BiConsumer getApplyParagraphStyle() { return applyParagraphStyle; } + + /** + * Indicates whether style should be preserved on undo/redo, + * copy/paste and text move. + * TODO: Currently, only undo/redo respect this flag. + */ + public final boolean isPreserveStyle() { return model.isPreserveStyle(); } + + /* ********************************************************************** * + * * + * Miscellaneous * + * * + * ********************************************************************** */ + + @Override public final SegmentOps getSegOps() { return getContent().getSegOps(); } + + /* ********************************************************************** * + * * + * Constructors * + * * + * ********************************************************************** */ + + /** + * Creates a text area with empty text content. + * + * @param initialParagraphStyle style to use in places where no other style is + * specified (yet). + * @param applyParagraphStyle function that, given a {@link TextFlow} node and + * a style, applies the style to the paragraph node. This function is + * used by the default skin to apply style to paragraph nodes. + * @param initialTextStyle style to use in places where no other style is + * specified (yet). + * @param segmentOps The operations which are defined on the text segment objects. + * @param nodeFactory A function which is used to create the JavaFX scene nodes for a + * particular segment. + */ + public GenericStyledArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, + S initialTextStyle, TextOps segmentOps, + Function nodeFactory) { + this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, true, nodeFactory); + } + + public GenericStyledArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, + S initialTextStyle, TextOps segmentOps, + boolean preserveStyle, Function nodeFactory) { + this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, + new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), segmentOps, preserveStyle, nodeFactory); + } + + /** + * The same as {@link #GenericStyledArea(Object, BiConsumer, Object, TextOps, Function)} except that + * this constructor can be used to create another {@code GenericStyledArea} object that + * shares the same {@link EditableStyledDocument}. + */ + public GenericStyledArea( + PS initialParagraphStyle, + BiConsumer applyParagraphStyle, + S initialTextStyle, + EditableStyledDocument document, + TextOps textOps, + Function nodeFactory) { + this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, document, textOps, true, nodeFactory); + + } + + public GenericStyledArea( + PS initialParagraphStyle, + BiConsumer applyParagraphStyle, + S initialTextStyle, + EditableStyledDocument document, + TextOps textOps, + boolean preserveStyle, + Function nodeFactory) { + this.model = new StyledTextAreaModel<>(initialParagraphStyle, initialTextStyle, document, textOps, preserveStyle); + this.applyParagraphStyle = applyParagraphStyle; + + // allow tab traversal into area + setFocusTraversable(true); + + this.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); + getStyleClass().add("styled-text-area"); + getStylesheets().add(StyledTextArea.class.getResource("styled-text-area.css").toExternalForm()); + + // keeps track of currently used non-empty cells + @SuppressWarnings("unchecked") + ObservableSet> nonEmptyCells = FXCollections.observableSet(); + + // Initialize content + virtualFlow = VirtualFlow.createVertical( + getParagraphs(), + par -> { + Cell, ParagraphBox> cell = createCell( + par, + applyParagraphStyle, + nodeFactory); + nonEmptyCells.add(cell.getNode()); + return cell.beforeReset(() -> nonEmptyCells.remove(cell.getNode())) + .afterUpdateItem(p -> nonEmptyCells.add(cell.getNode())); + }); + getChildren().add(virtualFlow); + + // initialize navigator + IntSupplier cellCount = () -> getParagraphs().size(); + IntUnaryOperator cellLength = i -> virtualFlow.getCell(i).getNode().getLineCount(); + navigator = new TwoLevelNavigator(cellCount, cellLength); + + // relayout the popup when any of its settings values change (besides the caret being dirty) + EventStream popupAlignmentDirty = invalidationsOf(popupAlignmentProperty()); + EventStream popupAnchorAdjustmentDirty = invalidationsOf(popupAnchorAdjustmentProperty()); + EventStream popupAnchorOffsetDirty = invalidationsOf(popupAnchorOffsetProperty()); + EventStream popupDirty = merge(popupAlignmentDirty, popupAnchorAdjustmentDirty, popupAnchorOffsetDirty); + subscribeTo(popupDirty, x -> layoutPopup()); + + // follow the caret every time the caret position or paragraphs change + EventStream caretPosDirty = invalidationsOf(caretPositionProperty()); + EventStream paragraphsDirty = invalidationsOf(getParagraphs()); + EventStream selectionDirty = invalidationsOf(selectionProperty()); + // need to reposition popup even when caret hasn't moved, but selection has changed (been deselected) + EventStream caretDirty = merge(caretPosDirty, paragraphsDirty, selectionDirty); + + // whether or not to display the caret + EventStream blinkCaret = EventStreams.valuesOf(showCaretProperty()) + .flatMap(mode -> { + switch (mode) { + case ON: + return EventStreams.valuesOf(Val.constant(true)); + case OFF: + return EventStreams.valuesOf(Val.constant(false)); + default: + case AUTO: + return EventStreams.valuesOf(focusedProperty() + .and(editableProperty()) + .and(disabledProperty().not())); + } + }); + + // the rate at which to display the caret + EventStream blinkRate = EventStreams.valuesOf(caretBlinkRate); + + // The caret is visible in periodic intervals, + // but only when blinkCaret is true. + caretVisible = EventStreams.combine(blinkCaret, blinkRate) + .flatMap(tuple -> { + Boolean blink = tuple.get1(); + javafx.util.Duration rate = tuple.get2(); + if(blink) { + return rate.lessThanOrEqualTo(ZERO) + ? EventStreams.valuesOf(Val.constant(true)) + : booleanPulse(rate, caretDirty); + } else { + return EventStreams.valuesOf(Val.constant(false)); + } + }) + .toBinding(false); + manageBinding(caretVisible); + + // Adjust popup anchor by either a user-provided function, + // or user-provided offset, or don't adjust at all. + Val> userOffset = Val.map( + popupAnchorOffsetProperty(), + offset -> anchor -> anchor.add(offset)); + _popupAnchorAdjustment = + Val.orElse( + popupAnchorAdjustmentProperty(), + userOffset) + .orElseConst(UnaryOperator.identity()); + + // dispatch MouseOverTextEvents when mouseOverTextDelay is not null + EventStreams.valuesOf(mouseOverTextDelayProperty()) + .flatMap(delay -> delay != null + ? mouseOverTextEvents(nonEmptyCells, delay) + : EventStreams.never()) + .subscribe(evt -> Event.fireEvent(this, evt)); + + new StyledTextAreaBehavior(this); + } + + + /* ********************************************************************** * + * * + * Queries * + * * + * Queries are parameterized observables. * + * * + * ********************************************************************** */ + + /** + * Returns caret bounds relative to the viewport, i.e. the visual bounds + * of the embedded VirtualFlow. + */ + Optional getCaretBounds() { + return virtualFlow.getCellIfVisible(getCurrentParagraph()) + .map(c -> { + Bounds cellBounds = c.getNode().getCaretBounds(); + return virtualFlow.cellToViewport(c, cellBounds); + }); + } + + /** + * Returns x coordinate of the caret in the current paragraph. + */ + ParagraphBox.CaretOffsetX getCaretOffsetX() { + int idx = getCurrentParagraph(); + return getCell(idx).getCaretOffsetX(); + } + + double getViewportHeight() { + return virtualFlow.getHeight(); + } + + CharacterHit hit(ParagraphBox.CaretOffsetX x, TwoDimensional.Position targetLine) { + int parIdx = targetLine.getMajor(); + ParagraphBox cell = virtualFlow.getCell(parIdx).getNode(); + CharacterHit parHit = cell.hitTextLine(x, targetLine.getMinor()); + return parHit.offset(getParagraphOffset(parIdx)); + } + + CharacterHit hit(ParagraphBox.CaretOffsetX x, double y) { + VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(0.0, y); + if(hit.isBeforeCells()) { + return CharacterHit.insertionAt(0); + } else if(hit.isAfterCells()) { + return CharacterHit.insertionAt(getLength()); + } else { + int parIdx = hit.getCellIndex(); + int parOffset = getParagraphOffset(parIdx); + ParagraphBox cell = hit.getCell().getNode(); + Point2D cellOffset = hit.getCellOffset(); + CharacterHit parHit = cell.hitText(x, cellOffset.getY()); + return parHit.offset(parOffset); + } + } + + /** + * Helpful for determining which letter is at point x, y: + *

+     *     {@code
+     *     StyledTextArea area = // creation code
+     *     area.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent e) -> {
+     *         CharacterHit hit = area.hit(e.getX(), e.getY());
+     *         int characterPosition = hit.getInsertionIndex();
+     *
+     *         // move the caret to that character's position
+     *         area.moveTo(characterPosition, SelectionPolicy.CLEAR);
+     *     }}
+     * 
+ */ + public CharacterHit hit(double x, double y) { + VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(x, y); + if(hit.isBeforeCells()) { + return CharacterHit.insertionAt(0); + } else if(hit.isAfterCells()) { + return CharacterHit.insertionAt(getLength()); + } else { + int parIdx = hit.getCellIndex(); + int parOffset = getParagraphOffset(parIdx); + ParagraphBox cell = hit.getCell().getNode(); + Point2D cellOffset = hit.getCellOffset(); + CharacterHit parHit = cell.hit(cellOffset); + return parHit.offset(parOffset); + } + } + + /** + * Returns the current line as a two-level index. + * The major number is the paragraph index, the minor + * number is the line number within the paragraph. + * + *

This method has a side-effect of bringing the current + * paragraph to the viewport if it is not already visible. + */ + TwoDimensional.Position currentLine() { + int parIdx = getCurrentParagraph(); + Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); + int lineIdx = cell.getNode().getCurrentLineIndex(); + return _position(parIdx, lineIdx); + } + + TwoDimensional.Position _position(int par, int line) { + return navigator.position(par, line); + } + + @Override + public final String getText(int start, int end) { + return model.getText(start, end); + } + + @Override + public String getText(int paragraph) { + return model.getText(paragraph); + } + + public Paragraph getParagraph(int index) { + return model.getParagraph(index); + } + + @Override + public StyledDocument subDocument(int start, int end) { + return model.subDocument(start, end); + } + + @Override + public StyledDocument subDocument(int paragraphIndex) { + return model.subDocument(paragraphIndex); + } + + /** + * Returns the selection range in the given paragraph. + */ + public IndexRange getParagraphSelection(int paragraph) { + return model.getParagraphSelection(paragraph); + } + + /** + * Returns the style of the character with the given index. + * If {@code index} points to a line terminator character, + * the last style used in the paragraph terminated by that + * line terminator is returned. + */ + public S getStyleOfChar(int index) { + return model.getStyleOfChar(index); + } + + /** + * Returns the style at the given position. That is the style of the + * character immediately preceding {@code position}, except when + * {@code position} points to a paragraph boundary, in which case it + * is the style at the beginning of the latter paragraph. + * + *

In other words, most of the time {@code getStyleAtPosition(p)} + * is equivalent to {@code getStyleOfChar(p-1)}, except when {@code p} + * points to a paragraph boundary, in which case it is equivalent to + * {@code getStyleOfChar(p)}. + */ + public S getStyleAtPosition(int position) { + return model.getStyleAtPosition(position); + } + + /** + * 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. If {@code position} + * points to a boundary between two paragraphs, then the first styled range + * of the latter paragraph is returned. + */ + public IndexRange getStyleRangeAtPosition(int position) { + return model.getStyleRangeAtPosition(position); + } + + /** + * Returns the styles in the given character range. + */ + public StyleSpans getStyleSpans(int from, int to) { + return model.getStyleSpans(from, to); + } + + /** + * Returns the styles in the given character range. + */ + public StyleSpans getStyleSpans(IndexRange range) { + return getStyleSpans(range.getStart(), range.getEnd()); + } + + /** + * Returns the style of the character with the given index in the given + * paragraph. If {@code index} is beyond the end of the paragraph, the + * style at the end of line is returned. If {@code index} is negative, it + * is the same as if it was 0. + */ + public S getStyleOfChar(int paragraph, int index) { + return model.getStyleOfChar(paragraph, index); + } + + /** + * Returns the style at the given position in the given paragraph. + * This is equivalent to {@code getStyleOfChar(paragraph, position-1)}. + */ + public S getStyleAtPosition(int paragraph, int position) { + return model.getStyleAtPosition(paragraph, position); + } + + /** + * Returns the range of homogeneous style that includes the given position + * in the given paragraph. If {@code position} points to a boundary between + * two styled ranges, then the range preceding {@code position} is returned. + */ + public IndexRange getStyleRangeAtPosition(int paragraph, int position) { + return model.getStyleRangeAtPosition(paragraph, position); + } + + /** + * Returns styles of the whole paragraph. + */ + public StyleSpans getStyleSpans(int paragraph) { + return model.getStyleSpans(paragraph); + } + + /** + * Returns the styles in the given character range of the given paragraph. + */ + public StyleSpans getStyleSpans(int paragraph, int from, int to) { + return model.getStyleSpans(paragraph, from, to); + } + + /** + * Returns the styles in the given character range of the given paragraph. + */ + public StyleSpans getStyleSpans(int paragraph, IndexRange range) { + return getStyleSpans(paragraph, range.getStart(), range.getEnd()); + } + + @Override + public int getAbsolutePosition(int paragraphIndex, int columnIndex) { + return model.getAbsolutePosition(paragraphIndex, columnIndex); + } + + @Override + public Position position(int row, int col) { + return model.position(row, col); + } + + @Override + public Position offsetToPosition(int charOffset, Bias bias) { + return model.offsetToPosition(charOffset, bias); + } + + + /* ********************************************************************** * + * * + * Actions * + * * + * Actions change the state of this control. They typically cause a * + * change of one or more observables and/or produce an event. * + * * + * ********************************************************************** */ + + void scrollBy(Point2D deltas) { + virtualFlow.scrollXBy(deltas.getX()); + virtualFlow.scrollYBy(deltas.getY()); + } + + void show(double y) { + virtualFlow.show(y); + } + + void showCaretAtBottom() { + int parIdx = getCurrentParagraph(); + Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); + Bounds caretBounds = cell.getNode().getCaretBounds(); + double y = caretBounds.getMaxY(); + virtualFlow.showAtOffset(parIdx, getViewportHeight() - y); + } + + void showCaretAtTop() { + int parIdx = getCurrentParagraph(); + Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); + Bounds caretBounds = cell.getNode().getCaretBounds(); + double y = caretBounds.getMinY(); + virtualFlow.showAtOffset(parIdx, -y); + } + + /** + * If the caret is not visible within the area's view, the area will scroll so that caret + * is visible in the next layout pass. Use this method when you wish to "follow the caret" + * (i.e. auto-scroll to caret) after making a change (add/remove/modify area's segments). + */ + public void requestFollowCaret() { + followCaretRequested = true; + requestLayout(); + } + + private void followCaret() { + int parIdx = getCurrentParagraph(); + Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); + Bounds caretBounds = cell.getNode().getCaretBounds(); + double graphicWidth = cell.getNode().getGraphicPrefWidth(); + Bounds region = extendLeft(caretBounds, graphicWidth); + virtualFlow.show(parIdx, region); + } + + /** + * Moves caret to the previous page (i.e. page up) + * @param selectionPolicy use {@link SelectionPolicy#CLEAR} when no selection is desired and + * {@link SelectionPolicy#ADJUST} when a selection from starting point + * to the place to where the caret is moved is desired. + */ + public void prevPage(SelectionPolicy selectionPolicy) { + showCaretAtBottom(); + CharacterHit hit = hit(getTargetCaretOffset(), 1.0); + model.moveTo(hit.getInsertionIndex(), selectionPolicy); + } + + /** + * Moves caret to the next page (i.e. page down) + * @param selectionPolicy use {@link SelectionPolicy#CLEAR} when no selection is desired and + * {@link SelectionPolicy#ADJUST} when a selection from starting point + * to the place to where the caret is moved is desired. + */ + public void nextPage(SelectionPolicy selectionPolicy) { + showCaretAtTop(); + CharacterHit hit = hit(getTargetCaretOffset(), getViewportHeight() - 1.0); + model.moveTo(hit.getInsertionIndex(), selectionPolicy); + } + + /** + * Sets style for the given character range. + */ + public void setStyle(int from, int to, S style) { + model.setStyle(from, to, style); + } + + /** + * Sets style for the whole paragraph. + */ + public void setStyle(int paragraph, S style) { + model.setStyle(paragraph, style); + } + + /** + * Sets style for the given range relative in the given paragraph. + */ + public void setStyle(int paragraph, int from, int to, S style) { + model.setStyle(paragraph, from, to, style); + } + + /** + * Set multiple style ranges at once. This is equivalent to + *

+     * for(StyleSpan{@code } span: styleSpans) {
+     *     setStyle(from, from + span.getLength(), span.getStyle());
+     *     from += span.getLength();
+     * }
+     * 
+ * but the actual implementation is more efficient. + */ + public void setStyleSpans(int from, StyleSpans styleSpans) { + model.setStyleSpans(from, styleSpans); + } + + /** + * Set multiple style ranges of a paragraph at once. This is equivalent to + *
+     * for(StyleSpan{@code } span: styleSpans) {
+     *     setStyle(paragraph, from, from + span.getLength(), span.getStyle());
+     *     from += span.getLength();
+     * }
+     * 
+ * but the actual implementation is more efficient. + */ + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + model.setStyleSpans(paragraph, from, styleSpans); + } + + /** + * Sets style for the whole paragraph. + */ + public void setParagraphStyle(int paragraph, PS paragraphStyle) { + model.setParagraphStyle(paragraph, paragraphStyle); + } + + /** + * Resets the style of the given range to the initial style. + */ + public void clearStyle(int from, int to) { + model.clearStyle(from, to); + } + + /** + * Resets the style of the given paragraph to the initial style. + */ + public void clearStyle(int paragraph) { + model.clearStyle(paragraph); + } + + /** + * Resets the style of the given range in the given paragraph + * to the initial style. + */ + public void clearStyle(int paragraph, int from, int to) { + model.clearStyle(paragraph, from, to); + } + + /** + * Resets the style of the given paragraph to the initial style. + */ + public void clearParagraphStyle(int paragraph) { + model.clearParagraphStyle(paragraph); + } + + @Override + public void replaceText(int start, int end, String text) { + model.replaceText(start, end, text); + } + + @Override + public void replace(int start, int end, StyledDocument replacement) { + model.replace(start, end, replacement); + } + + @Override + public void selectRange(int anchor, int caretPosition) { + model.selectRange(anchor, caretPosition); + } + + /** + * {@inheritDoc} + * @deprecated You probably meant to use {@link #moveTo(int)}. This method will be made + * package-private in the future + */ + @Deprecated + @Override + public void positionCaret(int pos) { + model.positionCaret(pos); + } + + /* ********************************************************************** * + * * + * Public API * + * * + * ********************************************************************** */ + + public void dispose() { + subscriptions.unsubscribe(); + model.dispose(); + virtualFlow.dispose(); + } + + /* ********************************************************************** * + * * + * Layout * + * * + * ********************************************************************** */ + + @Override + protected void layoutChildren() { + virtualFlow.resize(getWidth(), getHeight()); + if(followCaretRequested) { + followCaretRequested = false; + followCaret(); + } + + // position popup + layoutPopup(); + } + + /* ********************************************************************** * + * * + * Private methods * + * * + * ********************************************************************** */ + + private Cell, ParagraphBox> createCell( + Paragraph paragraph, + BiConsumer applyParagraphStyle, + Function nodeFactory) { + + ParagraphBox box = new ParagraphBox<>(paragraph, applyParagraphStyle, nodeFactory); + + box.highlightFillProperty().bind(highlightFill); + box.highlightTextFillProperty().bind(highlightTextFill); + box.wrapTextProperty().bind(wrapTextProperty()); + box.graphicFactoryProperty().bind(paragraphGraphicFactoryProperty()); + box.graphicOffset.bind(virtualFlow.breadthOffsetProperty()); + + Val hasCaret = Val.combine( + box.indexProperty(), + currentParagraphProperty(), + (bi, cp) -> bi.intValue() == cp.intValue()); + + Subscription hasCaretPseudoClass = hasCaret.values().subscribe(value -> box.pseudoClassStateChanged(HAS_CARET, value)); + Subscription firstParPseudoClass = box.indexProperty().values().subscribe(idx -> box.pseudoClassStateChanged(FIRST_PAR, idx == 0)); + Subscription lastParPseudoClass = EventStreams.combine( + box.indexProperty().values(), + getParagraphs().sizeProperty().values() + ).subscribe(in -> in.exec((i, n) -> box.pseudoClassStateChanged(LAST_PAR, i == n-1))); + + // caret is visible only in the paragraph with the caret + Val cellCaretVisible = hasCaret.flatMap(x -> x ? caretVisible : Val.constant(false)); + box.caretVisibleProperty().bind(cellCaretVisible); + + // bind cell's caret position to area's caret column, + // when the cell is the one with the caret + box.caretPositionProperty().bind(hasCaret.flatMap(has -> has + ? caretColumnProperty() + : Val.constant(0))); + + // keep paragraph selection updated + ObjectBinding cellSelection = Bindings.createObjectBinding(() -> { + int idx = box.getIndex(); + return idx != -1 + ? getParagraphSelection(idx) + : StyledTextArea.EMPTY_RANGE; + }, selectionProperty(), box.indexProperty()); + box.selectionProperty().bind(cellSelection); + + return new Cell, ParagraphBox>() { + @Override + public ParagraphBox getNode() { + return box; + } + + @Override + public void updateIndex(int index) { + box.setIndex(index); + } + + @Override + public void dispose() { + box.highlightFillProperty().unbind(); + box.highlightTextFillProperty().unbind(); + box.wrapTextProperty().unbind(); + box.graphicFactoryProperty().unbind(); + box.graphicOffset.unbind(); + + hasCaretPseudoClass.unsubscribe(); + firstParPseudoClass.unsubscribe(); + lastParPseudoClass.unsubscribe(); + + box.caretVisibleProperty().unbind(); + box.caretPositionProperty().unbind(); + + box.selectionProperty().unbind(); + cellSelection.dispose(); + } + }; + } + + private ParagraphBox getCell(int index) { + return virtualFlow.getCell(index).getNode(); + } + + private EventStream mouseOverTextEvents(ObservableSet> cells, Duration delay) { + return merge(cells, c -> c.stationaryIndices(delay).map(e -> e.unify( + l -> l.map((pos, charIdx) -> MouseOverTextEvent.beginAt(c.localToScreen(pos), getParagraphOffset(c.getIndex()) + charIdx)), + r -> MouseOverTextEvent.end()))); + } + + private int getParagraphOffset(int parIdx) { + return position(parIdx, 0).toOffset(); + } + + private void layoutPopup() { + PopupWindow popup = getPopupWindow(); + PopupAlignment alignment = getPopupAlignment(); + UnaryOperator adjustment = _popupAnchorAdjustment.getValue(); + if(popup != null) { + positionPopup(popup, alignment, adjustment); + } + } + + private void positionPopup( + PopupWindow popup, + PopupAlignment alignment, + UnaryOperator adjustment) { + Optional bounds = null; + switch(alignment.getAnchorObject()) { + case CARET: bounds = getCaretBoundsOnScreen(); break; + case SELECTION: bounds = getSelectionBoundsOnScreen(); break; + } + bounds.ifPresent(b -> { + double x = 0, y = 0; + switch(alignment.getHorizontalAlignment()) { + case LEFT: x = b.getMinX(); break; + case H_CENTER: x = (b.getMinX() + b.getMaxX()) / 2; break; + case RIGHT: x = b.getMaxX(); break; + } + switch(alignment.getVerticalAlignment()) { + case TOP: y = b.getMinY(); + case V_CENTER: y = (b.getMinY() + b.getMaxY()) / 2; break; + case BOTTOM: y = b.getMaxY(); break; + } + Point2D anchor = adjustment.apply(new Point2D(x, y)); + popup.setAnchorX(anchor.getX()); + popup.setAnchorY(anchor.getY()); + }); + } + + private Optional getCaretBoundsOnScreen() { + return virtualFlow.getCellIfVisible(getCurrentParagraph()) + .map(c -> c.getNode().getCaretBoundsOnScreen()); + } + + private Optional getSelectionBoundsOnScreen() { + IndexRange selection = getSelection(); + if(selection.getLength() == 0) { + return getCaretBoundsOnScreen(); + } + + Bounds[] bounds = virtualFlow.visibleCells().stream() + .map(c -> c.getNode().getSelectionBoundsOnScreen()) + .filter(Optional::isPresent) + .map(Optional::get) + .toArray(Bounds[]::new); + + if(bounds.length == 0) { + return Optional.empty(); + } + double minX = Stream.of(bounds).mapToDouble(Bounds::getMinX).min().getAsDouble(); + double maxX = Stream.of(bounds).mapToDouble(Bounds::getMaxX).max().getAsDouble(); + double minY = Stream.of(bounds).mapToDouble(Bounds::getMinY).min().getAsDouble(); + double maxY = Stream.of(bounds).mapToDouble(Bounds::getMaxY).max().getAsDouble(); + return Optional.of(new BoundingBox(minX, minY, maxX-minX, maxY-minY)); + } + + private void subscribeTo(EventStream src, Consumer cOnsumer) { + manageSubscription(src.subscribe(cOnsumer)); + } + + private void manageSubscription(Subscription subscription) { + subscriptions = subscriptions.and(subscription); + } + + private void manageBinding(Binding binding) { + subscriptions = subscriptions.and(binding::dispose); + } + + private static Bounds extendLeft(Bounds b, double w) { + if(w == 0) { + return b; + } else { + return new BoundingBox( + b.getMinX() - w, b.getMinY(), + b.getWidth() + w, b.getHeight()); + } + } + + void clearTargetCaretOffset() { + targetCaretOffset = Optional.empty(); + } + + ParagraphBox.CaretOffsetX getTargetCaretOffset() { + if(!targetCaretOffset.isPresent()) + targetCaretOffset = Optional.of(getCaretOffsetX()); + return targetCaretOffset.get(); + } + + private static EventStream booleanPulse(javafx.util.Duration javafxDuration, EventStream restartImpulse) { + Duration duration = Duration.ofMillis(Math.round(javafxDuration.toMillis())); + EventStream ticks = EventStreams.restartableTicks(duration, restartImpulse); + return StateMachine.init(false) + .on(restartImpulse.withDefaultEvent(null)).transition((state, impulse) -> true) + .on(ticks).transition((state, tick) -> !state) + .toStateStream(); + } +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java index 1fce6daa1..29bb4ccd3 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java @@ -1,11 +1,12 @@ package org.fxmisc.richtext; +import javafx.scene.text.TextFlow; + import org.fxmisc.richtext.model.Codec; import org.fxmisc.richtext.model.EditableStyledDocument; import org.fxmisc.richtext.model.SimpleEditableStyledDocument; - -import javafx.scene.text.TextFlow; +import org.fxmisc.richtext.model.StyledText; /** * Text area that uses inline css to define style of text segments and paragraph segments. @@ -13,10 +14,10 @@ public class InlineCssTextArea extends StyledTextArea { public InlineCssTextArea() { - this(new SimpleEditableStyledDocument<>("", "")); + this(new SimpleEditableStyledDocument<>("", "", StyledText.textOps())); } - public InlineCssTextArea(EditableStyledDocument document) { + public InlineCssTextArea(EditableStyledDocument, String> document) { super( "", TextFlow::setStyle, "", TextExt::setStyle, @@ -38,7 +39,7 @@ public InlineCssTextArea(String text) { getUndoManager().forgetHistory(); getUndoManager().mark(); - setStyleCodecs(Codec.STRING_CODEC, Codec.STRING_CODEC); + setStyleCodecs(Codec.STRING_CODEC, StyledText.codec(Codec.STRING_CODEC)); // position the caret at the beginning selectRange(0, 0); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java index ca7c4fd26..0ab625cec 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java @@ -6,6 +6,7 @@ import java.util.Optional; import java.util.OptionalInt; import java.util.function.BiConsumer; +import java.util.function.Function; import java.util.function.IntFunction; import javafx.beans.property.BooleanProperty; @@ -32,7 +33,7 @@ import org.reactfx.value.Val; import org.reactfx.value.Var; -class ParagraphBox extends Region { +class ParagraphBox extends Region { /** * An opaque class representing horizontal caret offset. @@ -47,7 +48,7 @@ private CaretOffsetX(double value) { } } - private final ParagraphText text; + private final ParagraphText text; private final ObjectProperty> graphicFactory = new SimpleObjectProperty<>(null); @@ -70,9 +71,10 @@ public ObjectProperty> graphicFactoryProperty() { public void setIndex(int index) { this.index.setValue(index); } public int getIndex() { return index.getValue(); } - ParagraphBox(Paragraph par, BiConsumer applyParagraphStyle, BiConsumer applyStyle) { + ParagraphBox(Paragraph par, BiConsumer applyParagraphStyle, + Function nodeFactory) { this.getStyleClass().add("paragraph-box"); - this.text = new ParagraphText<>(par, applyStyle); + this.text = new ParagraphText<>(par, nodeFactory); applyParagraphStyle.accept(this.text, par.getParagraphStyle()); this.index = Var.newSimpleVar(0); getChildren().add(text); @@ -108,7 +110,7 @@ public String toString() { public Property selectionProperty() { return text.selectionProperty(); } - Paragraph getParagraph() { + Paragraph getParagraph() { return text.getParagraph(); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java index f185a9217..1062d9e01 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.function.BiConsumer; +import java.util.function.Function; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; @@ -19,11 +19,10 @@ import javafx.scene.shape.StrokeLineCap; import org.fxmisc.richtext.model.Paragraph; -import org.fxmisc.richtext.model.Segment; import org.reactfx.value.Val; import org.reactfx.value.Var; -class ParagraphText extends TextFlowExt { +class ParagraphText extends TextFlowExt { // FIXME: changing it currently has not effect, because // Text.impl_selectionFillProperty().set(newFill) doesn't work @@ -38,11 +37,11 @@ public ObjectProperty highlightTextFillProperty() { public void setCaretPosition(int pos) { caretPosition.setValue(pos); } private final Val clampedCaretPosition; - private final ObjectProperty selection = new SimpleObjectProperty<>(StyledTextArea.EMPTY_RANGE); + private final ObjectProperty selection = new SimpleObjectProperty<>(GenericRichtextArea.EMPTY_RANGE); public ObjectProperty selectionProperty() { return selection; } public void setSelection(IndexRange sel) { selection.set(sel); } - private final Paragraph paragraph; + private final Paragraph paragraph; private final Path caretShape = new Path(); private final Path selectionShape = new Path(); @@ -57,7 +56,7 @@ public ObjectProperty highlightTextFillProperty() { caretShape.visibleProperty().bind(caretVisible); } - public ParagraphText(Paragraph par, BiConsumer applyStyle) { + ParagraphText(Paragraph par, Function nodeFactory) { this.paragraph = par; getStyleClass().add("paragraph-text"); @@ -97,12 +96,11 @@ public ParagraphText(Paragraph par, BiConsumer applyS // } // }); - // populate with nodes - for(Segment segment: par.getSegments()) { - - // Create the object node - Node t = segment.createNode(); - getChildren().add(t); + // populate with text nodes + for(SEG segment: par.getSegments()) { + // create Segment + Node fxNode = nodeFactory.apply(segment); + getChildren().add(fxNode); // add corresponding background node (empty) Path backgroundShape = new Path(); @@ -124,7 +122,7 @@ public ParagraphText(Paragraph par, BiConsumer applyS } } - public Paragraph getParagraph() { + public Paragraph getParagraph() { return paragraph; } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java index ed57050e7..7fad2ea3c 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java @@ -6,29 +6,30 @@ 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(EditableStyledDocument, Collection> document, boolean preserveStyle) { + public StyleClassedTextArea(EditableStyledDocument, StyledText>, Collection> document, boolean preserveStyle) { super(Collections.emptyList(), - (paragraph, styleClasses) -> paragraph.getStyleClass().addAll(styleClasses), - Collections.emptyList(), - (text, styleClasses) -> text.getStyleClass().addAll(styleClasses), - document, preserveStyle + (paragraph, styleClasses) -> paragraph.getStyleClass().addAll(styleClasses), + Collections.emptyList(), + (text, styleClasses) -> text.getStyleClass().addAll(styleClasses), + document, preserveStyle ); setStyleCodecs( Codec.collectionCodec(Codec.STRING_CODEC), - Codec.collectionCodec(Codec.STRING_CODEC) + StyledText.codec(Codec.collectionCodec(Codec.STRING_CODEC)) ); } public StyleClassedTextArea(boolean preserveStyle) { this( new SimpleEditableStyledDocument<>( - Collections.emptyList(), Collections.emptyList() + Collections.emptyList(), Collections.emptyList(), StyledText.textOps() ), preserveStyle); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java index 7628d6491..ff562c3c5 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java @@ -1,88 +1,15 @@ package org.fxmisc.richtext; -import static javafx.util.Duration.*; -import static org.fxmisc.richtext.PopupAlignment.*; -import static org.reactfx.EventStreams.*; -import static org.reactfx.util.Tuples.*; - -import java.time.Duration; -import java.util.Optional; import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.IntConsumer; -import java.util.function.IntFunction; -import java.util.function.IntSupplier; -import java.util.function.IntUnaryOperator; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; -import javafx.beans.binding.Binding; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.ObjectBinding; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ObservableSet; -import javafx.css.PseudoClass; -import javafx.css.StyleableObjectProperty; -import javafx.event.Event; -import javafx.geometry.BoundingBox; -import javafx.geometry.Bounds; -import javafx.geometry.Insets; -import javafx.geometry.Point2D; import javafx.geometry.VPos; import javafx.scene.Node; -import javafx.scene.control.IndexRange; -import javafx.scene.image.Image; -import javafx.scene.image.ImageView; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.Region; -import javafx.scene.paint.Color; -import javafx.scene.paint.Paint; -import javafx.scene.text.Text; import javafx.scene.text.TextFlow; -import javafx.stage.PopupWindow; -import org.fxmisc.flowless.Cell; -import org.fxmisc.flowless.VirtualFlow; -import org.fxmisc.flowless.VirtualFlowHit; -import org.fxmisc.flowless.Virtualized; -import org.fxmisc.flowless.VirtualizedScrollPane; -import org.fxmisc.richtext.CssProperties.EditableProperty; -import org.fxmisc.richtext.model.Codec; -import org.fxmisc.richtext.model.EditActions; import org.fxmisc.richtext.model.EditableStyledDocument; -import org.fxmisc.richtext.model.LinkedImage; -import org.fxmisc.richtext.model.NavigationActions; -import org.fxmisc.richtext.model.Paragraph; -import org.fxmisc.richtext.model.PlainTextChange; -import org.fxmisc.richtext.model.RichTextChange; +import org.fxmisc.richtext.model.SegmentOps; import org.fxmisc.richtext.model.SimpleEditableStyledDocument; -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyledDocument; import org.fxmisc.richtext.model.StyledText; -import org.fxmisc.richtext.model.StyledTextAreaModel; -import org.fxmisc.richtext.model.TextEditingArea; -import org.fxmisc.richtext.model.TwoDimensional; -import org.fxmisc.richtext.model.TwoLevelNavigator; -import org.fxmisc.richtext.model.UndoActions; -import org.fxmisc.undo.UndoManager; -import org.fxmisc.undo.UndoManagerFactory; -import org.reactfx.EventStream; -import org.reactfx.EventStreams; -import org.reactfx.StateMachine; -import org.reactfx.Subscription; -import org.reactfx.collection.LiveList; -import org.reactfx.util.Tuple2; -import org.reactfx.value.Val; -import org.reactfx.value.Var; /** * Text editing control. Accepts user input (keyboard, mouse) and @@ -94,7 +21,7 @@ * *

Note: Scroll bars no longer appear when the content spans outside * of the viewport. To add scroll bars, the area needs to be wrapped in - * a {@link VirtualizedScrollPane}. For example,

+ * a {@link org.fxmisc.flowless.VirtualizedScrollPane}. For example,

*
  * {@code
  * // shows area without scroll bars
@@ -108,20 +35,6 @@
  * }
  * 
* - *

Auto-Scrolling to the Caret

- * - *

Every time the underlying {@link EditableStyledDocument} changes via user interaction (e.g. typing) through - * the {@code StyledTextArea}, the area will scroll to insure the caret is kept in view. However, this does not - * occur if changes are done programmatically. For example, let's say the area is displaying the bottom part - * of the area's {@link EditableStyledDocument} and some code changes something in the top part of the document - * that is not currently visible. If there is no call to {@link #requestFollowCaret()} at the end of that code, - * the area will not auto-scroll to that section of the document. The change will occur, and the user will continue - * to see the bottom part of the document as before. If such a call is there, then the area will scroll - * to the top of the document and no longer display the bottom part of it.

- * - *

Additionally, when overriding the default user-interaction behavior, remember to include a call - * to {@link #requestFollowCaret()}.

- * *

Overriding keyboard shortcuts

* * {@code StyledTextArea} uses {@code KEY_TYPED} handler to handle ordinary @@ -146,1237 +59,53 @@ * * @param type of style that can be applied to text. */ -public class StyledTextArea extends Region - implements - TextEditingArea, - EditActions, - ClipboardActions, - NavigationActions, - UndoActions, - TwoDimensional, - Virtualized { - - /** - * Index range [0, 0). - */ - public static final IndexRange EMPTY_RANGE = new IndexRange(0, 0); - - private static final PseudoClass HAS_CARET = PseudoClass.getPseudoClass("has-caret"); - private static final PseudoClass FIRST_PAR = PseudoClass.getPseudoClass("first-paragraph"); - private static final PseudoClass LAST_PAR = PseudoClass.getPseudoClass("last-paragraph"); - - - /* ********************************************************************** * - * * - * Properties * - * * - * Properties affect behavior and/or appearance of this control. * - * * - * They are readable and writable by the client code and never change by * - * other means, i.e. they contain either the default value or the value * - * set by the client code. * - * * - * ********************************************************************** */ - - /** - * Background fill for highlighted text. - */ - private final StyleableObjectProperty highlightFill - = new CssProperties.HighlightFillProperty(this, Color.DODGERBLUE); - - /** - * Text color for highlighted text. - */ - private final StyleableObjectProperty highlightTextFill - = new CssProperties.HighlightTextFillProperty(this, Color.WHITE); - - /** - * Controls the blink rate of the caret, when one is displayed. Setting - * the duration to zero disables blinking. - */ - private final StyleableObjectProperty caretBlinkRate - = new CssProperties.CaretBlinkRateProperty(this, javafx.util.Duration.millis(500)); - - // editable property - /** - * Indicates whether this text area can be edited by the user. - * Note that this property doesn't affect editing through the API. - */ - private final BooleanProperty editable = new EditableProperty<>(this); - public final boolean isEditable() { return editable.get(); } - public final void setEditable(boolean value) { editable.set(value); } - public final BooleanProperty editableProperty() { return editable; } - - // wrapText property - /** - * When a run of text exceeds the width of the text region, - * then this property indicates whether the text should wrap - * onto another line. - */ - private final BooleanProperty wrapText = new SimpleBooleanProperty(this, "wrapText"); - public final boolean isWrapText() { return wrapText.get(); } - public final void setWrapText(boolean value) { wrapText.set(value); } - public final BooleanProperty wrapTextProperty() { return wrapText; } - - // showCaret property - /** - * Indicates when this text area should display a caret. - */ - private final Var showCaret = Var.newSimpleVar(CaretVisibility.AUTO); - public final CaretVisibility getShowCaret() { return showCaret.getValue(); } - public final void setShowCaret(CaretVisibility value) { showCaret.setValue(value); } - public final Var showCaretProperty() { return showCaret; } - - public static enum CaretVisibility { - /** Caret is displayed. */ - ON, - /** Caret is displayed when area is focused, enabled, and editable. */ - AUTO, - /** Caret is not displayed. */ - OFF - } - - // undo manager - @Override public UndoManager getUndoManager() { return model.getUndoManager(); } - @Override public void setUndoManager(UndoManagerFactory undoManagerFactory) { - model.setUndoManager(undoManagerFactory); - } - - /** - * Popup window that will be positioned by this text area relative to the - * caret or selection. Use {@link #popupAlignmentProperty()} to specify - * how the popup should be positioned relative to the caret or selection. - * Use {@link #popupAnchorOffsetProperty()} or - * {@link #popupAnchorAdjustmentProperty()} to further adjust the position. - */ - private final ObjectProperty popupWindow = new SimpleObjectProperty<>(); - public void setPopupWindow(PopupWindow popup) { popupWindow.set(popup); } - public PopupWindow getPopupWindow() { return popupWindow.get(); } - public ObjectProperty popupWindowProperty() { return popupWindow; } - - /** @deprecated Use {@link #setPopupWindow(PopupWindow)}. */ - @Deprecated - public void setPopupAtCaret(PopupWindow popup) { popupWindow.set(popup); } - /** @deprecated Use {@link #getPopupWindow()}. */ - @Deprecated - public PopupWindow getPopupAtCaret() { return popupWindow.get(); } - /** @deprecated Use {@link #popupWindowProperty()}. */ - @Deprecated - public ObjectProperty popupAtCaretProperty() { return popupWindow; } - - /** - * Specifies further offset (in pixels) of the popup window from the - * position specified by {@link #popupAlignmentProperty()}. - * - *

If {@link #popupAnchorAdjustmentProperty()} is also specified, then - * it overrides the offset set by this property. - */ - private final ObjectProperty popupAnchorOffset = new SimpleObjectProperty<>(); - public void setPopupAnchorOffset(Point2D offset) { popupAnchorOffset.set(offset); } - public Point2D getPopupAnchorOffset() { return popupAnchorOffset.get(); } - public ObjectProperty popupAnchorOffsetProperty() { return popupAnchorOffset; } - - /** - * Specifies how to adjust the popup window's anchor point. The given - * operator is invoked with the screen position calculated according to - * {@link #popupAlignmentProperty()} and should return a new screen - * position. This position will be used as the popup window's anchor point. - * - *

Setting this property overrides {@link #popupAnchorOffsetProperty()}. - */ - private final ObjectProperty> popupAnchorAdjustment = new SimpleObjectProperty<>(); - public void setPopupAnchorAdjustment(UnaryOperator f) { popupAnchorAdjustment.set(f); } - public UnaryOperator getPopupAnchorAdjustment() { return popupAnchorAdjustment.get(); } - public ObjectProperty> popupAnchorAdjustmentProperty() { return popupAnchorAdjustment; } - - /** - * Defines where the popup window given in {@link #popupWindowProperty()} - * is anchored, i.e. where its anchor point is positioned. This position - * can further be adjusted by {@link #popupAnchorOffsetProperty()} or - * {@link #popupAnchorAdjustmentProperty()}. - */ - private final ObjectProperty popupAlignment = new SimpleObjectProperty<>(CARET_TOP); - public void setPopupAlignment(PopupAlignment pos) { popupAlignment.set(pos); } - public PopupAlignment getPopupAlignment() { return popupAlignment.get(); } - public ObjectProperty popupAlignmentProperty() { return popupAlignment; } - - /** - * Defines how long the mouse has to stay still over the text before a - * {@link MouseOverTextEvent} of type {@code MOUSE_OVER_TEXT_BEGIN} is - * fired on this text area. When set to {@code null}, no - * {@code MouseOverTextEvent}s are fired on this text area. - * - *

Default value is {@code null}. - */ - private final ObjectProperty mouseOverTextDelay = new SimpleObjectProperty<>(null); - public void setMouseOverTextDelay(Duration delay) { mouseOverTextDelay.set(delay); } - public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); } - public ObjectProperty mouseOverTextDelayProperty() { return mouseOverTextDelay; } - - /** - * Defines how to handle an event in which the user has selected some text, dragged it to a - * new location within the area, and released the mouse at some character {@code index} - * within the area. - * - *

By default, this will relocate the selected text to the character index where the mouse - * was released. To override it, use {@link #setOnSelectionDrop(IntConsumer)}. - */ - private Property onSelectionDrop = new SimpleObjectProperty<>(this::moveSelectedText); - public final void setOnSelectionDrop(IntConsumer consumer) { onSelectionDrop.setValue(consumer); } - public final IntConsumer getOnSelectionDrop() { return onSelectionDrop.getValue(); } - - private final ObjectProperty> paragraphGraphicFactory = new SimpleObjectProperty<>(null); - public void setParagraphGraphicFactory(IntFunction factory) { paragraphGraphicFactory.set(factory); } - public IntFunction getParagraphGraphicFactory() { return paragraphGraphicFactory.get(); } - public ObjectProperty> paragraphGraphicFactoryProperty() { return paragraphGraphicFactory; } - - /** - * Indicates whether the initial style should also be used for plain text - * inserted into this text area. When {@code false}, the style immediately - * preceding the insertion position is used. Default value is {@code false}. - */ - public BooleanProperty useInitialStyleForInsertionProperty() { return model.useInitialStyleForInsertionProperty(); } - public void setUseInitialStyleForInsertion(boolean value) { model.setUseInitialStyleForInsertion(value); } - public boolean getUseInitialStyleForInsertion() { return model.getUseInitialStyleForInsertion(); } - - private Optional, Codec>> styleCodecs = Optional.empty(); - /** - * Sets codecs to encode/decode style information to/from binary format. - * Providing codecs enables clipboard actions to retain the style information. - */ - public void setStyleCodecs(Codec paragraphStyleCodec, Codec textStyleCodec) { - styleCodecs = Optional.of(t(paragraphStyleCodec, textStyleCodec)); - } - @Override - public Optional, Codec>> getStyleCodecs() { - return styleCodecs; - } - - /** - * The estimated scrollX value. This can be set in order to scroll the content. - * Value is only accurate when area does not wrap lines and uses the same font size - * throughout the entire area. - */ - @Override - public Var estimatedScrollXProperty() { return virtualFlow.estimatedScrollXProperty(); } - public double getEstimatedScrollX() { return virtualFlow.estimatedScrollXProperty().getValue(); } - public void setEstimatedScrollX(double value) { virtualFlow.estimatedScrollXProperty().setValue(value); } - - /** - * The estimated scrollY value. This can be set in order to scroll the content. - * Value is only accurate when area does not wrap lines and uses the same font size - * throughout the entire area. - */ - @Override - public Var estimatedScrollYProperty() { return virtualFlow.estimatedScrollYProperty(); } - public double getEstimatedScrollY() { return virtualFlow.estimatedScrollYProperty().getValue(); } - public void setEstimatedScrollY(double value) { virtualFlow.estimatedScrollYProperty().setValue(value); } - - - /* ********************************************************************** * - * * - * Observables * - * * - * Observables are "dynamic" (i.e. changing) characteristics of this * - * control. They are not directly settable by the client code, but change * - * in response to user input and/or API actions. * - * * - * ********************************************************************** */ - - // text - @Override public final String getText() { return model.getText(); } - @Override public final ObservableValue textProperty() { return model.textProperty(); } - - // rich text - @Override public final StyledDocument getDocument() { return model.getDocument(); } - - // length - @Override public final int getLength() { return model.getLength(); } - @Override public final ObservableValue lengthProperty() { return model.lengthProperty(); } - - // caret position - @Override public final int getCaretPosition() { return model.getCaretPosition(); } - @Override public final ObservableValue caretPositionProperty() { return model.caretPositionProperty(); } - - // selection anchor - @Override public final int getAnchor() { return model.getAnchor(); } - @Override public final ObservableValue anchorProperty() { return model.anchorProperty(); } - - // selection - @Override public final IndexRange getSelection() { return model.getSelection(); } - @Override public final ObservableValue selectionProperty() { return model.selectionProperty(); } - - // selected text - @Override public final String getSelectedText() { return model.getSelectedText(); } - @Override public final ObservableValue selectedTextProperty() { return model.selectedTextProperty(); } - - // current paragraph index - @Override public final int getCurrentParagraph() { return model.getCurrentParagraph(); } - @Override public final ObservableValue currentParagraphProperty() { return model.currentParagraphProperty(); } - - // caret column - @Override public final int getCaretColumn() { return model.getCaretColumn(); } - @Override public final ObservableValue caretColumnProperty() { return model.caretColumnProperty(); } - - // paragraphs - @Override public LiveList> getParagraphs() { return model.getParagraphs(); } - - // beingUpdated - public ObservableBooleanValue beingUpdatedProperty() { return model.beingUpdatedProperty(); } - public boolean isBeingUpdated() { return model.isBeingUpdated(); } - - // total width estimate - /** - * The estimated width of the entire document. Accurate when area does not wrap lines and - * uses the same font size throughout the entire area. Value is only supposed to be set by - * the skin, not the user. - */ - @Override - public Val totalWidthEstimateProperty() { return virtualFlow.totalWidthEstimateProperty(); } - public double getTotalWidthEstimate() { return virtualFlow.totalWidthEstimateProperty().getValue(); } - - // total height estimate - /** - * The estimated height of the entire document. Accurate when area does not wrap lines and - * uses the same font size throughout the entire area. Value is only supposed to be set by - * the skin, not the user. - */ - @Override - public Val totalHeightEstimateProperty() { return virtualFlow.totalHeightEstimateProperty(); } - public double getTotalHeightEstimate() { return virtualFlow.totalHeightEstimateProperty().getValue(); } - - /* ********************************************************************** * - * * - * Event streams * - * * - * ********************************************************************** */ - - // text changes - @Override public final EventStream plainTextChanges() { return model.plainTextChanges(); } - - // rich text changes - @Override public final EventStream> richChanges() { return model.richChanges(); } - - /* ********************************************************************** * - * * - * Private fields * - * * - * ********************************************************************** */ - - private final StyledTextAreaBehavior behavior; - - private Subscription subscriptions = () -> {}; - - // Remembers horizontal position when traversing up / down. - private Optional targetCaretOffset = Optional.empty(); - - private final Binding caretVisible; - - private final Val> _popupAnchorAdjustment; - - private final VirtualFlow, Cell, ParagraphBox>> virtualFlow; - - // used for two-level navigation, where on the higher level are - // paragraphs and on the lower level are lines within a paragraph - private final TwoLevelNavigator navigator; - - private boolean followCaretRequested = false; - - /** - * model - */ - private final StyledTextAreaModel model; - - /** - * @return this area's {@link StyledTextAreaModel} - */ - final StyledTextAreaModel getModel() { - return model; - } +public class StyledTextArea extends GenericStyledArea, S> { - /* ********************************************************************** * - * * - * Fields necessary for Cloning * - * * - * ********************************************************************** */ - - /** - * The underlying document that can be displayed by multiple {@code StyledTextArea}s. - */ - public final EditableStyledDocument getContent() { return model.getContent(); } - - /** - * Style used by default when no other style is provided. - */ - public final S getInitialTextStyle() { return model.getInitialTextStyle(); } - - /** - * Style used by default when no other style is provided. - */ - public final PS getInitialParagraphStyle() { return model.getInitialParagraphStyle(); } - - /** - * Style applicator used by the default skin. - */ - private final BiConsumer applyStyle; - public final BiConsumer getApplyStyle() { return applyStyle; } - - /** - * Style applicator used by the default skin. - */ - private final BiConsumer applyParagraphStyle; - public final BiConsumer getApplyParagraphStyle() { return applyParagraphStyle; } - - /** - * Indicates whether style should be preserved on undo/redo, - * copy/paste and text move. - * TODO: Currently, only undo/redo respect this flag. - */ - public final boolean isPreserveStyle() { return model.isPreserveStyle(); } - - /* ********************************************************************** * - * * - * Constructors * - * * - * ********************************************************************** */ - - /** - * Creates a text area with empty text content. - * - * @param initialTextStyle style to use in places where no other style is - * specified (yet). - * @param applyStyle function that, given a {@link Text} node and - * a style, applies the style to the text node. This function is - * used by the default skin to apply style to text nodes. - * @param initialParagraphStyle style to use in places where no other style is - * specified (yet). - * @param applyParagraphStyle function that, given a {@link TextFlow} node and - * a style, applies the style to the paragraph node. This function is - * used by the default skin to apply style to paragraph nodes. - */ public StyledTextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, BiConsumer applyStyle - ) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, applyStyle, true); - } - - public StyledTextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, BiConsumer applyStyle, - boolean preserveStyle - ) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, applyStyle, - new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle), preserveStyle); + S initialTextStyle, BiConsumer applyStyle, + EditableStyledDocument, S> document, boolean preserveStyle) { + super(initialParagraphStyle, applyParagraphStyle, + initialTextStyle, + document, StyledText.textOps(), preserveStyle, + seg -> createStyledTextNode(seg, document.getSegOps(), applyStyle)); } - /** - * The same as {@link #StyledTextArea(Object, BiConsumer, Object, BiConsumer)} except that - * this constructor can be used to create another {@code StyledTextArea} object that - * shares the same {@link EditableStyledDocument}. - */ public StyledTextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, S initialTextStyle, BiConsumer applyStyle, - EditableStyledDocument document - ) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, applyStyle, document, true); - + EditableStyledDocument, S> document) { + this(initialParagraphStyle, applyParagraphStyle, + initialTextStyle, applyStyle, document, true); } public StyledTextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, S initialTextStyle, BiConsumer applyStyle, - EditableStyledDocument document, boolean preserveStyle) { - this.model = new StyledTextAreaModel<>(initialParagraphStyle, initialTextStyle, document, preserveStyle); - this.applyStyle = applyStyle; - this.applyParagraphStyle = applyParagraphStyle; - - // Inject node factories into model layer - StyledText.setNodeFactory(segment -> { - TextExt t = new TextExt(segment.getText()); - t.setTextOrigin(VPos.TOP); - t.getStyleClass().add("text"); - this.applyStyle.accept(t, segment.getStyle()); - - // XXX: binding selectionFill to textFill, - // see the note at highlightTextFill - t.impl_selectionFillProperty().bind(t.fillProperty()); - - return t; - }); - - LinkedImage.setNodeFactory(segment -> { - String imagePath = segment.getImagePath(); - Image image = new Image("file:" + imagePath); // XXX: No need to create new Image objects each time - - // could be cached in the model layer - - ImageView result = new ImageView(image); - return result; - }); - - // allow tab traversal into area - setFocusTraversable(true); - - this.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); - getStyleClass().add("styled-text-area"); - getStylesheets().add(StyledTextArea.class.getResource("styled-text-area.css").toExternalForm()); - - // keeps track of currently used non-empty cells - @SuppressWarnings("unchecked") - ObservableSet> nonEmptyCells = FXCollections.observableSet(); - - // Initialize content - virtualFlow = VirtualFlow.createVertical( - getParagraphs(), - par -> { - Cell, ParagraphBox> cell = createCell( - par, - applyStyle, - applyParagraphStyle); - nonEmptyCells.add(cell.getNode()); - return cell.beforeReset(() -> nonEmptyCells.remove(cell.getNode())) - .afterUpdateItem(p -> nonEmptyCells.add(cell.getNode())); - }); - getChildren().add(virtualFlow); - - // initialize navigator - IntSupplier cellCount = () -> getParagraphs().size(); - IntUnaryOperator cellLength = i -> virtualFlow.getCell(i).getNode().getLineCount(); - navigator = new TwoLevelNavigator(cellCount, cellLength); - - // relayout the popup when any of its settings values change (besides the caret being dirty) - EventStream popupAlignmentDirty = invalidationsOf(popupAlignmentProperty()); - EventStream popupAnchorAdjustmentDirty = invalidationsOf(popupAnchorAdjustmentProperty()); - EventStream popupAnchorOffsetDirty = invalidationsOf(popupAnchorOffsetProperty()); - EventStream popupDirty = merge(popupAlignmentDirty, popupAnchorAdjustmentDirty, popupAnchorOffsetDirty); - subscribeTo(popupDirty, x -> layoutPopup()); - - // follow the caret every time the caret position or paragraphs change - EventStream caretPosDirty = invalidationsOf(caretPositionProperty()); - EventStream paragraphsDirty = invalidationsOf(getParagraphs()); - EventStream selectionDirty = invalidationsOf(selectionProperty()); - // need to reposition popup even when caret hasn't moved, but selection has changed (been deselected) - EventStream caretDirty = merge(caretPosDirty, paragraphsDirty, selectionDirty); - - // whether or not to display the caret - EventStream blinkCaret = EventStreams.valuesOf(showCaretProperty()) - .flatMap(mode -> { - switch (mode) { - case ON: - return EventStreams.valuesOf(Val.constant(true)); - case OFF: - return EventStreams.valuesOf(Val.constant(false)); - default: - case AUTO: - return EventStreams.valuesOf(focusedProperty() - .and(editableProperty()) - .and(disabledProperty().not())); - } - }); - - // the rate at which to display the caret - EventStream blinkRate = EventStreams.valuesOf(caretBlinkRate); - - // The caret is visible in periodic intervals, - // but only when blinkCaret is true. - caretVisible = EventStreams.combine(blinkCaret, blinkRate) - .flatMap(tuple -> { - Boolean blink = tuple.get1(); - javafx.util.Duration rate = tuple.get2(); - if(blink) { - return rate.lessThanOrEqualTo(ZERO) - ? EventStreams.valuesOf(Val.constant(true)) - : booleanPulse(rate, caretDirty); - } else { - return EventStreams.valuesOf(Val.constant(false)); - } - }) - .toBinding(false); - manageBinding(caretVisible); - - // Adjust popup anchor by either a user-provided function, - // or user-provided offset, or don't adjust at all. - Val> userOffset = Val.map( - popupAnchorOffsetProperty(), - offset -> anchor -> anchor.add(offset)); - _popupAnchorAdjustment = - Val.orElse( - popupAnchorAdjustmentProperty(), - userOffset) - .orElseConst(UnaryOperator.identity()); - - // dispatch MouseOverTextEvents when mouseOverTextDelay is not null - EventStreams.valuesOf(mouseOverTextDelayProperty()) - .flatMap(delay -> delay != null - ? mouseOverTextEvents(nonEmptyCells, delay) - : EventStreams.never()) - .subscribe(evt -> Event.fireEvent(this, evt)); - - behavior = new StyledTextAreaBehavior(this); - } - - - /* ********************************************************************** * - * * - * Queries * - * * - * Queries are parameterized observables. * - * * - * ********************************************************************** */ - - /** - * Returns caret bounds relative to the viewport, i.e. the visual bounds - * of the embedded VirtualFlow. - */ - Optional getCaretBounds() { - return virtualFlow.getCellIfVisible(getCurrentParagraph()) - .map(c -> { - Bounds cellBounds = c.getNode().getCaretBounds(); - return virtualFlow.cellToViewport(c, cellBounds); - }); - } - - /** - * Returns x coordinate of the caret in the current paragraph. - */ - ParagraphBox.CaretOffsetX getCaretOffsetX() { - int idx = getCurrentParagraph(); - return getCell(idx).getCaretOffsetX(); - } - - double getViewportHeight() { - return virtualFlow.getHeight(); - } - - CharacterHit hit(ParagraphBox.CaretOffsetX x, TwoDimensional.Position targetLine) { - int parIdx = targetLine.getMajor(); - ParagraphBox cell = virtualFlow.getCell(parIdx).getNode(); - CharacterHit parHit = cell.hitTextLine(x, targetLine.getMinor()); - return parHit.offset(getParagraphOffset(parIdx)); - } - - CharacterHit hit(ParagraphBox.CaretOffsetX x, double y) { - VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(0.0, y); - if(hit.isBeforeCells()) { - return CharacterHit.insertionAt(0); - } else if(hit.isAfterCells()) { - return CharacterHit.insertionAt(getLength()); - } else { - int parIdx = hit.getCellIndex(); - int parOffset = getParagraphOffset(parIdx); - ParagraphBox cell = hit.getCell().getNode(); - Point2D cellOffset = hit.getCellOffset(); - CharacterHit parHit = cell.hitText(x, cellOffset.getY()); - return parHit.offset(parOffset); - } - } - - /** - * Helpful for determining which letter is at point x, y: - *

-     *     {@code
-     *     StyledTextArea area = // creation code
-     *     area.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent e) -> {
-     *         CharacterHit hit = area.hit(e.getX(), e.getY());
-     *         int characterPosition = hit.getInsertionIndex();
-     *
-     *         // move the caret to that character's position
-     *         area.moveTo(characterPosition, SelectionPolicy.CLEAR);
-     *     }}
-     * 
- */ - public CharacterHit hit(double x, double y) { - VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(x, y); - if(hit.isBeforeCells()) { - return CharacterHit.insertionAt(0); - } else if(hit.isAfterCells()) { - return CharacterHit.insertionAt(getLength()); - } else { - int parIdx = hit.getCellIndex(); - int parOffset = getParagraphOffset(parIdx); - ParagraphBox cell = hit.getCell().getNode(); - Point2D cellOffset = hit.getCellOffset(); - CharacterHit parHit = cell.hit(cellOffset); - return parHit.offset(parOffset); - } - } - - /** - * Returns the current line as a two-level index. - * The major number is the paragraph index, the minor - * number is the line number within the paragraph. - * - *

This method has a side-effect of bringing the current - * paragraph to the viewport if it is not already visible. - */ - TwoDimensional.Position currentLine() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - int lineIdx = cell.getNode().getCurrentLineIndex(); - return _position(parIdx, lineIdx); - } - - TwoDimensional.Position _position(int par, int line) { - return navigator.position(par, line); - } - - @Override - public final String getText(int start, int end) { - return model.getText(start, end); - } - - @Override - public String getText(int paragraph) { - return model.getText(paragraph); - } - - public Paragraph getParagraph(int index) { - return model.getParagraph(index); - } - - @Override - public StyledDocument subDocument(int start, int end) { - return model.subDocument(start, end); - } - - @Override - public StyledDocument subDocument(int paragraphIndex) { - return model.subDocument(paragraphIndex); - } - - /** - * Returns the selection range in the given paragraph. - */ - public IndexRange getParagraphSelection(int paragraph) { - return model.getParagraphSelection(paragraph); + boolean preserveStyle) { + this( + initialParagraphStyle, + applyParagraphStyle, + initialTextStyle, + applyStyle, + new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, StyledText.textOps()), + preserveStyle); } - /** - * Returns the style of the character with the given index. - * If {@code index} points to a line terminator character, - * the last style used in the paragraph terminated by that - * line terminator is returned. - */ - public S getStyleOfChar(int index) { - return model.getStyleOfChar(index); - } - - /** - * Returns the style at the given position. That is the style of the - * character immediately preceding {@code position}, except when - * {@code position} points to a paragraph boundary, in which case it - * is the style at the beginning of the latter paragraph. - * - *

In other words, most of the time {@code getStyleAtPosition(p)} - * is equivalent to {@code getStyleOfChar(p-1)}, except when {@code p} - * points to a paragraph boundary, in which case it is equivalent to - * {@code getStyleOfChar(p)}. - */ - public S getStyleAtPosition(int position) { - return model.getStyleAtPosition(position); - } - - /** - * 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. If {@code position} - * points to a boundary between two paragraphs, then the first styled range - * of the latter paragraph is returned. - */ - public IndexRange getStyleRangeAtPosition(int position) { - return model.getStyleRangeAtPosition(position); - } - - /** - * Returns the styles in the given character range. - */ - public StyleSpans getStyleSpans(int from, int to) { - return model.getStyleSpans(from, to); - } - - /** - * Returns the styles in the given character range. - */ - public StyleSpans getStyleSpans(IndexRange range) { - return getStyleSpans(range.getStart(), range.getEnd()); - } - - /** - * Returns the style of the character with the given index in the given - * paragraph. If {@code index} is beyond the end of the paragraph, the - * style at the end of line is returned. If {@code index} is negative, it - * is the same as if it was 0. - */ - public S getStyleOfChar(int paragraph, int index) { - return model.getStyleOfChar(paragraph, index); - } - - /** - * Returns the style at the given position in the given paragraph. - * This is equivalent to {@code getStyleOfChar(paragraph, position-1)}. - */ - public S getStyleAtPosition(int paragraph, int position) { - return model.getStyleAtPosition(paragraph, position); - } - - /** - * Returns the range of homogeneous style that includes the given position - * in the given paragraph. If {@code position} points to a boundary between - * two styled ranges, then the range preceding {@code position} is returned. - */ - public IndexRange getStyleRangeAtPosition(int paragraph, int position) { - return model.getStyleRangeAtPosition(paragraph, position); - } - - /** - * Returns styles of the whole paragraph. - */ - public StyleSpans getStyleSpans(int paragraph) { - return model.getStyleSpans(paragraph); - } - - /** - * Returns the styles in the given character range of the given paragraph. - */ - public StyleSpans getStyleSpans(int paragraph, int from, int to) { - return model.getStyleSpans(paragraph, from, to); - } - - /** - * Returns the styles in the given character range of the given paragraph. - */ - public StyleSpans getStyleSpans(int paragraph, IndexRange range) { - return getStyleSpans(paragraph, range.getStart(), range.getEnd()); - } - - @Override - public int getAbsolutePosition(int paragraphIndex, int columnIndex) { - return model.getAbsolutePosition(paragraphIndex, columnIndex); - } - - @Override - public Position position(int row, int col) { - return model.position(row, col); - } - - @Override - public Position offsetToPosition(int charOffset, Bias bias) { - return model.offsetToPosition(charOffset, bias); - } - - - /* ********************************************************************** * - * * - * Actions * - * * - * Actions change the state of this control. They typically cause a * - * change of one or more observables and/or produce an event. * - * * - * ********************************************************************** */ - - void scrollBy(Point2D deltas) { - virtualFlow.scrollXBy(deltas.getX()); - virtualFlow.scrollYBy(deltas.getY()); - } - - void show(double y) { - virtualFlow.show(y); - } - - void showCaretAtBottom() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double y = caretBounds.getMaxY(); - virtualFlow.showAtOffset(parIdx, getViewportHeight() - y); - } - - void showCaretAtTop() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double y = caretBounds.getMinY(); - virtualFlow.showAtOffset(parIdx, -y); - } - - /** - * If the caret is not visible within the area's view, the area will scroll so that caret - * is visible in the next layout pass. Use this method when you wish to "follow the caret" - * (i.e. auto-scroll to caret) after making a change (add/remove/modify area's segments). - */ - public void requestFollowCaret() { - followCaretRequested = true; - requestLayout(); - } - - private void followCaret() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double graphicWidth = cell.getNode().getGraphicPrefWidth(); - Bounds region = extendLeft(caretBounds, graphicWidth); - virtualFlow.show(parIdx, region); - } - - /** - * Moves caret to the previous page (i.e. page up) - * @param selectionPolicy use {@link SelectionPolicy#CLEAR} when no selection is desired and - * {@link SelectionPolicy#ADJUST} when a selection from starting point - * to the place to where the caret is moved is desired. - */ - public void prevPage(SelectionPolicy selectionPolicy) { - showCaretAtBottom(); - CharacterHit hit = hit(getTargetCaretOffset(), 1.0); - model.moveTo(hit.getInsertionIndex(), selectionPolicy); - } - - /** - * Moves caret to the next page (i.e. page down) - * @param selectionPolicy use {@link SelectionPolicy#CLEAR} when no selection is desired and - * {@link SelectionPolicy#ADJUST} when a selection from starting point - * to the place to where the caret is moved is desired. - */ - public void nextPage(SelectionPolicy selectionPolicy) { - showCaretAtTop(); - CharacterHit hit = hit(getTargetCaretOffset(), getViewportHeight() - 1.0); - model.moveTo(hit.getInsertionIndex(), selectionPolicy); - } - - /** - * Sets style for the given character range. - */ - public void setStyle(int from, int to, S style) { - model.setStyle(from, to, style); - } - - /** - * Sets style for the whole paragraph. - */ - public void setStyle(int paragraph, S style) { - model.setStyle(paragraph, style); - } - - /** - * Sets style for the given range relative in the given paragraph. - */ - public void setStyle(int paragraph, int from, int to, S style) { - model.setStyle(paragraph, from, to, style); - } - - /** - * Set multiple style ranges at once. This is equivalent to - *

-     * for(StyleSpan{@code } span: styleSpans) {
-     *     setStyle(from, from + span.getLength(), span.getStyle());
-     *     from += span.getLength();
-     * }
-     * 
- * but the actual implementation is more efficient. - */ - public void setStyleSpans(int from, StyleSpans styleSpans) { - model.setStyleSpans(from, styleSpans); - } - - /** - * Set multiple style ranges of a paragraph at once. This is equivalent to - *
-     * for(StyleSpan{@code } span: styleSpans) {
-     *     setStyle(paragraph, from, from + span.getLength(), span.getStyle());
-     *     from += span.getLength();
-     * }
-     * 
- * but the actual implementation is more efficient. - */ - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { - model.setStyleSpans(paragraph, from, styleSpans); - } - - /** - * Sets style for the whole paragraph. - */ - public void setParagraphStyle(int paragraph, PS paragraphStyle) { - model.setParagraphStyle(paragraph, paragraphStyle); - } - - /** - * Resets the style of the given range to the initial style. - */ - public void clearStyle(int from, int to) { - model.clearStyle(from, to); - } - - /** - * Resets the style of the given paragraph to the initial style. - */ - public void clearStyle(int paragraph) { - model.clearStyle(paragraph); - } - - /** - * Resets the style of the given range in the given paragraph - * to the initial style. - */ - public void clearStyle(int paragraph, int from, int to) { - model.clearStyle(paragraph, from, to); - } - - /** - * Resets the style of the given paragraph to the initial style. - */ - public void clearParagraphStyle(int paragraph) { - model.clearParagraphStyle(paragraph); - } - - @Override - public void replaceText(int start, int end, String text) { - model.replaceText(start, end, text); - } - - @Override - public void replace(int start, int end, StyledDocument replacement) { - model.replace(start, end, replacement); - } - - @Override - public void selectRange(int anchor, int caretPosition) { - model.selectRange(anchor, caretPosition); - } - - /** - * {@inheritDoc} - * @deprecated You probably meant to use {@link #moveTo(int)}. This method will be made - * package-private in the future - */ - @Deprecated - @Override - public void positionCaret(int pos) { - model.positionCaret(pos); - } - - /* ********************************************************************** * - * * - * Public API * - * * - * ********************************************************************** */ - - public void dispose() { - subscriptions.unsubscribe(); - model.dispose(); - virtualFlow.dispose(); - } - - /* ********************************************************************** * - * * - * Layout * - * * - * ********************************************************************** */ - - @Override - protected void layoutChildren() { - virtualFlow.resize(getWidth(), getHeight()); - if(followCaretRequested) { - followCaretRequested = false; - followCaret(); - } - - // position popup - layoutPopup(); - } - - /* ********************************************************************** * - * * - * Private methods * - * * - * ********************************************************************** */ - - private Cell, ParagraphBox> createCell( - Paragraph paragraph, - BiConsumer applyStyle, - BiConsumer applyParagraphStyle) { - - ParagraphBox box = new ParagraphBox<>(paragraph, applyParagraphStyle, applyStyle); - - box.highlightFillProperty().bind(highlightFill); - box.highlightTextFillProperty().bind(highlightTextFill); - box.wrapTextProperty().bind(wrapTextProperty()); - box.graphicFactoryProperty().bind(paragraphGraphicFactoryProperty()); - box.graphicOffset.bind(virtualFlow.breadthOffsetProperty()); - - Val hasCaret = Val.combine( - box.indexProperty(), - currentParagraphProperty(), - (bi, cp) -> bi.intValue() == cp.intValue()); - - Subscription hasCaretPseudoClass = hasCaret.values().subscribe(value -> box.pseudoClassStateChanged(HAS_CARET, value)); - Subscription firstParPseudoClass = box.indexProperty().values().subscribe(idx -> box.pseudoClassStateChanged(FIRST_PAR, idx == 0)); - Subscription lastParPseudoClass = EventStreams.combine( - box.indexProperty().values(), - getParagraphs().sizeProperty().values() - ).subscribe(in -> in.exec((i, n) -> box.pseudoClassStateChanged(LAST_PAR, i == n-1))); - - // caret is visible only in the paragraph with the caret - Val cellCaretVisible = hasCaret.flatMap(x -> x ? caretVisible : Val.constant(false)); - box.caretVisibleProperty().bind(cellCaretVisible); - - // bind cell's caret position to area's caret column, - // when the cell is the one with the caret - box.caretPositionProperty().bind(hasCaret.flatMap(has -> has - ? caretColumnProperty() - : Val.constant(0))); - - // keep paragraph selection updated - ObjectBinding cellSelection = Bindings.createObjectBinding(() -> { - int idx = box.getIndex(); - return idx != -1 - ? getParagraphSelection(idx) - : StyledTextArea.EMPTY_RANGE; - }, selectionProperty(), box.indexProperty()); - box.selectionProperty().bind(cellSelection); - - return new Cell, ParagraphBox>() { - @Override - public ParagraphBox getNode() { - return box; - } - - @Override - public void updateIndex(int index) { - box.setIndex(index); - } - - @Override - public void dispose() { - box.highlightFillProperty().unbind(); - box.highlightTextFillProperty().unbind(); - box.wrapTextProperty().unbind(); - box.graphicFactoryProperty().unbind(); - box.graphicOffset.unbind(); - - hasCaretPseudoClass.unsubscribe(); - firstParPseudoClass.unsubscribe(); - lastParPseudoClass.unsubscribe(); - - box.caretVisibleProperty().unbind(); - box.caretPositionProperty().unbind(); - - box.selectionProperty().unbind(); - cellSelection.dispose(); - } - }; - } - - private ParagraphBox getCell(int index) { - return virtualFlow.getCell(index).getNode(); - } - - private EventStream mouseOverTextEvents(ObservableSet> cells, Duration delay) { - return merge(cells, c -> c.stationaryIndices(delay).map(e -> e.unify( - l -> l.map((pos, charIdx) -> MouseOverTextEvent.beginAt(c.localToScreen(pos), getParagraphOffset(c.getIndex()) + charIdx)), - r -> MouseOverTextEvent.end()))); - } - - private int getParagraphOffset(int parIdx) { - return position(parIdx, 0).toOffset(); - } - - private void layoutPopup() { - PopupWindow popup = getPopupWindow(); - PopupAlignment alignment = getPopupAlignment(); - UnaryOperator adjustment = _popupAnchorAdjustment.getValue(); - if(popup != null) { - positionPopup(popup, alignment, adjustment); - } - } - - private void positionPopup( - PopupWindow popup, - PopupAlignment alignment, - UnaryOperator adjustment) { - Optional bounds = null; - switch(alignment.getAnchorObject()) { - case CARET: bounds = getCaretBoundsOnScreen(); break; - case SELECTION: bounds = getSelectionBoundsOnScreen(); break; - } - bounds.ifPresent(b -> { - double x = 0, y = 0; - switch(alignment.getHorizontalAlignment()) { - case LEFT: x = b.getMinX(); break; - case H_CENTER: x = (b.getMinX() + b.getMaxX()) / 2; break; - case RIGHT: x = b.getMaxX(); break; - } - switch(alignment.getVerticalAlignment()) { - case TOP: y = b.getMinY(); - case V_CENTER: y = (b.getMinY() + b.getMaxY()) / 2; break; - case BOTTOM: y = b.getMaxY(); break; - } - Point2D anchor = adjustment.apply(new Point2D(x, y)); - popup.setAnchorX(anchor.getX()); - popup.setAnchorY(anchor.getY()); - }); - } - - private Optional getCaretBoundsOnScreen() { - return virtualFlow.getCellIfVisible(getCurrentParagraph()) - .map(c -> c.getNode().getCaretBoundsOnScreen()); - } - - private Optional getSelectionBoundsOnScreen() { - IndexRange selection = getSelection(); - if(selection.getLength() == 0) { - return getCaretBoundsOnScreen(); - } - - Bounds[] bounds = virtualFlow.visibleCells().stream() - .map(c -> c.getNode().getSelectionBoundsOnScreen()) - .filter(Optional::isPresent) - .map(Optional::get) - .toArray(Bounds[]::new); - - if(bounds.length == 0) { - return Optional.empty(); - } - double minX = Stream.of(bounds).mapToDouble(Bounds::getMinX).min().getAsDouble(); - double maxX = Stream.of(bounds).mapToDouble(Bounds::getMaxX).max().getAsDouble(); - double minY = Stream.of(bounds).mapToDouble(Bounds::getMinY).min().getAsDouble(); - double maxY = Stream.of(bounds).mapToDouble(Bounds::getMaxY).max().getAsDouble(); - return Optional.of(new BoundingBox(minX, minY, maxX-minX, maxY-minY)); - } - - private void subscribeTo(EventStream src, Consumer cOnsumer) { - manageSubscription(src.subscribe(cOnsumer)); - } - - private void manageSubscription(Subscription subscription) { - subscriptions = subscriptions.and(subscription); - } - - private void manageBinding(Binding binding) { - subscriptions = subscriptions.and(binding::dispose); - } - - private static Bounds extendLeft(Bounds b, double w) { - if(w == 0) { - return b; - } else { - return new BoundingBox( - b.getMinX() - w, b.getMinY(), - b.getWidth() + w, b.getHeight()); - } + public StyledTextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, + S initialTextStyle, BiConsumer applyStyle) { + this(initialParagraphStyle, applyParagraphStyle, + initialTextStyle, applyStyle, true); } - void clearTargetCaretOffset() { - targetCaretOffset = Optional.empty(); - } + public static Node createStyledTextNode(StyledText seg, SegmentOps, S> segOps, + BiConsumer applyStyle) { - ParagraphBox.CaretOffsetX getTargetCaretOffset() { - if(!targetCaretOffset.isPresent()) - targetCaretOffset = Optional.of(getCaretOffsetX()); - return targetCaretOffset.get(); - } + TextExt t = new TextExt(segOps.getText(seg)); + t.setTextOrigin(VPos.TOP); + t.getStyleClass().add("text"); + applyStyle.accept(t, segOps.getStyle(seg)); - private static EventStream booleanPulse(javafx.util.Duration javafxDuration, EventStream restartImpulse) { - Duration duration = Duration.ofMillis(Math.round(javafxDuration.toMillis())); - EventStream ticks = EventStreams.restartableTicks(duration, restartImpulse); - return StateMachine.init(false) - .on(restartImpulse.withDefaultEvent(null)).transition((state, impulse) -> true) - .on(ticks).transition((state, tick) -> !state) - .toStateStream(); + // XXX: binding selectionFill to textFill, + // see the note at highlightTextFill + t.impl_selectionFillProperty().bind(t.fillProperty()); + return t; } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java index 1144a6a92..73fc542bf 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java @@ -1,16 +1,13 @@ package org.fxmisc.richtext; -import static java.lang.Character.isWhitespace; +import static java.lang.Character.*; import static javafx.scene.input.KeyCode.*; import static javafx.scene.input.KeyCombination.*; import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; import static org.fxmisc.wellbehaved.event.EventPattern.*; -import static org.fxmisc.wellbehaved.event.template.InputMapTemplate.consume; -import static org.fxmisc.wellbehaved.event.template.InputMapTemplate.sequence; -import static org.fxmisc.wellbehaved.event.template.InputMapTemplate.when; +import static org.fxmisc.wellbehaved.event.template.InputMapTemplate.*; import static org.reactfx.EventStreams.*; -import java.util.Optional; import java.util.function.Predicate; import javafx.event.Event; @@ -21,19 +18,17 @@ import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; -import org.fxmisc.richtext.model.StyledTextAreaModel; import org.fxmisc.richtext.model.NavigationActions.SelectionPolicy; +import org.fxmisc.richtext.model.StyledTextAreaModel; import org.fxmisc.richtext.model.TwoDimensional.Position; -import org.fxmisc.richtext.ParagraphBox.CaretOffsetX; import org.fxmisc.wellbehaved.event.EventPattern; import org.fxmisc.wellbehaved.event.template.InputMapTemplate; import org.reactfx.EventStream; -import org.reactfx.Subscription; import org.reactfx.value.Val; import org.reactfx.value.Var; /** - * Controller for StyledTextArea. + * Controller for GenericStyledArea. */ class StyledTextAreaBehavior { @@ -209,9 +204,9 @@ private enum DragState { * Fields * * ********************************************************************** */ - private final StyledTextArea view; + private final GenericStyledArea view; - private final StyledTextAreaModel model; + private final StyledTextAreaModel model; /** * Indicates whether selection is being dragged by the user. @@ -224,7 +219,7 @@ private enum DragState { * Constructors * * ********************************************************************** */ - StyledTextAreaBehavior(StyledTextArea area) { + StyledTextAreaBehavior(GenericStyledArea area) { this.view = area; this.model = area.getModel(); 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 d9df0d249..48730f031 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Codec.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Codec.java @@ -9,6 +9,8 @@ import javafx.scene.paint.Color; +import org.reactfx.util.Either; + public interface Codec { String getName(); @@ -118,4 +120,33 @@ public E decode(DataInputStream is) throws IOException { }; } + + static Codec> eitherCodec(Codec lCodec, Codec rCodec) { + return new Codec>() { + + @Override + public String getName() { + return "either<" + lCodec.getName() + ", " + rCodec.getName() + ">"; + } + + @Override + public void encode(DataOutputStream os, Either e) throws IOException { + if(e.isLeft()) { + os.writeBoolean(false); + lCodec.encode(os, e.getLeft()); + } else { + os.writeBoolean(true); + rCodec.encode(os, e.getRight()); + } + } + + @Override + public Either decode(DataInputStream is) throws IOException { + boolean isRight = is.readBoolean(); + return isRight + ? Either.right(rCodec.decode(is)) + : Either.left (lCodec.decode(is)); + } + }; + } } \ No newline at end of file diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditActions.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditActions.java index 9162c5dd3..b00766e1a 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditActions.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditActions.java @@ -5,7 +5,7 @@ /** * Extended edit actions for {@link TextEditingArea}. */ -public interface EditActions extends TextEditingArea { +public interface EditActions extends TextEditingArea { /** * Appends the given text to the end of the text content. @@ -17,7 +17,7 @@ default void appendText(String text) { /** * Appends the given rich-text content to the end of this text-editing area. */ - default void append(StyledDocument document) { + default void append(StyledDocument document) { insert(getLength(), document); } @@ -51,7 +51,7 @@ default void insertText(int paragraphIndex, int columnIndex, String text) { * @param index The location to insert the text. * @param document The rich-text content to insert. */ - default void insert(int index, StyledDocument document) { + default void insert(int index, StyledDocument document) { replace(index, index, document); } @@ -64,7 +64,7 @@ default void insert(int index, StyledDocument document) { * * @param document The rich-text content to insert. */ - default void insert(int paragraphIndex, int columnIndex, StyledDocument document) { + default void insert(int paragraphIndex, int columnIndex, StyledDocument document) { int index = getAbsolutePosition(paragraphIndex, columnIndex); replace(index, index, document); } @@ -149,7 +149,7 @@ default void replaceText(String replacement) { /** * Replaces the entire content with the given rich-text content. */ - default void replace(StyledDocument replacement) { + default void replace(StyledDocument replacement) { replace(0, getLength(), replacement); } @@ -169,7 +169,7 @@ default void replaceSelection(String replacement) { * caret position. If there was a selection, then the selection is cleared * and the given replacement text is inserted. */ - default void replaceSelection(StyledDocument replacement) { + default void replaceSelection(StyledDocument replacement) { replace(getSelection(), replacement); } @@ -180,7 +180,7 @@ default void moveSelectedText(int pos) { // no move, just position the caret selectRange(pos, pos); } else { - StyledDocument text = this.subDocument(sel.getStart(), sel.getEnd()); + StyledDocument text = this.subDocument(sel.getStart(), sel.getEnd()); if(pos > sel.getEnd()) pos -= sel.getLength(); deleteText(sel); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java index e478242eb..cf62aeb73 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java @@ -8,11 +8,11 @@ import org.reactfx.value.Val; /** - * Content model for {@link org.fxmisc.richtext.StyledTextArea}. Implements edit operations + * Content model for {@link org.fxmisc.richtext.GenericRichtextArea}. Implements edit operations * on styled text, but not worrying about additional aspects such as * caret or selection, which are handled by {@link StyledTextAreaModel}. */ -public interface EditableStyledDocument extends StyledDocument { +public interface EditableStyledDocument extends StyledDocument { /* ********************************************************************** * * * @@ -30,12 +30,12 @@ public interface EditableStyledDocument extends StyledDocument { Val lengthProperty(); @Override - ObservableList> getParagraphs(); + ObservableList> getParagraphs(); /** * Read-only snapshot of the current state of this document. */ - ReadOnlyStyledDocument snapshot(); + ReadOnlyStyledDocument snapshot(); /* ********************************************************************** * * * @@ -50,7 +50,7 @@ default EventStream plainChanges() { .filter(pc -> !pc.removed.equals(pc.inserted)); } - EventStream> richChanges(); + EventStream> richChanges(); SuspendableNo beingUpdatedProperty(); boolean isBeingUpdated(); @@ -64,7 +64,7 @@ default EventStream plainChanges() { * * * ********************************************************************** */ - void replace(int start, int end, StyledDocument replacement); + void replace(int start, int end, StyledDocument replacement); void setStyle(int from, int to, S style); @@ -74,7 +74,7 @@ default EventStream plainChanges() { void setStyleSpans(int from, StyleSpans styleSpens); - void setStyleSpans(int paragraph, int from, StyleSpans styleSpens); + void setStyleSpans(int paragraph, int from, StyleSpans styleSpens); void setParagraphStyle(int parIdx, PS style); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/NavigationActions.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/NavigationActions.java index 8ce05cee9..558899c2f 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/NavigationActions.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/NavigationActions.java @@ -7,7 +7,7 @@ /** * Navigation actions for {@link TextEditingArea}. */ -public interface NavigationActions extends TextEditingArea { +public interface NavigationActions extends TextEditingArea { /** * Indicates how to treat selection when caret is moved. 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 acca8bb16..989f4bd0c 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java @@ -7,12 +7,13 @@ import java.util.Iterator; import java.util.List; import java.util.Objects; +import java.util.Optional; import javafx.scene.control.IndexRange; import org.fxmisc.richtext.model.TwoDimensional.Position; -public final class Paragraph { +public final class Paragraph { @SafeVarargs private static List list(T head, T... tail) { @@ -26,28 +27,28 @@ private static List list(T head, T... tail) { } } - private final List> segments; + private final List segments; private final TwoLevelNavigator navigator; private final PS paragraphStyle; - public Paragraph(PS paragraphStyle, String text, S style) { - this(paragraphStyle, new StyledText<>(text, style)); - } + private final SegmentOps segmentOps; @SafeVarargs - public Paragraph(PS paragraphStyle, Segment text, Segment... texts) { - this(paragraphStyle, list(text, texts)); + public Paragraph(PS paragraphStyle, SegmentOps segmentOps, SEG text, SEG... texts) { + this(paragraphStyle, segmentOps, list(text, texts)); } - Paragraph(PS paragraphStyle, List> segments) { + Paragraph(PS paragraphStyle, SegmentOps segmentOps, List segments) { assert !segments.isEmpty(); + + this.segmentOps = segmentOps; this.segments = segments; this.paragraphStyle = paragraphStyle; navigator = new TwoLevelNavigator(segments::size, - i -> segments.get(i).length()); + i -> segmentOps.length(segments.get(i))); } - public List> getSegments() { + public List getSegments() { return Collections.unmodifiableList(segments); } @@ -58,14 +59,14 @@ public PS getParagraphStyle() { private int length = -1; public int length() { if(length == -1) { - length = segments.stream().mapToInt(Segment::length).sum(); + length = segments.stream().mapToInt(segmentOps::length).sum(); } return length; } public char charAt(int index) { Position pos = navigator.offsetToPosition(index, Forward); - return segments.get(pos.getMajor()).charAt(pos.getMinor()); + return segmentOps.charAt(segments.get(pos.getMajor()), pos.getMinor()); } public String substring(int from, int to) { @@ -82,7 +83,7 @@ public String substring(int from) { * unless this paragraph is empty and {@code p} is non-empty, in which * case the paragraph style of the result will be that of {@code p}. */ - public Paragraph concat(Paragraph p) { + public Paragraph concat(Paragraph p) { if(p.length() == 0) { return this; } @@ -91,20 +92,21 @@ public Paragraph concat(Paragraph p) { return p; } - Segment left = segments.get(segments.size() - 1); - Segment right = p.segments.get(0); - if(left.canJoin(right)) { - Segment segment = left.append(right.getText()); - List> segs = new ArrayList<>(segments.size() + p.segments.size() - 1); + SEG left = segments.get(segments.size() - 1); + SEG right = p.segments.get(0); + Optional joined = segmentOps.join(left, right); + 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, segs); + return new Paragraph<>(paragraphStyle, segmentOps, segs); } else { - List> segs = new ArrayList<>(segments.size() + p.segments.size()); + List segs = new ArrayList<>(segments.size() + p.segments.size()); segs.addAll(segments); segs.addAll(p.segments); - return new Paragraph<>(paragraphStyle, segs); + return new Paragraph<>(paragraphStyle, segmentOps, segs); } } @@ -112,56 +114,30 @@ public Paragraph concat(Paragraph p) { * Similar to {@link #concat(Paragraph)}, except in case both paragraphs * are empty, the result's paragraph style will be that of the argument. */ - Paragraph concatR(Paragraph that) { + Paragraph concatR(Paragraph that) { return this.length() == 0 && that.length() == 0 ? that : concat(that); } - public Paragraph append(String str) { - if(str.length() == 0) { - return this; - } - - List> segs = new ArrayList<>(segments); - int lastIdx = segments.size() - 1; - segs.set(lastIdx, segments.get(lastIdx).append(str)); - return new Paragraph<>(paragraphStyle, 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(); - Segment seg = segments.get(segIdx); - Segment replacement = seg.spliced(segPos, segPos, str); - List> segs = new ArrayList<>(segments); - segs.set(segIdx, replacement); - return new Paragraph<>(paragraphStyle, segs); - } - - public Paragraph subSequence(int start, int end) { + public Paragraph subSequence(int start, int end) { return trim(end).subSequence(start); } - public Paragraph trim(int length) { + 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); + List segs = new ArrayList<>(segIdx + 1); segs.addAll(segments.subList(0, segIdx)); - segs.add(segments.get(segIdx).subSequence(0, pos.getMinor())); - return new Paragraph<>(paragraphStyle, segs); + segs.add(segmentOps.subSequence(segments.get(segIdx), 0, pos.getMinor())); + return new Paragraph<>(paragraphStyle, segmentOps, segs); } } - public Paragraph subSequence(int start) { + public Paragraph subSequence(int start) { if(start < 0) { throw new IllegalArgumentException("start must not be negative (was: " + start + ")"); } else if(start == 0) { @@ -169,91 +145,73 @@ public Paragraph subSequence(int start) { } 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())); + List segs = new ArrayList<>(segments.size() - segIdx); + segs.add(segmentOps.subSequence(segments.get(segIdx), pos.getMinor())); segs.addAll(segments.subList(segIdx + 1, segments.size())); - return new Paragraph<>(paragraphStyle, segs); + return new Paragraph<>(paragraphStyle, segmentOps, segs); } else { throw new IndexOutOfBoundsException(start + " not in [0, " + length() + "]"); } } - public Paragraph delete(int start, int end) { + public Paragraph delete(int start, int end) { return trim(start).concat(subSequence(end)); } - public Paragraph restyle(S style) { - return new Paragraph<>(paragraphStyle, getText(), style); + 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); } - public Paragraph restyle(int from, int to, S 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<>(paragraphStyle, substring(from, to), style); - Paragraph right = subSequence(to); + Paragraph left = subSequence(0, from); + Paragraph middle = subSequence(from, to).restyle(style); + Paragraph right = subSequence(to); return left.concat(middle).concat(right); } } - public Paragraph restyle(int from, StyleSpans styleSpans) { + public Paragraph restyle(int from, StyleSpans styleSpans) { int len = styleSpans.length(); if(styleSpans.equals(getStyleSpans(from, from + len))) { return this; } - // Step 1: Restyle all segments according to the corresponding style span - // - // Assumptions: - // * borders of segments are aligned to borders of spans - // * One style span can span multiple segments - // * One segment can (obviously) not span multiple StyleSpans - List> intermediateSegs = new ArrayList<>(segments.size()); - int segOff = 0; - Iterator> spans = styleSpans.iterator(); - StyleSpan span = spans.next(); - int spanEnd = span.getLength(); - for (Segment seg : segments) { - seg.setStyle(span.getStyle()); - - intermediateSegs.add(seg); - - segOff = segOff + seg.length(); - if (segOff >= spanEnd && spans.hasNext()) { - span = (StyleSpan) spans.next(); - spanEnd += span.getLength(); - } - } - - // Step 2: Join segments with the same style - // - // Assumptions: - // * There is at least one segment available - List> middleSegs = new ArrayList<>(intermediateSegs.size()); - Iterator> segs = intermediateSegs.iterator(); - Segment currentSeg = (Segment) segs.next(); - while(segs.hasNext()) { - Segment nextSeg = (Segment) segs.next(); - if (currentSeg.canJoin(nextSeg)) { - currentSeg = currentSeg.append(nextSeg.getText()); - } else { - middleSegs.add(currentSeg); - currentSeg = nextSeg; - } + 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; } - middleSegs.add(currentSeg); - - Paragraph middle = new Paragraph<>(paragraphStyle, middleSegs); + Paragraph newMiddle = new Paragraph<>(paragraphStyle, segmentOps, middleSegs); - Paragraph left = trim(from); - Paragraph right = subSequence(from + len); - return left.concat(middle).concat(right); + return left.concat(newMiddle).concat(right); } - public Paragraph setParagraphStyle(PS paragraphStyle) { - return new Paragraph<>(paragraphStyle, segments); + public Paragraph setParagraphStyle(PS paragraphStyle) { + return new Paragraph<>(paragraphStyle, segmentOps, segments); } /** @@ -263,11 +221,11 @@ public Paragraph setParagraphStyle(PS paragraphStyle) { */ public S getStyleOfChar(int charIdx) { if(charIdx < 0) { - return segments.get(0).getStyle(); + return segmentOps.getStyle(segments.get(0)); } Position pos = navigator.offsetToPosition(charIdx, Forward); - return segments.get(pos.getMajor()).getStyle(); + return segmentOps.getStyle(segments.get(pos.getMajor())); } /** @@ -288,7 +246,7 @@ public S getStyleAtPosition(int position) { } Position pos = navigator.offsetToPosition(position, Backward); - return segments.get(pos.getMajor()).getStyle(); + return segmentOps.getStyle(segments.get(pos.getMajor())); } /** @@ -299,14 +257,15 @@ public S getStyleAtPosition(int position) { public IndexRange getStyleRangeAtPosition(int position) { Position pos = navigator.offsetToPosition(position, Backward); int start = position - pos.getMinor(); - int end = start + segments.get(pos.getMajor()).length(); + int end = start + segmentOps.length(segments.get(pos.getMajor())); return new IndexRange(start, end); } public StyleSpans getStyleSpans() { StyleSpansBuilder builder = new StyleSpansBuilder<>(segments.size()); - for(Segment seg: segments) { - builder.add(seg.getStyle(), seg.length()); + for(SEG seg: segments) { + builder.add(segmentOps.getStyle(seg), + segmentOps.length(seg)); } return builder.create(); } @@ -323,19 +282,20 @@ public StyleSpans getStyleSpans(int from, int to) { StyleSpansBuilder builder = new StyleSpansBuilder<>(n); if(startSegIdx == endSegIdx) { - Segment seg = segments.get(startSegIdx); - builder.add(seg.getStyle(), to - from); + SEG seg = segments.get(startSegIdx); + builder.add(segmentOps.getStyle(seg), to - from); } else { - Segment startSeg = segments.get(startSegIdx); - builder.add(startSeg.getStyle(), startSeg.length() - start.getMinor()); + SEG startSeg = segments.get(startSegIdx); + builder.add(segmentOps.getStyle(startSeg), segmentOps.length(startSeg) - start.getMinor()); for(int i = startSegIdx + 1; i < endSegIdx; ++i) { - Segment seg = segments.get(i); - builder.add(seg.getStyle(), seg.length()); + SEG seg = segments.get(i); + builder.add(segmentOps.getStyle(seg), + segmentOps.length(seg)); } - Segment endSeg = segments.get(endSegIdx); - builder.add(endSeg.getStyle(), end.getMinor()); + SEG endSeg = segments.get(endSegIdx); + builder.add(segmentOps.getStyle(endSeg), end.getMinor()); } return builder.create(); @@ -349,8 +309,8 @@ public StyleSpans getStyleSpans(int from, int to) { public String getText() { if(text == null) { StringBuilder sb = new StringBuilder(length()); - for(Segment seg: segments) - sb.append(seg.getText()); + for(SEG seg: segments) + sb.append(segmentOps.getText(seg)); text = sb.toString(); } return text; @@ -360,7 +320,7 @@ public String getText() { public String toString() { return "Par[" + paragraphStyle + "; " + - segments.stream().map(Segment::toString) + segments.stream().map(Object::toString) .reduce((s1, s2) -> s1 + "," + s2).orElse("") + "]"; } @@ -368,7 +328,7 @@ public String toString() { @Override public boolean equals(Object other) { if(other instanceof Paragraph) { - Paragraph that = (Paragraph) other; + Paragraph that = (Paragraph) other; return Objects.equals(this.paragraphStyle, that.paragraphStyle) && Objects.equals(this.segments, that.segments); } else { 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 562b52bd6..e281a77d5 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java @@ -25,7 +25,7 @@ import org.reactfx.util.Tuple2; import org.reactfx.util.Tuple3; -public final class ReadOnlyStyledDocument implements StyledDocument { +public final class ReadOnlyStyledDocument implements StyledDocument { private static class Summary { private final int paragraphCount; @@ -44,11 +44,11 @@ public int length() { } } - private static ToSemigroup, Summary> summaryProvider() { - return new ToSemigroup, Summary>() { + private static ToSemigroup, Summary> summaryProvider() { + return new ToSemigroup, Summary>() { @Override - public Summary apply(Paragraph p) { + public Summary apply(Paragraph p) { return new Summary(1, p.length()); } @@ -67,143 +67,108 @@ public Summary reduce(Summary left, Summary right) { private static final BiFunction> NAVIGATE = (s, i) -> i <= s.length() ? left(i) : right(i - (s.length() + 1)); - public static ReadOnlyStyledDocument fromString(String str, PS paragraphStyle, S style) { + public static ReadOnlyStyledDocument fromString(String str, PS paragraphStyle, S style, TextOps segmentOps) { Matcher m = LINE_TERMINATOR.matcher(str); int n = 1; while(m.find()) ++n; - List> res = new ArrayList<>(n); + List> res = new ArrayList<>(n); int start = 0; m.reset(); while(m.find()) { String s = str.substring(start, m.start()); - res.add(new Paragraph<>(paragraphStyle, 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, last, style)); + res.add(new Paragraph<>(paragraphStyle, segmentOps, segmentOps.create(last, style))); - return new ReadOnlyStyledDocument<>(res); + return new ReadOnlyStyledDocument<>(res, segmentOps); } - public static ReadOnlyStyledDocument from(StyledDocument doc) { + 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); + return new ReadOnlyStyledDocument<>(res, segmentOps); + } + + public static ReadOnlyStyledDocument from(StyledDocument doc) { if(doc instanceof ReadOnlyStyledDocument) { - return (ReadOnlyStyledDocument) doc; + return (ReadOnlyStyledDocument) doc; } else { - return new ReadOnlyStyledDocument<>(doc.getParagraphs()); + return new ReadOnlyStyledDocument<>(doc.getParagraphs(), doc.getSegOps()); } } - /** - * Creates a new ReadOnlyStyledDocument with one Segment. - * The resulting ReadOnlyStyledDocument can be inserted or appended to a StyledTextArea. - * - * @param seg The segment which shall be contained in the document. - * @param paragraphStyle The paragraph style to use for the paragraph which contains the segment. - * - * @return A ReadOnlyStyledDocument with the given segment. - */ - public static ReadOnlyStyledDocument from(Segment seg, PS paragraphStyle) { - List> res = new ArrayList<>(1); - Paragraph content = new Paragraph(paragraphStyle, Arrays.asList(seg)); - res.add(content); - return new ReadOnlyStyledDocument<>(res); - } - public static Codec> codec(Codec pCodec, Codec tCodec) { - return new Codec>() { - private final Codec>> codec = Codec.listCodec(paragraphCodec(pCodec, tCodec)); + public static Codec> codec(Codec pCodec, Codec segCodec, SegmentOps segmentOps) { + return new Codec>() { + private final Codec>> codec = Codec.listCodec(paragraphCodec(pCodec, segCodec, segmentOps)); @Override public String getName() { - return "application/richtextfx-styled-document<" + tCodec.getName() + ";" + pCodec.getName() + ">"; + return "application/richtextfx-styled-document<" + pCodec.getName() + ";" + segCodec.getName() + ">"; } @Override - public void encode(DataOutputStream os, StyledDocument doc) throws IOException { + public void encode(DataOutputStream os, StyledDocument doc) throws IOException { codec.encode(os, doc.getParagraphs()); } @Override - public StyledDocument decode(DataInputStream is) throws IOException { - return new ReadOnlyStyledDocument<>(codec.decode(is)); + public StyledDocument decode(DataInputStream is) throws IOException { + return new ReadOnlyStyledDocument<>(codec.decode(is), segmentOps); } }; } - private static Codec> paragraphCodec(Codec pCodec, Codec tCodec) { - return new Codec>() { - private final Codec>> segmentsCodec = Codec.listCodec(styledTextCodec(tCodec)); + private static Codec> paragraphCodec(Codec pCodec, Codec segCodec, SegmentOps segmentOps) { + return new Codec>() { + private final Codec> segmentsCodec = Codec.listCodec(segCodec); @Override public String getName() { - return "paragraph<" + tCodec.getName() + ";" + pCodec.getName() + ">"; + return "paragraph<" + pCodec.getName() + ";" + segCodec.getName() + ">"; } @Override - public void encode(DataOutputStream os, Paragraph p) throws IOException { + public void encode(DataOutputStream os, Paragraph p) throws IOException { pCodec.encode(os, p.getParagraphStyle()); segmentsCodec.encode(os, p.getSegments()); } @Override - public Paragraph decode(DataInputStream is) throws IOException { + public Paragraph decode(DataInputStream is) throws IOException { PS paragraphStyle = pCodec.decode(is); - List> segments = segmentsCodec.decode(is); - return new Paragraph<>(paragraphStyle, segments); + List segments = segmentsCodec.decode(is); + return new Paragraph<>(paragraphStyle, segmentOps, segments); } }; } - private static Codec> styledTextCodec(Codec styleCodec) { - return new Codec>() { - - @Override - public String getName() { - return "styledtext<" + styleCodec.getName() + ">"; - } - @Override - public void encode(DataOutputStream os, Segment t) throws IOException { - // encode the segment type and content - STRING_CODEC.encode(os, t.getClass().getName()); - t.encode(os, styleCodec); - } - - @Override - public Segment decode(DataInputStream is) throws IOException { - String segmentType = is.readUTF(); - try { - @SuppressWarnings("unchecked") - Class> clazz = (Class>) Class.forName(segmentType); - Segment result = (Segment) clazz.newInstance(); - result.decode(is, styleCodec); - return result; - } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { - throw new IOException("Could not create Segment for " + segmentType, e); - } - } - - }; - } - - - private final NonEmptyFingerTree, Summary> tree; + private final NonEmptyFingerTree, Summary> tree; private String text = null; - private List> paragraphs = null; + private List> paragraphs = null; - private ReadOnlyStyledDocument(NonEmptyFingerTree, Summary> tree) { + private final SegmentOps segmentOps; + @Override + public final SegmentOps getSegOps() { return segmentOps; } + + private ReadOnlyStyledDocument(NonEmptyFingerTree, Summary> tree, SegmentOps segmentOps) { this.tree = tree; + this.segmentOps = segmentOps; } - ReadOnlyStyledDocument(List> paragraphs) { + ReadOnlyStyledDocument(List> paragraphs, SegmentOps segmentOps) { this.tree = FingerTree.mkTree(paragraphs, summaryProvider()).caseEmpty().unify( emptyTree -> { throw new AssertionError("Unreachable code"); }, neTree -> neTree); + this.segmentOps = segmentOps; } @Override @@ -226,12 +191,12 @@ public int getParagraphCount() { return tree.getLeafCount(); } - public Paragraph getParagraph(int index) { + public Paragraph getParagraph(int index) { return tree.getLeaf(index); } @Override - public List> getParagraphs() { + public List> getParagraphs() { if(paragraphs == null) { paragraphs = tree.asList(); } @@ -248,94 +213,99 @@ public Position offsetToPosition(int offset, Bias bias) { return position(0, 0).offsetBy(offset, bias); } - public Tuple2, ReadOnlyStyledDocument> split(int position) { + public Tuple2, ReadOnlyStyledDocument> split(int position) { return tree.locate(NAVIGATE, position).map(this::split); } - public Tuple2, ReadOnlyStyledDocument> split( + public Tuple2, ReadOnlyStyledDocument> split( int row, int col) { return tree.splitAt(row).map((l, p, r) -> { - Paragraph p1 = p.trim(col); - Paragraph p2 = p.subSequence(col); - ReadOnlyStyledDocument doc1 = new ReadOnlyStyledDocument<>(l.append(p1)); - ReadOnlyStyledDocument doc2 = new ReadOnlyStyledDocument<>(r.prepend(p2)); + Paragraph p1 = p.trim(col); + Paragraph p2 = p.subSequence(col); + ReadOnlyStyledDocument doc1 = new ReadOnlyStyledDocument<>(l.append(p1), segmentOps); + ReadOnlyStyledDocument doc2 = new ReadOnlyStyledDocument<>(r.prepend(p2), segmentOps); return t(doc1, doc2); }); } @Override - public ReadOnlyStyledDocument concat(StyledDocument other) { + public ReadOnlyStyledDocument concat(StyledDocument other) { return concat0(other, Paragraph::concat); } - private ReadOnlyStyledDocument concatR(StyledDocument other) { + private ReadOnlyStyledDocument concatR(StyledDocument other) { return concat0(other, Paragraph::concatR); } - private ReadOnlyStyledDocument concat0(StyledDocument other, BinaryOperator> parConcat) { + private ReadOnlyStyledDocument concat0(StyledDocument other, BinaryOperator> parConcat) { int n = tree.getLeafCount() - 1; - Paragraph p0 = tree.getLeaf(n); - Paragraph p1 = other.getParagraphs().get(0); - Paragraph p = parConcat.apply(p0, p1); - NonEmptyFingerTree, Summary> tree1 = tree.updateLeaf(n, p); - FingerTree, Summary> tree2 = (other instanceof ReadOnlyStyledDocument) - ? ((ReadOnlyStyledDocument) other).tree.split(1)._2 + Paragraph p0 = tree.getLeaf(n); + Paragraph p1 = other.getParagraphs().get(0); + Paragraph p = parConcat.apply(p0, p1); + NonEmptyFingerTree, Summary> tree1 = tree.updateLeaf(n, p); + FingerTree, Summary> tree2 = (other instanceof ReadOnlyStyledDocument) + ? ((ReadOnlyStyledDocument) other).tree.split(1)._2 : FingerTree.mkTree(other.getParagraphs().subList(1, other.getParagraphs().size()), summaryProvider()); - return new ReadOnlyStyledDocument<>(tree1.join(tree2)); + return new ReadOnlyStyledDocument<>(tree1.join(tree2), segmentOps); } @Override - public StyledDocument subSequence(int start, int end) { + public StyledDocument subSequence(int start, int end) { return split(end)._1.split(start)._2; } - public Tuple3, RichTextChange, MaterializedListModification>> replace( - int from, int to, ReadOnlyStyledDocument replacement) { + @Override + public StyledDocument subDocument(int paragraphIndex) { + return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex)), segmentOps); + } + + public Tuple3, RichTextChange, MaterializedListModification>> replace( + int from, int to, ReadOnlyStyledDocument replacement) { return replace(from, to, x -> replacement); } - Tuple3, RichTextChange, MaterializedListModification>> replace( - int from, int to, UnaryOperator> f) { + Tuple3, RichTextChange, MaterializedListModification>> replace( + int from, int to, UnaryOperator> f) { BiIndex start = tree.locate(NAVIGATE, from); BiIndex end = tree.locate(NAVIGATE, to); return replace(start, end, f); } - Tuple3, RichTextChange, MaterializedListModification>> replace( - BiIndex start, BiIndex end, UnaryOperator> f) { + Tuple3, RichTextChange, MaterializedListModification>> replace( + BiIndex start, BiIndex end, UnaryOperator> f) { int pos = tree.getSummaryBetween(0, start.major).map(s -> s.length() + 1).orElse(0) + start.minor; - List> removedPars = + List> removedPars = getParagraphs().subList(start.major, end.major + 1); return end.map(this::split).map((l0, r) -> { return start.map(l0::split).map((l, removed) -> { - ReadOnlyStyledDocument replacement = f.apply(removed); - ReadOnlyStyledDocument doc = l.concatR(replacement).concat(r); - RichTextChange change = new RichTextChange<>(pos, removed, replacement); - List> addedPars = doc.getParagraphs().subList(start.major, start.major + replacement.getParagraphCount()); - MaterializedListModification> parChange = + ReadOnlyStyledDocument replacement = f.apply(removed); + ReadOnlyStyledDocument doc = l.concatR(replacement).concat(r); + RichTextChange change = new RichTextChange<>(pos, removed, replacement); + List> addedPars = doc.getParagraphs().subList(start.major, start.major + replacement.getParagraphCount()); + MaterializedListModification> parChange = MaterializedListModification.create(start.major, removedPars, addedPars); return t(doc, change, parChange); }); }); } - Tuple3, RichTextChange, MaterializedListModification>> replaceParagraph( - int parIdx, UnaryOperator> f) { + Tuple3, RichTextChange, MaterializedListModification>> replaceParagraph( + int parIdx, UnaryOperator> f) { return replace( new BiIndex(parIdx, 0), new BiIndex(parIdx, tree.getLeaf(parIdx).length()), doc -> doc.mapParagraphs(f)); } - ReadOnlyStyledDocument mapParagraphs(UnaryOperator> f) { + ReadOnlyStyledDocument mapParagraphs(UnaryOperator> f) { int n = tree.getLeafCount(); - List> pars = new ArrayList<>(n); + List> pars = new ArrayList<>(n); for(int i = 0; i < n; ++i) { pars.add(f.apply(tree.getLeaf(i))); } - return new ReadOnlyStyledDocument<>(pars); + return new ReadOnlyStyledDocument<>(pars, segmentOps); } @Override @@ -350,7 +320,7 @@ public String toString() { @Override public final boolean equals(Object other) { if(other instanceof StyledDocument) { - StyledDocument that = (StyledDocument) other; + StyledDocument that = (StyledDocument) other; return Objects.equals(this.getParagraphs(), that.getParagraphs()); } else { return false; diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/RichTextChange.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/RichTextChange.java index b5aef4483..8db5e0a2c 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/RichTextChange.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/RichTextChange.java @@ -1,8 +1,8 @@ package org.fxmisc.richtext.model; -public class RichTextChange extends TextChange, RichTextChange> { +public class RichTextChange extends TextChange, RichTextChange> { - public RichTextChange(int position, StyledDocument removed, StyledDocument inserted) { + public RichTextChange(int position, StyledDocument removed, StyledDocument inserted) { super(position, removed, inserted); } @@ -17,17 +17,17 @@ protected int insertedLength() { } @Override - protected final StyledDocument concat(StyledDocument a, StyledDocument b) { + protected final StyledDocument concat(StyledDocument a, StyledDocument b) { return a.concat(b); } @Override - protected final StyledDocument sub(StyledDocument doc, int from, int to) { + protected final StyledDocument sub(StyledDocument doc, int from, int to) { return doc.subSequence(from, to); } @Override - protected final RichTextChange create(int position, StyledDocument removed, StyledDocument inserted) { + protected final RichTextChange create(int position, StyledDocument removed, StyledDocument inserted) { return new RichTextChange<>(position, removed, inserted); } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java new file mode 100644 index 000000000..2b1c718d3 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java @@ -0,0 +1,99 @@ +package org.fxmisc.richtext.model; + +import java.util.Optional; + +import org.reactfx.util.Either; + +/** + * Defines the operations which are supported on a specific segment type. + * + * @param + * @param + */ +public interface SegmentOps { + public int length(SEG seg); + + public char charAt(SEG seg, int index); + + public String getText(SEG seg); + + public SEG subSequence(SEG seg, int start, int end); + + public SEG subSequence(SEG seg, int start); + + public S getStyle(SEG seg); + + public SEG setStyle(SEG seg, S style); + + public Optional join(SEG currentSeg, SEG nextSeg); + + public default SegmentOps, S> or(SegmentOps rOps) { + return either(this, rOps); + } + + public default TextOps, S> or_(TextOps rOps) { + return TextOps.eitherR(this, rOps); + } + + public static SegmentOps, S> either(SegmentOps lOps, SegmentOps rOps) { + return new EitherSegmentOps(lOps, rOps); + } +} + +class EitherSegmentOps implements SegmentOps, S> { + + private final SegmentOps lOps; + private final SegmentOps rOps; + + EitherSegmentOps(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(l -> lOps.charAt(l, index), + r -> rOps.charAt(r, 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(l -> lOps.subSequence(l, start, end), + r -> rOps.subSequence(r, start, end)); + } + + @Override + public Either subSequence(Either seg, int start) { + return seg.map(l -> lOps.subSequence(l, start), + r -> rOps.subSequence(r, start)); + } + + @Override + public S getStyle(Either seg) { + return seg.unify(l -> lOps.getStyle(l), + r -> rOps.getStyle(r)); + } + + @Override + public Either setStyle(Either seg, S style) { + return seg.map(l -> lOps.setStyle(l, style), + r -> rOps.setStyle(r, style)); + } + + @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))); + } +} \ No newline at end of file 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 988b7714b..b5df8d4fe 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java @@ -22,14 +22,14 @@ /** * Provides an implementation of {@link EditableStyledDocument} */ -public final class SimpleEditableStyledDocument implements EditableStyledDocument { +public final class SimpleEditableStyledDocument implements EditableStyledDocument { private class ParagraphList - extends LiveListBase> - implements UnmodifiableByDefaultLiveList> { + extends LiveListBase> + implements UnmodifiableByDefaultLiveList> { @Override - public Paragraph get(int index) { + public Paragraph get(int index) { return doc.getParagraph(index); } @@ -42,17 +42,17 @@ public int size() { protected Subscription observeInputs() { return parChanges.subscribe(mod -> { mod = mod.trim(); - QuasiListModification> qmod = + QuasiListModification> qmod = QuasiListModification.create(mod.getFrom(), mod.getRemoved(), mod.getAddedSize()); notifyObservers(qmod.asListChange()); }); } } - private ReadOnlyStyledDocument doc; + private ReadOnlyStyledDocument doc; - private final EventSource> richChanges = new EventSource<>(); - @Override public EventStream> richChanges() { return richChanges; } + private final EventSource> richChanges = new EventSource<>(); + @Override public EventStream> richChanges() { return richChanges; } private final Val text = Val.create(() -> doc.getText(), richChanges); @Override public String getText() { return text.getValue(); } @@ -64,18 +64,18 @@ protected Subscription observeInputs() { @Override public Val lengthProperty() { return length; } @Override public int length() { return length.getValue(); } - private final EventSource>> parChanges = + private final EventSource>> parChanges = new EventSource<>(); - private final LiveList> paragraphs = new ParagraphList(); + private final LiveList> paragraphs = new ParagraphList(); @Override - public LiveList> getParagraphs() { + public LiveList> getParagraphs() { return paragraphs; } @Override - public ReadOnlyStyledDocument snapshot() { + public ReadOnlyStyledDocument snapshot() { return doc; } @@ -83,16 +83,19 @@ public ReadOnlyStyledDocument snapshot() { @Override public final SuspendableNo beingUpdatedProperty() { return beingUpdated; } @Override public final boolean isBeingUpdated() { return beingUpdated.get(); } + private final SegmentOps segmentOps; + @Override public final SegmentOps getSegOps() { return segmentOps; } - SimpleEditableStyledDocument(Paragraph initialParagraph) { - this.doc = new ReadOnlyStyledDocument<>(Collections.singletonList(initialParagraph)); + SimpleEditableStyledDocument(Paragraph initialParagraph, SegmentOps segmentOps) { + this.doc = new ReadOnlyStyledDocument<>(Collections.singletonList(initialParagraph), segmentOps); + this.segmentOps = segmentOps; } /** * Creates an empty {@link EditableStyledDocument} */ - public SimpleEditableStyledDocument(PS initialParagraphStyle, S initialStyle) { - this(new Paragraph<>(initialParagraphStyle, "", initialStyle)); + public SimpleEditableStyledDocument(PS initialParagraphStyle, S initialStyle, TextOps segmentOps) { + this(new Paragraph<>(initialParagraphStyle, segmentOps, segmentOps.create("", initialStyle)), segmentOps); } @@ -107,9 +110,9 @@ public Position offsetToPosition(int offset, Bias bias) { } @Override - public void replace(int start, int end, StyledDocument replacement) { + public void replace(int start, int end, StyledDocument replacement) { ensureValidRange(start, end); - doc.replace(start, end, ReadOnlyStyledDocument.from(replacement)).exec(this::update); + doc.replace(start, end, ReadOnlyStyledDocument.from(replacement, segmentOps)).exec(this::update); } @Override @@ -135,24 +138,24 @@ public void setStyle(int paragraph, int fromCol, int toCol, S style) { } @Override - public void setStyleSpans(int from, StyleSpans styleSpans) { + public void setStyleSpans(int from, StyleSpans styleSpans) { int len = styleSpans.length(); ensureValidRange(from, from + len); doc.replace(from, from + len, d -> { Position i = styleSpans.position(0, 0); - List> pars = new ArrayList<>(d.getParagraphs().size()); - for(Paragraph p: d.getParagraphs()) { + List> pars = new ArrayList<>(d.getParagraphs().size()); + for(Paragraph p: d.getParagraphs()) { Position j = i.offsetBy(p.length(), Backward); - StyleSpans spans = styleSpans.subView(i, j); + StyleSpans spans = styleSpans.subView(i, j); pars.add(p.restyle(0, spans)); i = j.offsetBy(1, Forward); // skip the newline } - return new ReadOnlyStyledDocument<>(pars); + return new ReadOnlyStyledDocument<>(pars, segmentOps); }).exec(this::update); } @Override - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { setStyleSpans(doc.position(paragraph, from).toOffset(), styleSpans); } @@ -163,15 +166,19 @@ public void setParagraphStyle(int parIdx, PS style) { } @Override - public StyledDocument concat(StyledDocument that) { + public StyledDocument concat(StyledDocument that) { return doc.concat(that); } @Override - public StyledDocument subSequence(int start, int end) { + public StyledDocument subSequence(int start, int end) { return doc.subSequence(start, end); } + @Override + public StyledDocument subDocument(int paragraphIndex) { + return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex)), segmentOps); + } /* ********************************************************************** * * * @@ -198,9 +205,9 @@ private int fullLength(int par) { } private void update( - ReadOnlyStyledDocument newValue, - RichTextChange change, - MaterializedListModification> parChange) { + ReadOnlyStyledDocument newValue, + RichTextChange change, + MaterializedListModification> parChange) { this.doc = newValue; beingUpdated.suspendWhile(() -> { richChanges.push(change); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java index 3cec28421..e20baf236 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java @@ -3,23 +3,23 @@ import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import javafx.scene.control.IndexRange; -public interface StyledDocument extends TwoDimensional { +public interface StyledDocument extends TwoDimensional { int length(); String getText(); - List> getParagraphs(); + List> getParagraphs(); - StyledDocument concat(StyledDocument that); + StyledDocument concat(StyledDocument that); - StyledDocument subSequence(int start, int end); + StyledDocument subSequence(int start, int end); + SegmentOps getSegOps(); default String getText(IndexRange range) { return getText(range.getStart(), range.getEnd()); @@ -29,12 +29,12 @@ default String getText(int start, int end) { return subSequence(start, end).getText(); } - default StyledDocument subSequence(IndexRange range) { + default StyledDocument subSequence(IndexRange range) { return subSequence(range.getStart(), range.getEnd()); } - default StyledDocument subDocument(int paragraphIndex) { - return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex))); + default StyledDocument subDocument(int paragraphIndex) { + return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex)), getSegOps()); } default char charAt(int index) { @@ -95,18 +95,18 @@ default StyleSpans getStyleSpans(int from, int to) { List> subSpans = new ArrayList<>(affectedPars); if(startParIdx == endParIdx) { - Paragraph par = getParagraphs().get(startParIdx); + Paragraph par = getParagraphs().get(startParIdx); subSpans.add(par.getStyleSpans(start.getMinor(), end.getMinor())); } else { - Paragraph startPar = getParagraphs().get(startParIdx); + Paragraph startPar = getParagraphs().get(startParIdx); subSpans.add(startPar.getStyleSpans(start.getMinor(), startPar.length() + 1)); for(int i = startParIdx + 1; i < endParIdx; ++i) { - Paragraph par = getParagraphs().get(i); + Paragraph par = getParagraphs().get(i); subSpans.add(par.getStyleSpans(0, par.length() + 1)); } - Paragraph endPar = getParagraphs().get(endParIdx); + Paragraph endPar = getParagraphs().get(endParIdx); subSpans.add(endPar.getStyleSpans(0, end.getMinor())); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java index 918c81abd..600d32a69 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java @@ -4,65 +4,100 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.Objects; -import java.util.function.Function; +import java.util.Optional; -import javafx.scene.Node; +public class StyledText { -public class StyledText implements Segment { - private String text; - private S style; + public static TextOps, S> textOps() { + return new TextOps, S>() { - StyledText() {} + @Override + public int length(StyledText styledText) { + return styledText.getText().length(); + } - public StyledText(String text, S style) { - this.text = text; - this.style = style; - } + @Override + public char charAt(StyledText styledText, int index) { + return styledText.getText().charAt(index); + } - @Override - public int length() { - return text.length(); - } + @Override + public String getText(StyledText styledText) { + return styledText.getText(); + } - @Override - public char charAt(int index) { - return text.charAt(index); - } + @Override + public StyledText subSequence(StyledText styledText, int start, int end) { + return new StyledText<>(styledText.getText().substring(start, end), styledText.getStyle()); + } - @Override - public String getText() { - return text; - } + @Override + public StyledText subSequence(StyledText styledText, int start) { + return new StyledText<>(styledText.getText().substring(start), styledText.getStyle()); + } - @Override - public StyledText subSequence(int start, int end) { - return new StyledText<>(text.substring(start, end), style); - } + @Override + public S getStyle(StyledText styledText) { + return styledText.getStyle(); + } - @Override - public StyledText subSequence(int start) { - return new StyledText<>(text.substring(start), style); - } + @Override + public StyledText setStyle(StyledText seg, S style) { + return seg.setStyle(style); + } - @Override - public StyledText append(String str) { - return new StyledText<>(text + str, 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); + } + }; } - @Override - public StyledText spliced(int from, int to, CharSequence replacement) { - String left = text.substring(0, from); - String right = text.substring(to); - return new StyledText<>(left + replacement + right, style); + 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); + } + + }; } - @Override - public S getStyle() { - return style; + + private final String text; + + public String getText() { return text; } + + private final S style; + public S getStyle() { return style; } + + public StyledText setStyle(S style) { + return new StyledText<>(text, style); } - @Override - public void setStyle(S style) { + public StyledText(String text, S style) { + this.text = text; this.style = style; } @@ -87,41 +122,4 @@ public int hashCode() { return Objects.hash(text, style); } - - @Override - public boolean canJoin(Segment right) { - - if (right instanceof StyledText) { - return Objects.equals(getStyle(), right.getStyle()); - } - - return false; - } - - - @Override - public void encode(DataOutputStream os, Codec styleCodec) throws IOException { - Codec.STRING_CODEC.encode(os, getText()); - styleCodec.encode(os, style); - } - - @Override - public void decode(DataInputStream is, Codec styleCodec) throws IOException { - text = Codec.STRING_CODEC.decode(is); - style = styleCodec.decode(is); - } - - @SuppressWarnings("rawtypes") - private static Function nodeFactory; - - @Override - public Node createNode() { - return nodeFactory.apply(this); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static void setNodeFactory(Function, Node> nodeFactory) { - StyledText.nodeFactory = (Function) (Object) nodeFactory; - } - } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck new file mode 100644 index 000000000..918c81abd --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck @@ -0,0 +1,127 @@ +package org.fxmisc.richtext.model; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Objects; +import java.util.function.Function; + +import javafx.scene.Node; + +public class StyledText implements Segment { + private String text; + private S style; + + StyledText() {} + + public StyledText(String text, S style) { + this.text = text; + this.style = style; + } + + @Override + public int length() { + return text.length(); + } + + @Override + public char charAt(int index) { + return text.charAt(index); + } + + @Override + public String getText() { + return text; + } + + @Override + public StyledText subSequence(int start, int end) { + return new StyledText<>(text.substring(start, end), style); + } + + @Override + public StyledText subSequence(int start) { + return new StyledText<>(text.substring(start), style); + } + + @Override + public StyledText append(String str) { + return new StyledText<>(text + str, style); + } + + @Override + public StyledText spliced(int from, int to, CharSequence replacement) { + String left = text.substring(0, from); + String right = text.substring(to); + return new StyledText<>(left + replacement + right, style); + } + + @Override + public S getStyle() { + return style; + } + + @Override + public void setStyle(S style) { + 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 Objects.equals(this.text, that.text) + && Objects.equals(this.style, that.style); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(text, style); + } + + + @Override + public boolean canJoin(Segment right) { + + if (right instanceof StyledText) { + return Objects.equals(getStyle(), right.getStyle()); + } + + return false; + } + + + @Override + public void encode(DataOutputStream os, Codec styleCodec) throws IOException { + Codec.STRING_CODEC.encode(os, getText()); + styleCodec.encode(os, style); + } + + @Override + public void decode(DataInputStream is, Codec styleCodec) throws IOException { + text = Codec.STRING_CODEC.decode(is); + style = styleCodec.decode(is); + } + + @SuppressWarnings("rawtypes") + private static Function nodeFactory; + + @Override + public Node createNode() { + return nodeFactory.apply(this); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static void setNodeFactory(Function, Node> nodeFactory) { + StyledText.nodeFactory = (Function) (Object) nodeFactory; + } + +} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index 3f0afc401..a173d43c1 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -28,15 +28,15 @@ import org.reactfx.value.Var; /** - * Model for {@link org.fxmisc.richtext.StyledTextArea} + * Model for {@link org.fxmisc.richtext.GenericRichtextArea} * * @param type of style that can be applied to text. * @param type of style that can be applied to Paragraph */ -public class StyledTextAreaModel +public class StyledTextAreaModel implements - EditActions, - NavigationActions, + EditActions, + NavigationActions, UndoActions, TwoDimensional { @@ -103,7 +103,7 @@ private static int clamp(int min, int val, int max) { @Override public final ObservableValue textProperty() { return text; } // rich text - @Override public final StyledDocument getDocument() { return content.snapshot(); } + @Override public final StyledDocument getDocument() { return content.snapshot(); } // length private final SuspendableVal length; @@ -143,8 +143,8 @@ private static int clamp(int min, int val, int max) { @Override public final ObservableValue caretColumnProperty() { return caretColumn; } // paragraphs - private final SuspendableList> paragraphs; - @Override public LiveList> getParagraphs() { return paragraphs; } + private final SuspendableList> paragraphs; + @Override public LiveList> getParagraphs() { return paragraphs; } // beingUpdated private final SuspendableNo beingUpdated = new SuspendableNo(); @@ -163,9 +163,9 @@ private static int clamp(int min, int val, int max) { public final EventStream plainTextChanges() { return plainTextChanges; } // rich text changes - private final SuspendableEventStream> richTextChanges; + private final SuspendableEventStream> richTextChanges; @Override - public final EventStream> richChanges() { return richTextChanges; } + public final EventStream> richChanges() { return richTextChanges; } /* ********************************************************************** * * * @@ -173,22 +173,26 @@ private static int clamp(int min, int val, int max) { * * * ********************************************************************** */ + private final TextOps textOps; + private Subscription subscriptions = () -> {}; private Position selectionStart2D; private Position selectionEnd2D; + private final SegmentOps segmentOps; + /** * content model */ - private final EditableStyledDocument content; + private final EditableStyledDocument content; /** * Usually used to create another area (View) that shares * the same document (Model). * @return this area's {@link EditableStyledDocument} */ - public final EditableStyledDocument getContent() { return content; } + public final EditableStyledDocument getContent() { return content; } /** * Style used by default when no other style is provided. @@ -225,33 +229,40 @@ private static int clamp(int min, int val, int max) { * @param initialParagraphStyle style to use in places where no other style is * specified (yet). */ - public StyledTextAreaModel(PS initialParagraphStyle, S initialTextStyle) { - this(initialParagraphStyle, initialTextStyle, true); + public StyledTextAreaModel(PS initialParagraphStyle, S initialTextStyle, TextOps segmentOps) { + this(initialParagraphStyle, initialTextStyle, segmentOps, true); } - public StyledTextAreaModel(PS initialParagraphStyle, S initialTextStyle, boolean preserveStyle + public StyledTextAreaModel(PS initialParagraphStyle, S initialTextStyle, TextOps segmentOps, boolean preserveStyle ) { this(initialParagraphStyle, initialTextStyle, - new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle), preserveStyle); + new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), + segmentOps, preserveStyle); } /** - * The same as {@link #StyledTextAreaModel(Object, Object)} except that + * The same as {@link #StyledTextAreaModel(Object, Object, TextOps)} except that * this constructor can be used to create another {@code StyledTextArea} object that * shares the same {@link EditableStyledDocument}. */ public StyledTextAreaModel(PS initialParagraphStyle, S initialTextStyle, - EditableStyledDocument document + EditableStyledDocument document, TextOps textOps ) { - this(initialParagraphStyle, initialTextStyle, document, true); + this(initialParagraphStyle, initialTextStyle, document, textOps, true); } - public StyledTextAreaModel(PS initialParagraphStyle, S initialTextStyle, - EditableStyledDocument document, boolean preserveStyle + public StyledTextAreaModel( + PS initialParagraphStyle, + S initialTextStyle, + EditableStyledDocument document, + TextOps textOps, + boolean preserveStyle ) { + this.textOps = textOps; this.initialTextStyle = initialTextStyle; this.initialParagraphStyle = initialParagraphStyle; this.preserveStyle = preserveStyle; + this.segmentOps = segmentOps; content = document; paragraphs = LiveList.suspendable(content.getParagraphs()); @@ -375,17 +386,17 @@ public String getText(int paragraph) { return paragraphs.get(paragraph).getText(); } - public Paragraph getParagraph(int index) { + public Paragraph getParagraph(int index) { return paragraphs.get(index); } @Override - public StyledDocument subDocument(int start, int end) { + public StyledDocument subDocument(int start, int end) { return content.subSequence(start, end); } @Override - public StyledDocument subDocument(int paragraphIndex) { + public StyledDocument subDocument(int paragraphIndex) { return content.subDocument(paragraphIndex); } @@ -507,6 +518,7 @@ public StyleSpans getStyleSpans(int paragraph, IndexRange range) { return getStyleSpans(paragraph, range.getStart(), range.getEnd()); } + @Override public int getAbsolutePosition(int paragraphIndex, int columnIndex) { return content.getAbsolutePosition(paragraphIndex, columnIndex); } @@ -562,7 +574,7 @@ public void setStyle(int paragraph, int from, int to, S style) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int from, StyleSpans styleSpans) { + public void setStyleSpans(int from, StyleSpans styleSpans) { content.setStyleSpans(from, styleSpans); } @@ -576,7 +588,7 @@ public void setStyleSpans(int from, StyleSpans styleSpans) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { content.setStyleSpans(paragraph, from, styleSpans); } @@ -618,13 +630,13 @@ public void clearParagraphStyle(int paragraph) { @Override public void replaceText(int start, int end, String text) { - StyledDocument doc = ReadOnlyStyledDocument.fromString( - text, getParagraphStyleForInsertionAt(start), getStyleForInsertionAt(start)); + StyledDocument doc = ReadOnlyStyledDocument.fromString( + text, getParagraphStyleForInsertionAt(start), getStyleForInsertionAt(start), textOps); replace(start, end, doc); } @Override - public void replace(int start, int end, StyledDocument replacement) { + public void replace(int start, int end, StyledDocument replacement) { try (Guard g = content.beingUpdatedProperty().suspend()) { start = clamp(0, start, getLength()); end = clamp(0, end, getLength()); @@ -702,8 +714,8 @@ private UndoManager createPlainUndoManager(UndoManagerFactory factory) { } private UndoManager createRichUndoManager(UndoManagerFactory factory) { - Consumer> apply = change -> replace(change.getPosition(), change.getPosition() + change.getRemoved().length(), change.getInserted()); - BiFunction, RichTextChange, Optional>> merge = RichTextChange::mergeWith; + Consumer> apply = change -> replace(change.getPosition(), change.getPosition() + change.getRemoved().length(), change.getInserted()); + BiFunction, RichTextChange, Optional>> merge = RichTextChange::mergeWith; return factory.create(richChanges(), RichTextChange::invert, apply, merge); } 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 0c573ab58..f252dca13 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextEditingArea.java @@ -14,7 +14,7 @@ * * @param type of style that can be applied to text. */ -public interface TextEditingArea { +public interface TextEditingArea { /******************* * * @@ -39,7 +39,7 @@ public interface TextEditingArea { * The returned document is immutable, it does not reflect * subsequent edits of this text-editing area. */ - StyledDocument getDocument(); + StyledDocument getDocument(); /** * The current position of the caret, as a character offset in the text. @@ -90,7 +90,7 @@ public interface TextEditingArea { /** * Unmodifiable observable list of paragraphs in this text area. */ - ObservableList> getParagraphs(); + ObservableList> getParagraphs(); /********************* @@ -107,7 +107,7 @@ public interface TextEditingArea { /** * Stream of rich text changes. */ - EventStream> richChanges(); + EventStream> richChanges(); /*************** @@ -141,12 +141,12 @@ default String getText(int startParagraph, int startColumn, int endParagraph, in /** * Returns rich-text content of the given paragraph. */ - StyledDocument subDocument(int paragraphIndex); + StyledDocument subDocument(int paragraphIndex); /** * Returns rich-text content of the given character range. */ - StyledDocument subDocument(int start, int end); + StyledDocument subDocument(int start, int end); /** * Returns rich-text content of the given character range. @@ -154,7 +154,7 @@ default String getText(int startParagraph, int startColumn, int endParagraph, in *

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

*/ - default StyledDocument subDocument(int startParagraph, int startColumn, int endParagraph, int endColumn) { + default StyledDocument subDocument(int startParagraph, int startColumn, int endParagraph, int endColumn) { int start = getAbsolutePosition(startParagraph, startColumn); int end = getAbsolutePosition(endParagraph, endColumn); return subDocument(start, end); @@ -219,7 +219,7 @@ default void replaceText(int startParagraph, int startColumn, int endParagraph, /** * Replaces a range of characters with the given rich-text document. */ - void replace(int start, int end, StyledDocument replacement); + void replace(int start, int end, StyledDocument replacement); /** * Replaces a range of characters with the given rich-text document. @@ -227,7 +227,7 @@ default void replaceText(int startParagraph, int startColumn, int endParagraph, *

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

*/ - default void replace(int startParagraph, int startColumn, int endParagraph, int endColumn, StyledDocument replacement) { + default void replace(int startParagraph, int startColumn, int endParagraph, int endColumn, StyledDocument replacement) { int start = getAbsolutePosition(startParagraph, startColumn); int end = getAbsolutePosition(endParagraph, endColumn); replace(start, end, replacement); @@ -250,7 +250,7 @@ default void replaceText(IndexRange range, String text) { * Equivalent to * {@code replace(range.getStart(), range.getEnd(), replacement)}. */ - default void replace(IndexRange range, StyledDocument replacement) { + default void replace(IndexRange range, StyledDocument replacement) { 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 new file mode 100644 index 000000000..fb00fc0b9 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java @@ -0,0 +1,51 @@ +package org.fxmisc.richtext.model; + +import org.reactfx.util.Either; + +public interface TextOps extends SegmentOps { + public SEG create(String text, S style); + + public default TextOps, S> _or(SegmentOps rOps) { + return eitherL(this, rOps); + } + + public static TextOps, S> eitherL(TextOps lOps, SegmentOps rOps) { + return new LeftTextOps<>(lOps, rOps); + } + + public static TextOps, S> eitherR(SegmentOps lOps, TextOps rOps) { + return new RightTextOps<>(lOps, rOps); + } +} + +class LeftTextOps extends EitherSegmentOps implements TextOps, S> { + + private final TextOps lOps; + + LeftTextOps(TextOps lOps, SegmentOps rOps) { + super(lOps, rOps); + this.lOps = lOps; + } + + @Override + public Either create(String text, S style) { + return Either.left(lOps.create(text, style)); + } + +} + +class RightTextOps extends EitherSegmentOps implements TextOps, S> { + + private final TextOps rOps; + + RightTextOps(SegmentOps lOps, TextOps rOps) { + super(lOps, rOps); + this.rOps = rOps; + } + + @Override + public Either create(String text, S style) { + return Either.right(rOps.create(text, style)); + } + +} \ 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 87a298147..d7ff9ec32 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java @@ -1,6 +1,7 @@ package org.fxmisc.richtext.model; import static org.junit.Assert.*; + import org.junit.Test; public class ParagraphTest { @@ -10,10 +11,11 @@ public class ParagraphTest { // This relates to merging text changes and issue #216. @Test public void concatEmptyParagraphsTest() { - Paragraph p1 = new Paragraph<>(null, "", true); - Paragraph p2 = new Paragraph<>(null, "", false); + 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)); - Paragraph p = p1.concat(p2); + Paragraph, Boolean> p = p1.concat(p2); assertEquals(Boolean.TRUE, p.getStyleAtPosition(0)); } 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 bbb60175b..ee10f244e 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java @@ -1,22 +1,24 @@ package org.fxmisc.richtext.model; import static org.fxmisc.richtext.model.ReadOnlyStyledDocument.*; -import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.*; import java.util.List; + import org.junit.Test; public class ReadOnlyStyledDocumentTest { @Test public void testUndo() { - ReadOnlyStyledDocument doc0 = fromString("", "X", "X"); + TextOps, String> segOps = StyledText.textOps(); + ReadOnlyStyledDocument, String> doc0 = fromString("", "X", "X", segOps); - doc0.replace(0, 0, fromString("abcd", "Y", "Y")).exec((doc1, chng1, pchng1) -> { + doc0.replace(0, 0, fromString("abcd", "Y", "Y", segOps)).exec((doc1, chng1, pchng1) -> { // undo chng1 - doc1.replace(chng1.getPosition(), chng1.getInsertionEnd(), from(chng1.getRemoved())).exec((doc2, chng2, pchng2) -> { + doc1.replace(chng1.getPosition(), chng1.getInsertionEnd(), from(chng1.getRemoved(), ops)).exec((doc2, chng2, pchng2) -> { // we should have arrived at the original document assertEquals(doc0, doc2); @@ -28,15 +30,16 @@ public void testUndo() { @Test public void deleteNewlineTest() { - ReadOnlyStyledDocument doc0 = fromString("Foo\nBar", null, null); - doc0.replace(3, 4, fromString("", null, null)).exec((doc1, ch, pch) -> { - List> removed = pch.getRemoved(); - List> added = pch.getAdded(); + 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(); assertEquals(2, removed.size()); - assertEquals(new Paragraph(null, "Foo", null), removed.get(0)); - assertEquals(new Paragraph(null, "Bar", null), removed.get(1)); + 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)); assertEquals(1, added.size()); - assertEquals(new Paragraph(null, "FooBar", null), added.get(0)); + assertEquals(new Paragraph, Void>(null, segOps, segOps.create("FooBar", null)), added.get(0)); }); } @@ -45,8 +48,9 @@ public void testRestyle() { final String fooBar = "Foo Bar"; final String and = " and "; final String helloWorld = "Hello World"; - - SimpleEditableStyledDocument doc0 = new SimpleEditableStyledDocument<>("", ""); + TextOps, String> segOps = StyledText.textOps(); + + SimpleEditableStyledDocument, String> doc0 = new SimpleEditableStyledDocument<>("", "", segOps); ReadOnlyStyledDocument text = fromString(fooBar, "", "bold"); doc0.replace(doc0.getLength(), doc0.getLength(), text); @@ -73,5 +77,5 @@ public void testRestyle() { assertThat(result.get(1).getText(), equalTo("Bar and Hello")); assertThat(result.get(2).getText(), equalTo(" World")); } - + } 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 7a14817ec..a15cce014 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java @@ -1,23 +1,29 @@ package org.fxmisc.richtext.model; import static org.junit.Assert.*; + import org.junit.Test; public class SimpleEditableStyledDocumentTest { + /** * The style of the inserted text will be the style at position * {@code start} in the current document. + * @param ops */ - private void replaceText(EditableStyledDocument doc, int start, int end, String text) { - StyledDocument styledDoc = ReadOnlyStyledDocument.fromString( - text, doc.getParagraphStyleAtPosition(start), doc.getStyleAtPosition(start)); + private void replaceText(EditableStyledDocument doc, int start, int end, String text, TextOps segOps) { + StyledDocument styledDoc = ReadOnlyStyledDocument.fromString( + text, doc.getParagraphStyleAtPosition(start), doc.getStyleAtPosition(start), segOps); doc.replace(start, end, styledDoc); } + + private final TextOps, String> segOps = StyledText.textOps(); + @Test public void testConsistencyOfTextWithLength() { - SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); + SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); document.getText(); // enforce evaluation of text property document.getLength(); // enforce evaluation of length property @@ -27,12 +33,12 @@ public void testConsistencyOfTextWithLength() { assertEquals(length, textLength); }); - replaceText(document, 0, 0, "A"); + replaceText(document, 0, 0, "A", segOps); } @Test public void testConsistencyOfLengthWithText() { - SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); + SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); document.getText(); // enforce evaluation of text property document.getLength(); // enforce evaluation of length property @@ -42,54 +48,54 @@ public void testConsistencyOfLengthWithText() { assertEquals(textLength, length); }); - replaceText(document, 0, 0, "A"); + replaceText(document, 0, 0, "A", segOps); } @Test public void testUnixParagraphCount() { - SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); + SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); String text = "X\nY"; - replaceText(document, 0, 0, text); + replaceText(document, 0, 0, text, segOps); assertEquals(2, document.getParagraphs().size()); } @Test public void testMacParagraphCount() { - SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); + SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); String text = "X\rY"; - replaceText(document, 0, 0, text); + replaceText(document, 0, 0, text, segOps); assertEquals(2, document.getParagraphs().size()); } @Test public void testWinParagraphCount() { - SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); + SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); String text = "X\r\nY"; - replaceText(document, 0, 0, text); + replaceText(document, 0, 0, text, segOps); assertEquals(2, document.getParagraphs().size()); } @Test public void testGetTextWithEndAfterNewline() { - SimpleEditableStyledDocument doc = new SimpleEditableStyledDocument<>(true, ""); + SimpleEditableStyledDocument, String> doc = new SimpleEditableStyledDocument<>(true, "", segOps); - replaceText(doc, 0, 0, "123\n"); + replaceText(doc, 0, 0, "123\n", segOps); String txt1 = doc.getText(0, 4); assertEquals(4, txt1.length()); - replaceText(doc, 4, 4, "567"); + replaceText(doc, 4, 4, "567", segOps); String txt2 = doc.getText(2, 4); assertEquals(2, txt2.length()); - replaceText(doc, 4, 4, "\n"); + replaceText(doc, 4, 4, "\n", segOps); String txt3 = doc.getText(2, 4); assertEquals(2, txt3.length()); } @Test public void testWinDocumentLength() { - SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); - replaceText(document, 0, 0, "X\r\nY"); + SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); + replaceText(document, 0, 0, "X\r\nY", segOps); assertEquals(document.getText().length(), document.getLength()); } } diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java index ffbc6f410..c5c069640 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java @@ -9,13 +9,17 @@ public class StyledTextAreaModelTest { + @Test public void testUndoWithWinNewlines() { + final TextOps>, Collection> segOps = StyledText.textOps(); + String text1 = "abc\r\ndef"; String text2 = "A\r\nB\r\nC"; - StyledTextAreaModel, Collection> model = new StyledTextAreaModel<>( + StyledTextAreaModel, StyledText>, Collection> model = new StyledTextAreaModel<>( Collections.emptyList(), - Collections.emptyList() + Collections.emptyList(), + segOps ); model.replaceText(text1); @@ -29,10 +33,12 @@ public void testUndoWithWinNewlines() { @Test public void testForBug216() { + final TextOps, Boolean> segOps = StyledText.textOps(); + // set up area with some styled text content boolean initialStyle = false; - StyledTextAreaModel model = new StyledTextAreaModel<>( - "", initialStyle, new SimpleEditableStyledDocument<>("", initialStyle), true); + StyledTextAreaModel, Boolean> model = new StyledTextAreaModel<>( + "", initialStyle, new SimpleEditableStyledDocument<>("", initialStyle, segOps), segOps, true); model.replaceText("testtest"); model.setStyle(0, 8, true); From 1ffb66020cd66b6b60504d9b5ef137b0eff67739 Mon Sep 17 00:00:00 2001 From: Andreas Fester Date: Wed, 19 Oct 2016 13:32:30 +0200 Subject: [PATCH 3/9] Added compatibility layer for SimpleEditableStyledDocument, removed storing segment operations in ReadOnlyStyledDocument --- .../richtext/demo/richtext/RichText.java | 2 +- .../fxmisc/richtext/GenericStyledArea.java | 14 +- .../fxmisc/richtext/InlineCssTextArea.java | 2 +- .../fxmisc/richtext/StyleClassedTextArea.java | 10 +- .../org/fxmisc/richtext/StyledTextArea.java | 4 +- .../richtext/StyledTextAreaBehavior.java | 2 +- .../model/EditableStyledDocument.java | 4 +- .../model/GenericEditableStyledDocument.java | 17 ++ .../GenericEditableStyledDocumentBase.java | 209 +++++++++++++++++ .../model/ReadOnlyStyledDocument.java | 26 +-- .../org/fxmisc/richtext/model/SegmentOps.java | 4 +- .../model/SimpleEditableStyledDocument.java | 215 +----------------- .../fxmisc/richtext/model/StyledDocument.java | 4 +- .../richtext/model/StyledTextAreaModel.java | 9 +- .../model/ReadOnlyStyledDocumentTest.java | 2 +- .../SimpleEditableStyledDocumentTest.java | 41 ++-- .../model/StyledTextAreaModelTest.java | 3 +- 17 files changed, 288 insertions(+), 280 deletions(-) create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocument.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java 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 3b913fbcf..7341ec050 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 @@ -413,7 +413,7 @@ private void save(File file) { // Use the Codec to save the document in a binary format area.getStyleCodecs().ifPresent(codecs -> { Codec, LinkedImage>, TextStyle>> codec = - ReadOnlyStyledDocument.codec(codecs._1, codecs._2, doc.getSegOps()); + ReadOnlyStyledDocument.codec(codecs._1, codecs._2, area.getSegOps()); try { FileOutputStream fos = new FileOutputStream(file); DataOutputStream dos = new DataOutputStream(fos); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index c7c2171c6..881160159 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -56,15 +56,15 @@ import org.fxmisc.richtext.model.Codec; import org.fxmisc.richtext.model.EditActions; import org.fxmisc.richtext.model.EditableStyledDocument; +import org.fxmisc.richtext.model.GenericEditableStyledDocument; +import org.fxmisc.richtext.model.StyledTextAreaModel; import org.fxmisc.richtext.model.NavigationActions; import org.fxmisc.richtext.model.Paragraph; import org.fxmisc.richtext.model.PlainTextChange; import org.fxmisc.richtext.model.RichTextChange; import org.fxmisc.richtext.model.SegmentOps; -import org.fxmisc.richtext.model.SimpleEditableStyledDocument; import org.fxmisc.richtext.model.StyleSpans; import org.fxmisc.richtext.model.StyledDocument; -import org.fxmisc.richtext.model.StyledTextAreaModel; import org.fxmisc.richtext.model.TextEditingArea; import org.fxmisc.richtext.model.TextOps; import org.fxmisc.richtext.model.TwoDimensional; @@ -529,7 +529,8 @@ final StyledTextAreaModel getModel() { * * * ********************************************************************** */ - @Override public final SegmentOps getSegOps() { return getContent().getSegOps(); } + private final TextOps segmentOps; + @Override public final SegmentOps getSegOps() { return segmentOps; } /* ********************************************************************** * * * @@ -561,7 +562,7 @@ public GenericStyledArea(PS initialParagraphStyle, BiConsumer appl S initialTextStyle, TextOps segmentOps, boolean preserveStyle, Function nodeFactory) { this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, - new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), segmentOps, preserveStyle, nodeFactory); + new GenericEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), segmentOps, preserveStyle, nodeFactory); } /** @@ -590,6 +591,7 @@ public GenericStyledArea( Function nodeFactory) { this.model = new StyledTextAreaModel<>(initialParagraphStyle, initialTextStyle, document, textOps, preserveStyle); this.applyParagraphStyle = applyParagraphStyle; + this.segmentOps = textOps; // allow tab traversal into area setFocusTraversable(true); @@ -1050,7 +1052,7 @@ public void setStyle(int paragraph, int from, int to, S style) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int from, StyleSpans styleSpans) { + public void setStyleSpans(int from, StyleSpans styleSpans) { model.setStyleSpans(from, styleSpans); } @@ -1064,7 +1066,7 @@ public void setStyleSpans(int from, StyleSpans styleSpans) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { model.setStyleSpans(paragraph, from, styleSpans); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java index 29bb4ccd3..003a76a09 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/InlineCssTextArea.java @@ -14,7 +14,7 @@ public class InlineCssTextArea extends StyledTextArea { public InlineCssTextArea() { - this(new SimpleEditableStyledDocument<>("", "", StyledText.textOps())); + this(new SimpleEditableStyledDocument<>("", "")); } public InlineCssTextArea(EditableStyledDocument, String> document) { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java index 7fad2ea3c..363324971 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyleClassedTextArea.java @@ -15,10 +15,10 @@ public class StyleClassedTextArea extends StyledTextArea, Col public StyleClassedTextArea(EditableStyledDocument, StyledText>, Collection> document, boolean preserveStyle) { super(Collections.emptyList(), - (paragraph, styleClasses) -> paragraph.getStyleClass().addAll(styleClasses), - Collections.emptyList(), - (text, styleClasses) -> text.getStyleClass().addAll(styleClasses), - document, preserveStyle + (paragraph, styleClasses) -> paragraph.getStyleClass().addAll(styleClasses), + Collections.emptyList(), + (text, styleClasses) -> text.getStyleClass().addAll(styleClasses), + document, preserveStyle ); setStyleCodecs( @@ -29,7 +29,7 @@ public StyleClassedTextArea(EditableStyledDocument, StyledTex public StyleClassedTextArea(boolean preserveStyle) { this( new SimpleEditableStyledDocument<>( - Collections.emptyList(), Collections.emptyList(), StyledText.textOps() + Collections.emptyList(), Collections.emptyList() ), preserveStyle); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java index ff562c3c5..3be3366a6 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java @@ -67,7 +67,7 @@ public StyledTextArea(PS initialParagraphStyle, BiConsumer applyPa super(initialParagraphStyle, applyParagraphStyle, initialTextStyle, document, StyledText.textOps(), preserveStyle, - seg -> createStyledTextNode(seg, document.getSegOps(), applyStyle)); + seg -> createStyledTextNode(seg, StyledText.textOps(), applyStyle)); } public StyledTextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, @@ -85,7 +85,7 @@ public StyledTextArea(PS initialParagraphStyle, BiConsumer applyPa applyParagraphStyle, initialTextStyle, applyStyle, - new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, StyledText.textOps()), + new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle), preserveStyle); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java index 73fc542bf..e5a7ea2c9 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextAreaBehavior.java @@ -18,8 +18,8 @@ import javafx.scene.input.MouseButton; import javafx.scene.input.MouseEvent; -import org.fxmisc.richtext.model.NavigationActions.SelectionPolicy; import org.fxmisc.richtext.model.StyledTextAreaModel; +import org.fxmisc.richtext.model.NavigationActions.SelectionPolicy; import org.fxmisc.richtext.model.TwoDimensional.Position; import org.fxmisc.wellbehaved.event.EventPattern; import org.fxmisc.wellbehaved.event.template.InputMapTemplate; diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java index cf62aeb73..562715c17 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java @@ -72,9 +72,9 @@ default EventStream plainChanges() { void setStyle(int paragraph, int fromCol, int toCol, S style); - void setStyleSpans(int from, StyleSpans styleSpens); + void setStyleSpans(int from, StyleSpans styleSpens); - void setStyleSpans(int paragraph, int from, StyleSpans styleSpens); + void setStyleSpans(int paragraph, int from, StyleSpans styleSpens); void setParagraphStyle(int parIdx, PS style); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocument.java new file mode 100644 index 000000000..25eedda95 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocument.java @@ -0,0 +1,17 @@ +package org.fxmisc.richtext.model; + +/** + * Provides a basic implementation of {@link EditableStyledDocument}. See {@link SimpleEditableStyledDocument} for + * a version that is specified for {@link StyledText}. + * @param + * @param + * @param + */ +public final class GenericEditableStyledDocument extends GenericEditableStyledDocumentBase + implements EditableStyledDocument { + + public GenericEditableStyledDocument(PS initialParagraphStyle, S initialStyle, TextOps 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 new file mode 100644 index 000000000..2260b8105 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java @@ -0,0 +1,209 @@ +package org.fxmisc.richtext.model; + +import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.reactfx.EventSource; +import org.reactfx.EventStream; +import org.reactfx.Subscription; +import org.reactfx.SuspendableNo; +import org.reactfx.collection.LiveList; +import org.reactfx.collection.LiveListBase; +import org.reactfx.collection.MaterializedListModification; +import org.reactfx.collection.QuasiListModification; +import org.reactfx.collection.UnmodifiableByDefaultLiveList; +import org.reactfx.util.BiIndex; +import org.reactfx.util.Lists; +import org.reactfx.value.Val; + +/** + * Provides an implementation of {@link EditableStyledDocument} + */ +class GenericEditableStyledDocumentBase implements EditableStyledDocument { + + private class ParagraphList + extends LiveListBase> + implements UnmodifiableByDefaultLiveList> { + + @Override + public Paragraph get(int index) { + return doc.getParagraph(index); + } + + @Override + public int size() { + return doc.getParagraphCount(); + } + + @Override + protected Subscription observeInputs() { + return parChanges.subscribe(mod -> { + mod = mod.trim(); + QuasiListModification> qmod = + QuasiListModification.create(mod.getFrom(), mod.getRemoved(), mod.getAddedSize()); + notifyObservers(qmod.asListChange()); + }); + } + } + + private ReadOnlyStyledDocument doc; + + private final EventSource> richChanges = new EventSource<>(); + @Override public EventStream> richChanges() { return richChanges; } + + private final Val text = Val.create(() -> doc.getText(), richChanges); + @Override public String getText() { return text.getValue(); } + @Override public Val textProperty() { return text; } + + + private final Val length = Val.create(() -> doc.length(), richChanges); + @Override public int getLength() { return length.getValue(); } + @Override public Val lengthProperty() { return length; } + @Override public int length() { return length.getValue(); } + + private final EventSource>> parChanges = + new EventSource<>(); + + private final LiveList> paragraphs = new ParagraphList(); + + @Override + public LiveList> getParagraphs() { + return paragraphs; + } + + @Override + public ReadOnlyStyledDocument snapshot() { + return doc; + } + + private final SuspendableNo beingUpdated = new SuspendableNo(); + @Override public final SuspendableNo beingUpdatedProperty() { return beingUpdated; } + @Override public final boolean isBeingUpdated() { return beingUpdated.get(); } + + GenericEditableStyledDocumentBase(Paragraph initialParagraph/*, SegmentOps segmentOps*/) { + this.doc = new ReadOnlyStyledDocument<>(Collections.singletonList(initialParagraph)); + } + + /** + * Creates an empty {@link EditableStyledDocument} + */ + public GenericEditableStyledDocumentBase(PS initialParagraphStyle, S initialStyle, TextOps segmentOps) { + this(new Paragraph<>(initialParagraphStyle, segmentOps, segmentOps.create("", initialStyle))); + } + + + @Override + public Position position(int major, int minor) { + return doc.position(major, minor); + } + + @Override + public Position offsetToPosition(int offset, Bias bias) { + return doc.offsetToPosition(offset, bias); + } + + @Override + public void replace(int start, int end, StyledDocument replacement) { + ensureValidRange(start, end); + doc.replace(start, end, ReadOnlyStyledDocument.from(replacement)).exec(this::update); + } + + @Override + public void setStyle(int from, int to, S style) { + ensureValidRange(from, to); + doc.replace(from, to, removed -> removed.mapParagraphs(par -> par.restyle(style))).exec(this::update); + } + + @Override + public void setStyle(int paragraph, S style) { + ensureValidParagraphIndex(paragraph); + doc.replaceParagraph(paragraph, p -> p.restyle(style)).exec(this::update); + } + + @Override + public void setStyle(int paragraph, int fromCol, int toCol, S style) { + ensureValidParagraphRange(paragraph, fromCol, toCol); + doc.replace( + new BiIndex(paragraph, fromCol), + new BiIndex(paragraph, toCol), + d -> d.mapParagraphs(p -> p.restyle(style)) + ).exec(this::update); + } + + @Override + public void setStyleSpans(int from, StyleSpans styleSpans) { + int len = styleSpans.length(); + ensureValidRange(from, from + len); + doc.replace(from, from + len, d -> { + Position i = styleSpans.position(0, 0); + List> pars = new ArrayList<>(d.getParagraphs().size()); + for(Paragraph p: d.getParagraphs()) { + Position j = i.offsetBy(p.length(), Backward); + StyleSpans spans = styleSpans.subView(i, j); + pars.add(p.restyle(0, spans)); + i = j.offsetBy(1, Forward); // skip the newline + } + return new ReadOnlyStyledDocument<>(pars); + }).exec(this::update); + } + + @Override + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + setStyleSpans(doc.position(paragraph, from).toOffset(), styleSpans); + } + + @Override + public void setParagraphStyle(int parIdx, PS style) { + ensureValidParagraphIndex(parIdx); + doc.replaceParagraph(parIdx, p -> p.setParagraphStyle(style)).exec(this::update); + } + + @Override + public StyledDocument concat(StyledDocument that) { + return doc.concat(that); + } + + @Override + public StyledDocument subSequence(int start, int end) { + return doc.subSequence(start, end); + } + + + /* ********************************************************************** * + * * + * Private and package private methods * + * * + * ********************************************************************** */ + + private void ensureValidParagraphIndex(int parIdx) { + Lists.checkIndex(parIdx, doc.getParagraphCount()); + } + + private void ensureValidRange(int start, int end) { + Lists.checkRange(start, end, length()); + } + + private void ensureValidParagraphRange(int par, int start, int end) { + ensureValidParagraphIndex(par); + Lists.checkRange(start, end, fullLength(par)); + } + + private int fullLength(int par) { + int n = doc.getParagraphCount(); + return doc.getParagraph(par).length() + (par == n-1 ? 0 : 1); + } + + private void update( + ReadOnlyStyledDocument newValue, + RichTextChange change, + MaterializedListModification> parChange) { + this.doc = newValue; + beingUpdated.suspendWhile(() -> { + richChanges.push(change); + parChanges.push(parChange); + }); + } +} 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 e281a77d5..1187caed0 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java @@ -84,20 +84,20 @@ public static ReadOnlyStyledDocument fromString(String String last = str.substring(start); res.add(new Paragraph<>(paragraphStyle, segmentOps, segmentOps.create(last, style))); - return new ReadOnlyStyledDocument<>(res, segmentOps); + 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); - return new ReadOnlyStyledDocument<>(res, segmentOps); + return new ReadOnlyStyledDocument<>(res); } public static ReadOnlyStyledDocument from(StyledDocument doc) { if(doc instanceof ReadOnlyStyledDocument) { return (ReadOnlyStyledDocument) doc; } else { - return new ReadOnlyStyledDocument<>(doc.getParagraphs(), doc.getSegOps()); + return new ReadOnlyStyledDocument<>(doc.getParagraphs()); } } @@ -118,7 +118,7 @@ public void encode(DataOutputStream os, StyledDocument doc) throws I @Override public StyledDocument decode(DataInputStream is) throws IOException { - return new ReadOnlyStyledDocument<>(codec.decode(is), segmentOps); + return new ReadOnlyStyledDocument<>(codec.decode(is)); } }; @@ -154,21 +154,15 @@ public Paragraph decode(DataInputStream is) throws IOException { private String text = null; private List> paragraphs = null; - private final SegmentOps segmentOps; - @Override - public final SegmentOps getSegOps() { return segmentOps; } - - private ReadOnlyStyledDocument(NonEmptyFingerTree, Summary> tree, SegmentOps segmentOps) { + private ReadOnlyStyledDocument(NonEmptyFingerTree, Summary> tree) { this.tree = tree; - this.segmentOps = segmentOps; } - ReadOnlyStyledDocument(List> paragraphs, SegmentOps segmentOps) { + ReadOnlyStyledDocument(List> paragraphs) { this.tree = FingerTree.mkTree(paragraphs, summaryProvider()).caseEmpty().unify( emptyTree -> { throw new AssertionError("Unreachable code"); }, neTree -> neTree); - this.segmentOps = segmentOps; } @Override @@ -222,8 +216,8 @@ public Tuple2, ReadOnlyStyledDocument { Paragraph p1 = p.trim(col); Paragraph p2 = p.subSequence(col); - ReadOnlyStyledDocument doc1 = new ReadOnlyStyledDocument<>(l.append(p1), segmentOps); - ReadOnlyStyledDocument doc2 = new ReadOnlyStyledDocument<>(r.prepend(p2), segmentOps); + ReadOnlyStyledDocument doc1 = new ReadOnlyStyledDocument<>(l.append(p1)); + ReadOnlyStyledDocument doc2 = new ReadOnlyStyledDocument<>(r.prepend(p2)); return t(doc1, doc2); }); } @@ -246,7 +240,7 @@ private ReadOnlyStyledDocument concat0(StyledDocument ot FingerTree, Summary> tree2 = (other instanceof ReadOnlyStyledDocument) ? ((ReadOnlyStyledDocument) other).tree.split(1)._2 : FingerTree.mkTree(other.getParagraphs().subList(1, other.getParagraphs().size()), summaryProvider()); - return new ReadOnlyStyledDocument<>(tree1.join(tree2), segmentOps); + return new ReadOnlyStyledDocument<>(tree1.join(tree2)); } @Override @@ -305,7 +299,7 @@ ReadOnlyStyledDocument mapParagraphs(UnaryOperator(pars, segmentOps); + return new ReadOnlyStyledDocument<>(pars); } @Override 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 2b1c718d3..8c0f72a8d 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java @@ -7,8 +7,8 @@ /** * Defines the operations which are supported on a specific segment type. * - * @param - * @param + * @param The segment type + * @param The style type for the segment */ public interface SegmentOps { public int length(SEG seg); 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 b5df8d4fe..968dc639a 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java @@ -1,217 +1,12 @@ package org.fxmisc.richtext.model; -import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.reactfx.EventSource; -import org.reactfx.EventStream; -import org.reactfx.Subscription; -import org.reactfx.SuspendableNo; -import org.reactfx.collection.LiveList; -import org.reactfx.collection.LiveListBase; -import org.reactfx.collection.MaterializedListModification; -import org.reactfx.collection.QuasiListModification; -import org.reactfx.collection.UnmodifiableByDefaultLiveList; -import org.reactfx.util.BiIndex; -import org.reactfx.util.Lists; -import org.reactfx.value.Val; - /** - * Provides an implementation of {@link EditableStyledDocument} + * Provides an implementation of {@link EditableStyledDocument} that is specified for {@link StyledText} as its segment. + * See also {@link GenericEditableStyledDocument}. */ -public final class SimpleEditableStyledDocument implements EditableStyledDocument { - - private class ParagraphList - extends LiveListBase> - implements UnmodifiableByDefaultLiveList> { - - @Override - public Paragraph get(int index) { - return doc.getParagraph(index); - } - - @Override - public int size() { - return doc.getParagraphCount(); - } - - @Override - protected Subscription observeInputs() { - return parChanges.subscribe(mod -> { - mod = mod.trim(); - QuasiListModification> qmod = - QuasiListModification.create(mod.getFrom(), mod.getRemoved(), mod.getAddedSize()); - notifyObservers(qmod.asListChange()); - }); - } - } - - private ReadOnlyStyledDocument doc; - - private final EventSource> richChanges = new EventSource<>(); - @Override public EventStream> richChanges() { return richChanges; } - - private final Val text = Val.create(() -> doc.getText(), richChanges); - @Override public String getText() { return text.getValue(); } - @Override public Val textProperty() { return text; } - - - private final Val length = Val.create(() -> doc.length(), richChanges); - @Override public int getLength() { return length.getValue(); } - @Override public Val lengthProperty() { return length; } - @Override public int length() { return length.getValue(); } - - private final EventSource>> parChanges = - new EventSource<>(); - - private final LiveList> paragraphs = new ParagraphList(); - - @Override - public LiveList> getParagraphs() { - return paragraphs; - } - - @Override - public ReadOnlyStyledDocument snapshot() { - return doc; - } - - private final SuspendableNo beingUpdated = new SuspendableNo(); - @Override public final SuspendableNo beingUpdatedProperty() { return beingUpdated; } - @Override public final boolean isBeingUpdated() { return beingUpdated.get(); } - - private final SegmentOps segmentOps; - @Override public final SegmentOps getSegOps() { return segmentOps; } - - SimpleEditableStyledDocument(Paragraph initialParagraph, SegmentOps segmentOps) { - this.doc = new ReadOnlyStyledDocument<>(Collections.singletonList(initialParagraph), segmentOps); - this.segmentOps = segmentOps; - } - - /** - * Creates an empty {@link EditableStyledDocument} - */ - public SimpleEditableStyledDocument(PS initialParagraphStyle, S initialStyle, TextOps segmentOps) { - this(new Paragraph<>(initialParagraphStyle, segmentOps, segmentOps.create("", initialStyle)), segmentOps); - } - - - @Override - public Position position(int major, int minor) { - return doc.position(major, minor); - } - - @Override - public Position offsetToPosition(int offset, Bias bias) { - return doc.offsetToPosition(offset, bias); - } - - @Override - public void replace(int start, int end, StyledDocument replacement) { - ensureValidRange(start, end); - doc.replace(start, end, ReadOnlyStyledDocument.from(replacement, segmentOps)).exec(this::update); - } - - @Override - public void setStyle(int from, int to, S style) { - ensureValidRange(from, to); - doc.replace(from, to, removed -> removed.mapParagraphs(par -> par.restyle(style))).exec(this::update); - } - - @Override - public void setStyle(int paragraph, S style) { - ensureValidParagraphIndex(paragraph); - doc.replaceParagraph(paragraph, p -> p.restyle(style)).exec(this::update); - } - - @Override - public void setStyle(int paragraph, int fromCol, int toCol, S style) { - ensureValidParagraphRange(paragraph, fromCol, toCol); - doc.replace( - new BiIndex(paragraph, fromCol), - new BiIndex(paragraph, toCol), - d -> d.mapParagraphs(p -> p.restyle(style)) - ).exec(this::update); - } - - @Override - public void setStyleSpans(int from, StyleSpans styleSpans) { - int len = styleSpans.length(); - ensureValidRange(from, from + len); - doc.replace(from, from + len, d -> { - Position i = styleSpans.position(0, 0); - List> pars = new ArrayList<>(d.getParagraphs().size()); - for(Paragraph p: d.getParagraphs()) { - Position j = i.offsetBy(p.length(), Backward); - StyleSpans spans = styleSpans.subView(i, j); - pars.add(p.restyle(0, spans)); - i = j.offsetBy(1, Forward); // skip the newline - } - return new ReadOnlyStyledDocument<>(pars, segmentOps); - }).exec(this::update); - } - - @Override - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { - setStyleSpans(doc.position(paragraph, from).toOffset(), styleSpans); - } - - @Override - public void setParagraphStyle(int parIdx, PS style) { - ensureValidParagraphIndex(parIdx); - doc.replaceParagraph(parIdx, p -> p.setParagraphStyle(style)).exec(this::update); - } - - @Override - public StyledDocument concat(StyledDocument that) { - return doc.concat(that); - } - - @Override - public StyledDocument subSequence(int start, int end) { - return doc.subSequence(start, end); - } - - @Override - public StyledDocument subDocument(int paragraphIndex) { - return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex)), segmentOps); - } - - /* ********************************************************************** * - * * - * Private and package private methods * - * * - * ********************************************************************** */ - - private void ensureValidParagraphIndex(int parIdx) { - Lists.checkIndex(parIdx, doc.getParagraphCount()); - } - - private void ensureValidRange(int start, int end) { - Lists.checkRange(start, end, length()); - } - - private void ensureValidParagraphRange(int par, int start, int end) { - ensureValidParagraphIndex(par); - Lists.checkRange(start, end, fullLength(par)); - } - - private int fullLength(int par) { - int n = doc.getParagraphCount(); - return doc.getParagraph(par).length() + (par == n-1 ? 0 : 1); - } +public final class SimpleEditableStyledDocument extends GenericEditableStyledDocumentBase, S> { - private void update( - ReadOnlyStyledDocument newValue, - RichTextChange change, - MaterializedListModification> parChange) { - this.doc = newValue; - beingUpdated.suspendWhile(() -> { - richChanges.push(change); - parChanges.push(parChange); - }); + public SimpleEditableStyledDocument(PS initialParagraphStyle, S initialStyle) { + super(initialParagraphStyle, initialStyle, StyledText.textOps()); } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java index e20baf236..436188597 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java @@ -19,8 +19,6 @@ public interface StyledDocument extends TwoDimensional { StyledDocument subSequence(int start, int end); - SegmentOps getSegOps(); - default String getText(IndexRange range) { return getText(range.getStart(), range.getEnd()); } @@ -34,7 +32,7 @@ default StyledDocument subSequence(IndexRange range) { } default StyledDocument subDocument(int paragraphIndex) { - return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex)), getSegOps()); + return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex))); } default char charAt(int index) { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index a173d43c1..c5fd6892f 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -180,8 +180,6 @@ private static int clamp(int min, int val, int max) { private Position selectionStart2D; private Position selectionEnd2D; - private final SegmentOps segmentOps; - /** * content model */ @@ -236,7 +234,7 @@ public StyledTextAreaModel(PS initialParagraphStyle, S initialTextStyle, TextOps public StyledTextAreaModel(PS initialParagraphStyle, S initialTextStyle, TextOps segmentOps, boolean preserveStyle ) { this(initialParagraphStyle, initialTextStyle, - new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), + new GenericEditableStyledDocumentBase<>(initialParagraphStyle, initialTextStyle, segmentOps), segmentOps, preserveStyle); } @@ -262,7 +260,6 @@ public StyledTextAreaModel( this.initialTextStyle = initialTextStyle; this.initialParagraphStyle = initialParagraphStyle; this.preserveStyle = preserveStyle; - this.segmentOps = segmentOps; content = document; paragraphs = LiveList.suspendable(content.getParagraphs()); @@ -574,7 +571,7 @@ public void setStyle(int paragraph, int from, int to, S style) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int from, StyleSpans styleSpans) { + public void setStyleSpans(int from, StyleSpans styleSpans) { content.setStyleSpans(from, styleSpans); } @@ -588,7 +585,7 @@ public void setStyleSpans(int from, StyleSpans styleSpans) { * * but the actual implementation is more efficient. */ - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { + public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { content.setStyleSpans(paragraph, from, styleSpans); } 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 ee10f244e..888c782e0 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java @@ -50,7 +50,7 @@ public void testRestyle() { final String helloWorld = "Hello World"; TextOps, String> segOps = StyledText.textOps(); - SimpleEditableStyledDocument, String> doc0 = new SimpleEditableStyledDocument<>("", "", segOps); + SimpleEditableStyledDocument doc0 = new SimpleEditableStyledDocument<>("", ""); ReadOnlyStyledDocument text = fromString(fooBar, "", "bold"); doc0.replace(doc0.getLength(), doc0.getLength(), text); 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 a15cce014..8c41c9479 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java @@ -1,29 +1,26 @@ package org.fxmisc.richtext.model; import static org.junit.Assert.*; - import org.junit.Test; public class SimpleEditableStyledDocumentTest { + private final TextOps, String> segOps = StyledText.textOps(); /** * The style of the inserted text will be the style at position * {@code start} in the current document. * @param ops */ - private void replaceText(EditableStyledDocument doc, int start, int end, String text, TextOps segOps) { - StyledDocument styledDoc = ReadOnlyStyledDocument.fromString( + private void replaceText(EditableStyledDocument, String> doc, int start, int end, String text) { + StyledDocument, String> styledDoc = ReadOnlyStyledDocument.fromString( text, doc.getParagraphStyleAtPosition(start), doc.getStyleAtPosition(start), segOps); doc.replace(start, end, styledDoc); } - - private final TextOps, String> segOps = StyledText.textOps(); - @Test public void testConsistencyOfTextWithLength() { - SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); + SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); document.getText(); // enforce evaluation of text property document.getLength(); // enforce evaluation of length property @@ -33,12 +30,12 @@ public void testConsistencyOfTextWithLength() { assertEquals(length, textLength); }); - replaceText(document, 0, 0, "A", segOps); + replaceText(document, 0, 0, "A"); } @Test public void testConsistencyOfLengthWithText() { - SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); + SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); document.getText(); // enforce evaluation of text property document.getLength(); // enforce evaluation of length property @@ -48,54 +45,54 @@ public void testConsistencyOfLengthWithText() { assertEquals(textLength, length); }); - replaceText(document, 0, 0, "A", segOps); + replaceText(document, 0, 0, "A"); } @Test public void testUnixParagraphCount() { - SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); + SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); String text = "X\nY"; - replaceText(document, 0, 0, text, segOps); + replaceText(document, 0, 0, text); assertEquals(2, document.getParagraphs().size()); } @Test public void testMacParagraphCount() { - SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); + SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); String text = "X\rY"; - replaceText(document, 0, 0, text, segOps); + replaceText(document, 0, 0, text); assertEquals(2, document.getParagraphs().size()); } @Test public void testWinParagraphCount() { - SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); + SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); String text = "X\r\nY"; - replaceText(document, 0, 0, text, segOps); + replaceText(document, 0, 0, text); assertEquals(2, document.getParagraphs().size()); } @Test public void testGetTextWithEndAfterNewline() { - SimpleEditableStyledDocument, String> doc = new SimpleEditableStyledDocument<>(true, "", segOps); + SimpleEditableStyledDocument doc = new SimpleEditableStyledDocument<>(true, ""); - replaceText(doc, 0, 0, "123\n", segOps); + replaceText(doc, 0, 0, "123\n"); String txt1 = doc.getText(0, 4); assertEquals(4, txt1.length()); - replaceText(doc, 4, 4, "567", segOps); + replaceText(doc, 4, 4, "567"); String txt2 = doc.getText(2, 4); assertEquals(2, txt2.length()); - replaceText(doc, 4, 4, "\n", segOps); + replaceText(doc, 4, 4, "\n"); String txt3 = doc.getText(2, 4); assertEquals(2, txt3.length()); } @Test public void testWinDocumentLength() { - SimpleEditableStyledDocument, String> document = new SimpleEditableStyledDocument<>("", "", segOps); - replaceText(document, 0, 0, "X\r\nY", segOps); + SimpleEditableStyledDocument document = new SimpleEditableStyledDocument<>("", ""); + replaceText(document, 0, 0, "X\r\nY"); assertEquals(document.getText().length(), document.getLength()); } } diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java index c5c069640..b8df9131d 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java @@ -9,7 +9,6 @@ public class StyledTextAreaModelTest { - @Test public void testUndoWithWinNewlines() { final TextOps>, Collection> segOps = StyledText.textOps(); @@ -38,7 +37,7 @@ public void testForBug216() { // set up area with some styled text content boolean initialStyle = false; StyledTextAreaModel, Boolean> model = new StyledTextAreaModel<>( - "", initialStyle, new SimpleEditableStyledDocument<>("", initialStyle, segOps), segOps, true); + "", initialStyle, new SimpleEditableStyledDocument<>("", initialStyle), segOps, true); model.replaceText("testtest"); model.setStyle(0, 8, true); From de10a1b7f15e7b78ee4de49e4570cf3d26eb8933 Mon Sep 17 00:00:00 2001 From: Andreas Fester Date: Tue, 25 Oct 2016 14:59:06 +0200 Subject: [PATCH 4/9] Fixed duplication of images which are inserted at the end of a paragraph --- .../demo/richtext/LinkedImageOps.java | 22 +- .../richtext/demo/richtext/RichText.java | 6 +- .../fxmisc/richtext/GenericStyledArea.java | 2 +- .../org/fxmisc/richtext/ParagraphBox.java | 2 +- .../org/fxmisc/richtext/StyledTextArea.java | 4 +- .../fxmisc/richtext/model/EmptyParagraph.java | 131 +++++++ .../GenericEditableStyledDocumentBase.java | 2 +- .../richtext/model/NonEmptyParagraph.java | 350 ++++++++++++++++++ .../org/fxmisc/richtext/model/Paragraph.java | 304 ++------------- .../model/ReadOnlyStyledDocument.java | 8 +- .../org/fxmisc/richtext/model/SegmentOps.java | 48 ++- .../model/SimpleEditableStyledDocument.java | 2 +- .../org/fxmisc/richtext/model/StyledText.java | 15 +- .../org/fxmisc/richtext/model/TextOps.java | 22 +- .../fxmisc/richtext/model/ParagraphTest.java | 8 +- .../model/ReadOnlyStyledDocumentTest.java | 12 +- .../SimpleEditableStyledDocumentTest.java | 2 +- .../model/StyledTextAreaModelTest.java | 4 +- 18 files changed, 606 insertions(+), 338 deletions(-) create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/EmptyParagraph.java create mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/NonEmptyParagraph.java 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 a8c7869f9..a43a7b7cc 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 @@ -6,6 +6,12 @@ public class LinkedImageOps implements SegmentOps, S> { + private final S defaultStyle; + + public LinkedImageOps(S defaultStyle) { + this.defaultStyle = defaultStyle; + } + @Override public int length(LinkedImage seg) { return 1; @@ -22,13 +28,21 @@ public String getText(LinkedImage seg) { } @Override - public LinkedImage subSequence(LinkedImage seg, int start, int end) { - return seg; + public Optional> subSequence(LinkedImage linkedImage, int start, int end) { + return start < length(linkedImage) && end > 0 + ? Optional.of(linkedImage) + : Optional.empty(); } @Override - public LinkedImage subSequence(LinkedImage seg, int start) { - return seg; + public Optional> subSequence(LinkedImage linkedImage, int start) { + return start < length(linkedImage) + ? Optional.of(linkedImage) + : Optional.empty(); + } + + public S defaultStyle() { + return defaultStyle; } @Override 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 7341ec050..e5e6a03da 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 @@ -66,8 +66,8 @@ public static void main(String[] args) { launch(args); } - private final TextOps, TextStyle> styledTextOps = StyledText.textOps(); - private final LinkedImageOps linkedImageOps = new LinkedImageOps<>(); + private final TextOps, TextStyle> styledTextOps = StyledText.textOps(TextStyle.EMPTY); + private final LinkedImageOps linkedImageOps = new LinkedImageOps<>(TextStyle.EMPTY); private final GenericStyledArea, LinkedImage>, TextStyle> area = new GenericStyledArea<>( @@ -120,7 +120,7 @@ public void start(Stage primaryStage) { ColorPicker textColorPicker = new ColorPicker(Color.BLACK); ColorPicker backgroundColorPicker = new ColorPicker(); - paragraphBackgroundPicker.setTooltip(new Tooltip("Paragraph background")); + paragraphBackgroundPicker.setTooltip(new Tooltip("NonEmptyParagraph background")); textColorPicker.setTooltip(new Tooltip("Text color")); backgroundColorPicker.setTooltip(new Tooltip("Text background")); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java index 881160159..47a6ffdd2 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledArea.java @@ -57,9 +57,9 @@ import org.fxmisc.richtext.model.EditActions; import org.fxmisc.richtext.model.EditableStyledDocument; import org.fxmisc.richtext.model.GenericEditableStyledDocument; +import org.fxmisc.richtext.model.Paragraph; import org.fxmisc.richtext.model.StyledTextAreaModel; import org.fxmisc.richtext.model.NavigationActions; -import org.fxmisc.richtext.model.Paragraph; import org.fxmisc.richtext.model.PlainTextChange; import org.fxmisc.richtext.model.RichTextChange; import org.fxmisc.richtext.model.SegmentOps; diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java index 0ab625cec..b7ed80c32 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphBox.java @@ -71,7 +71,7 @@ public ObjectProperty> graphicFactoryProperty() { public void setIndex(int index) { this.index.setValue(index); } public int getIndex() { return index.getValue(); } - ParagraphBox(Paragraph par, BiConsumer applyParagraphStyle, + ParagraphBox(Paragraph par, BiConsumer applyParagraphStyle, Function nodeFactory) { this.getStyleClass().add("paragraph-box"); this.text = new ParagraphText<>(par, nodeFactory); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java index 3be3366a6..726314ad4 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java @@ -66,8 +66,8 @@ public StyledTextArea(PS initialParagraphStyle, BiConsumer applyPa EditableStyledDocument, S> document, boolean preserveStyle) { super(initialParagraphStyle, applyParagraphStyle, initialTextStyle, - document, StyledText.textOps(), preserveStyle, - seg -> createStyledTextNode(seg, StyledText.textOps(), applyStyle)); + document, StyledText.textOps(initialTextStyle), preserveStyle, + seg -> createStyledTextNode(seg, StyledText.textOps(initialTextStyle), applyStyle)); } public StyledTextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/EmptyParagraph.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/EmptyParagraph.java new file mode 100644 index 000000000..41a1f7b12 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/EmptyParagraph.java @@ -0,0 +1,131 @@ +package org.fxmisc.richtext.model; + +import javafx.scene.control.IndexRange; + +import java.util.Collections; +import java.util.List; + +public final class EmptyParagraph implements Paragraph { + + private static final IndexRange EMPTY = new IndexRange(0, 0); + + private final PS parStyle; + private final S textStyle; + + public EmptyParagraph(PS parStyle, S textStyle) { + this.parStyle = parStyle; + this.textStyle = textStyle; + } + + @Override + public List getSegments() { + return Collections.emptyList(); + } + + @Override + public PS getParagraphStyle() { + return parStyle; + } + + @Override + public int length() { + return 0; + } + + @Override + public char charAt(int index) { + return 0; + } + + @Override + public String substring(int from, int to) { + return ""; + } + + @Override + public String substring(int from) { + return ""; + } + + @Override + public Paragraph concat(Paragraph p) { + return p; + } + + @Override + public Paragraph concatR(Paragraph that) { + return that; + } + + @Override + public Paragraph subSequence(int start, int end) { + return this; + } + + @Override + public Paragraph trim(int length) { + return this; + } + + @Override + public Paragraph subSequence(int start) { + return this; + } + + @Override + public Paragraph delete(int start, int end) { + return this; + } + + @Override + public Paragraph restyle(S style) { + return new EmptyParagraph<>(parStyle, style); + } + + @Override + public Paragraph restyle(int from, int to, S style) { + return restyle(style); + } + + @Override + public Paragraph restyle(int from, StyleSpans styleSpans) { + return this; + } + + @Override + public Paragraph setParagraphStyle(PS paragraphStyle) { + return new EmptyParagraph<>(paragraphStyle, textStyle); + } + + @Override + public S getStyleOfChar(int charIdx) { + return textStyle; + } + + @Override + public S getStyleAtPosition(int position) { + return textStyle; + } + + @Override + public IndexRange getStyleRangeAtPosition(int position) { + return EMPTY; + } + + @Override + public StyleSpans getStyleSpans() { + StyleSpansBuilder b = new StyleSpansBuilder<>(); + b.add(textStyle, 0); + return b.create(); + } + + @Override + public StyleSpans getStyleSpans(int from, int to) { + return getStyleSpans(); + } + + @Override + public String getText() { + return ""; + } +} 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 2260b8105..2c3a8c3ed 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/GenericEditableStyledDocumentBase.java @@ -91,7 +91,7 @@ public ReadOnlyStyledDocument snapshot() { * Creates an empty {@link EditableStyledDocument} */ public GenericEditableStyledDocumentBase(PS initialParagraphStyle, S initialStyle, TextOps segmentOps) { - this(new Paragraph<>(initialParagraphStyle, segmentOps, segmentOps.create("", initialStyle))); + this(new NonEmptyParagraph<>(initialParagraphStyle, segmentOps, segmentOps.create("", initialStyle))); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/NonEmptyParagraph.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/NonEmptyParagraph.java new file mode 100644 index 000000000..ffb5fd928 --- /dev/null +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/NonEmptyParagraph.java @@ -0,0 +1,350 @@ +package org.fxmisc.richtext.model; + +import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javafx.scene.control.IndexRange; + +import org.fxmisc.richtext.model.TwoDimensional.Position; + +public final class NonEmptyParagraph implements Paragraph { + + @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); + Collections.addAll(list, tail); + return list; + } + } + + private final List segments; + private final TwoLevelNavigator navigator; + private final PS paragraphStyle; + + private final SegmentOps segmentOps; + + @SafeVarargs + public NonEmptyParagraph(PS paragraphStyle, SegmentOps segmentOps, SEG text, SEG... texts) { + this(paragraphStyle, segmentOps, list(text, texts)); + } + + NonEmptyParagraph(PS paragraphStyle, SegmentOps segmentOps, List segments) { + assert !segments.isEmpty(); + + this.segmentOps = segmentOps; + this.segments = segments; + this.paragraphStyle = paragraphStyle; + navigator = new TwoLevelNavigator(segments::size, + i -> segmentOps.length(segments.get(i))); + } + + public List getSegments() { + return Collections.unmodifiableList(segments); + } + + public PS getParagraphStyle() { + return paragraphStyle; + } + + private int length = -1; + public int length() { + if(length == -1) { + length = segments.stream().mapToInt(segmentOps::length).sum(); + } + return length; + } + + public char charAt(int index) { + Position pos = navigator.offsetToPosition(index, Forward); + return segmentOps.charAt(segments.get(pos.getMajor()), pos.getMinor()); + } + + public String substring(int from, int to) { + return getText().substring(from, Math.min(to, length())); + } + + public String substring(int from) { + return getText().substring(from); + } + + /** + * Concatenates this paragraph with the given paragraph {@code p}. + * The paragraph style of the result will be that of this paragraph, + * unless this paragraph is empty and {@code p} is non-empty, in which + * case the paragraph style of the result will be that of {@code p}. + */ + public Paragraph concat(Paragraph p) { + if(p.length() == 0) { + return this; + } + + if(length() == 0) { + return p; + } + + SEG left = segments.get(segments.size() - 1); + SEG right = p.getSegments().get(0); + Optional joined = segmentOps.join(left, right); + if(joined.isPresent()) { + SEG segment = joined.get(); + List segs = new ArrayList<>(segments.size() + p.getSegments().size() - 1); + segs.addAll(segments.subList(0, segments.size()-1)); + segs.add(segment); + segs.addAll(p.getSegments().subList(1, p.getSegments().size())); + return new NonEmptyParagraph<>(paragraphStyle, segmentOps, segs); + } else { + List segs = new ArrayList<>(segments.size() + p.getSegments().size()); + segs.addAll(segments); + segs.addAll(p.getSegments()); + return new NonEmptyParagraph<>(paragraphStyle, segmentOps, segs); + } + } + + /** + * Similar to {@link #concat(Paragraph)}, except in case both paragraphs + * are empty, the result's paragraph style will be that of the argument. + */ + public Paragraph concatR(Paragraph that) { + return this.length() == 0 && that.length() == 0 + ? that + : concat(that); + } + + 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)); + segmentOps.subSequence(segments.get(segIdx), 0, pos.getMinor()) + .ifPresent(segs::add); + return segs.isEmpty() + ? new EmptyParagraph<>(paragraphStyle, segmentOps.defaultStyle()) + : new NonEmptyParagraph<>(paragraphStyle, segmentOps, 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); + segmentOps.subSequence(segments.get(segIdx), pos.getMinor()) + .ifPresent(segs::add); + segs.addAll(segments.subList(segIdx + 1, segments.size())); + return segs.isEmpty() + ? new EmptyParagraph<>(paragraphStyle, segmentOps.defaultStyle()) + : new NonEmptyParagraph<>(paragraphStyle, segmentOps, 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) { + 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 NonEmptyParagraph<>(paragraphStyle, segmentOps, segs); + } + + 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); + } + } + + 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); + + 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()).getSegments()); + offset = end; + } + Paragraph newMiddle = new NonEmptyParagraph<>(paragraphStyle, segmentOps, middleSegs); + + return left.concat(newMiddle).concat(right); + } + + public Paragraph setParagraphStyle(PS paragraphStyle) { + return new NonEmptyParagraph<>(paragraphStyle, segmentOps, segments); + } + + /** + * 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 segmentOps.getStyle(segments.get(0)); + } + + Position pos = navigator.offsetToPosition(charIdx, Forward); + return segmentOps.getStyle(segments.get(pos.getMajor())); + } + + /** + * 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 segmentOps.getStyle(segments.get(pos.getMajor())); + } + + /** + * 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 + segmentOps.length(segments.get(pos.getMajor())); + 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(); + } + + 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(); + } + + private String text = null; + /** + * Returns the plain text content of this paragraph, + * not including the line terminator. + */ + public String getText() { + if(text == null) { + StringBuilder sb = new StringBuilder(length()); + for(SEG seg: segments) + sb.append(segmentOps.getText(seg)); + text = sb.toString(); + } + return text; + } + + @Override + public String toString() { + return + "Par[" + paragraphStyle + "; " + + segments.stream().map(Object::toString) + .reduce((s1, s2) -> s1 + "," + s2).orElse("") + + "]"; + } + + @Override + public boolean equals(Object other) { + if(other instanceof NonEmptyParagraph) { + NonEmptyParagraph that = (NonEmptyParagraph) other; + return Objects.equals(this.paragraphStyle, that.paragraphStyle) + && Objects.equals(this.getSegments(), that.getSegments()); + } else { + return false; + } + } + + @Override + public int hashCode() { + return Objects.hash(paragraphStyle, segments); + } + +} 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 989f4bd0c..d51a632db 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java @@ -1,81 +1,22 @@ package org.fxmisc.richtext.model; -import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - import javafx.scene.control.IndexRange; -import org.fxmisc.richtext.model.TwoDimensional.Position; - -public final class Paragraph { - - @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); - Collections.addAll(list, tail); - return list; - } - } - - private final List segments; - 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)); - } - - Paragraph(PS paragraphStyle, SegmentOps segmentOps, List segments) { - assert !segments.isEmpty(); +import java.util.List; - this.segmentOps = segmentOps; - this.segments = segments; - this.paragraphStyle = paragraphStyle; - navigator = new TwoLevelNavigator(segments::size, - i -> segmentOps.length(segments.get(i))); - } +public interface Paragraph { - public List getSegments() { - return Collections.unmodifiableList(segments); - } + public List getSegments(); - public PS getParagraphStyle() { - return paragraphStyle; - } + public PS getParagraphStyle(); - private int length = -1; - public int length() { - if(length == -1) { - length = segments.stream().mapToInt(segmentOps::length).sum(); - } - return length; - } + public int length(); - public char charAt(int index) { - Position pos = navigator.offsetToPosition(index, Forward); - return segmentOps.charAt(segments.get(pos.getMajor()), pos.getMinor()); - } + public char charAt(int index); - public String substring(int from, int to) { - return getText().substring(from, Math.min(to, length())); - } + public String substring(int from, int to); - public String substring(int from) { - return getText().substring(from); - } + public String substring(int from); /** * Concatenates this paragraph with the given paragraph {@code p}. @@ -83,150 +24,36 @@ public String substring(int from) { * unless this paragraph is empty and {@code p} is non-empty, in which * case the paragraph style of the result will be that of {@code p}. */ - public Paragraph concat(Paragraph p) { - if(p.length() == 0) { - return this; - } - - if(length() == 0) { - return p; - } - - SEG left = segments.get(segments.size() - 1); - SEG right = p.segments.get(0); - Optional joined = segmentOps.join(left, right); - 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); - } else { - List segs = new ArrayList<>(segments.size() + p.segments.size()); - segs.addAll(segments); - segs.addAll(p.segments); - return new Paragraph<>(paragraphStyle, segmentOps, segs); - } - } + public Paragraph concat(Paragraph p); /** * Similar to {@link #concat(Paragraph)}, except in case both paragraphs * are empty, the result's paragraph style will be that of the argument. */ - Paragraph concatR(Paragraph that) { - return this.length() == 0 && that.length() == 0 - ? that - : concat(that); - } - - public Paragraph subSequence(int start, int end) { - return trim(end).subSequence(start); - } + Paragraph concatR(Paragraph that); - 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(segmentOps.subSequence(segments.get(segIdx), 0, pos.getMinor())); - return new Paragraph<>(paragraphStyle, segmentOps, segs); - } - } + public Paragraph subSequence(int start, int end); - 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(segmentOps.subSequence(segments.get(segIdx), pos.getMinor())); - segs.addAll(segments.subList(segIdx + 1, segments.size())); - return new Paragraph<>(paragraphStyle, segmentOps, segs); - } else { - throw new IndexOutOfBoundsException(start + " not in [0, " + length() + "]"); - } - } + public Paragraph trim(int length); - public Paragraph delete(int start, int end) { - return trim(start).concat(subSequence(end)); - } + public Paragraph subSequence(int start); - 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); - } + public Paragraph delete(int start, int end); - 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); - } - } + public Paragraph restyle(S style); - public Paragraph restyle(int from, StyleSpans styleSpans) { - int len = styleSpans.length(); - if(styleSpans.equals(getStyleSpans(from, from + len))) { - return this; - } + public Paragraph restyle(int from, int to, S style); - Paragraph left = trim(from); - Paragraph right = subSequence(from + len); + public Paragraph restyle(int from, StyleSpans styleSpans); - 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); - - return left.concat(newMiddle).concat(right); - } - - public Paragraph setParagraphStyle(PS paragraphStyle) { - return new Paragraph<>(paragraphStyle, segmentOps, segments); - } + public Paragraph setParagraphStyle(PS paragraphStyle); /** * 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 segmentOps.getStyle(segments.get(0)); - } - - Position pos = navigator.offsetToPosition(charIdx, Forward); - return segmentOps.getStyle(segments.get(pos.getMajor())); - } + public S getStyleOfChar(int charIdx); /** * Returns the style at the given position. That is the style of the @@ -240,105 +67,22 @@ public S getStyleOfChar(int charIdx) { *

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 segmentOps.getStyle(segments.get(pos.getMajor())); - } + public S getStyleAtPosition(int position); /** * 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 + segmentOps.length(segments.get(pos.getMajor())); - 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(); - } - - 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); + public IndexRange getStyleRangeAtPosition(int position); - 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()); + public StyleSpans getStyleSpans(); - for(int i = startSegIdx + 1; i < endSegIdx; ++i) { - SEG seg = segments.get(i); - builder.add(segmentOps.getStyle(seg), - segmentOps.length(seg)); - } + public StyleSpans getStyleSpans(int from, int to); - SEG endSeg = segments.get(endSegIdx); - builder.add(segmentOps.getStyle(endSeg), end.getMinor()); - } - - return builder.create(); - } - - private String text = null; /** * Returns the plain text content of this paragraph, * not including the line terminator. */ - public String getText() { - if(text == null) { - StringBuilder sb = new StringBuilder(length()); - for(SEG seg: segments) - sb.append(segmentOps.getText(seg)); - text = sb.toString(); - } - return text; - } - - @Override - public String toString() { - return - "Par[" + paragraphStyle + "; " + - segments.stream().map(Object::toString) - .reduce((s1, s2) -> s1 + "," + s2).orElse("") + - "]"; - } - - @Override - 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); - } else { - return false; - } - } - - @Override - public int hashCode() { - return Objects.hash(paragraphStyle, segments); - } - + public String getText(); } 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 1187caed0..c1cceb00c 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java @@ -78,17 +78,17 @@ 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 NonEmptyParagraph<>(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 NonEmptyParagraph<>(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)); + Paragraph content = new NonEmptyParagraph<>(paragraphStyle, segmentOps, Arrays.asList(segment)); List> res = Arrays.asList(content); return new ReadOnlyStyledDocument<>(res); } @@ -143,7 +143,7 @@ public void encode(DataOutputStream os, Paragraph p) throws IOExcept public Paragraph decode(DataInputStream is) throws IOException { PS paragraphStyle = pCodec.decode(is); List segments = segmentsCodec.decode(is); - return new Paragraph<>(paragraphStyle, segmentOps, segments); + return new NonEmptyParagraph<>(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 8c0f72a8d..8fc442ff0 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java @@ -17,9 +17,11 @@ public interface SegmentOps { public String getText(SEG seg); - public SEG subSequence(SEG seg, int start, int end); + public Optional subSequence(SEG seg, int start, int end); - public SEG subSequence(SEG seg, int start); + public Optional subSequence(SEG seg, int start); + + public S defaultStyle(); public S getStyle(SEG seg); @@ -28,26 +30,33 @@ public interface SegmentOps { public Optional join(SEG currentSeg, SEG nextSeg); public default SegmentOps, S> or(SegmentOps rOps) { - return either(this, rOps); + return either(this, rOps, EitherSegmentOps.StyleChoice.LEFT); } - public default TextOps, S> or_(TextOps rOps) { - return TextOps.eitherR(this, rOps); + public default TextOps, S> or_(TextOps rOps, EitherSegmentOps.StyleChoice choice) { + return TextOps.eitherR(this, rOps, choice); } - public static SegmentOps, S> either(SegmentOps lOps, SegmentOps rOps) { - return new EitherSegmentOps(lOps, rOps); + public static SegmentOps, S> either(SegmentOps lOps, SegmentOps rOps, EitherSegmentOps.StyleChoice choice) { + return new EitherSegmentOps<>(lOps, rOps, choice); } } class EitherSegmentOps implements SegmentOps, S> { + public static enum StyleChoice { + LEFT, + RIGHT + } + private final SegmentOps lOps; private final SegmentOps rOps; + private final StyleChoice choice; - EitherSegmentOps(SegmentOps lOps, SegmentOps rOps) { + EitherSegmentOps(SegmentOps lOps, SegmentOps rOps, StyleChoice choice) { this.lOps = lOps; this.rOps = rOps; + this.choice = choice; } @@ -68,15 +77,26 @@ public String getText(Either seg) { } @Override - public Either subSequence(Either seg, int start, int end) { - return seg.map(l -> lOps.subSequence(l, start, end), - r -> rOps.subSequence(r, start, end)); + public Optional> subSequence(Either seg, int start, int end) { + return seg.unify(ll -> seg.unify(l -> lOps.subSequence(l, start, end).map(Either::left), + e -> Optional.empty()), + rr -> seg.unify(e -> Optional.empty(), + r -> rOps.subSequence(r, start, end).map(Either::right))); + } + + @Override + public Optional> subSequence(Either seg, int start) { + return seg.unify(ll -> seg.unify(l -> lOps.subSequence(l, start).map(Either::left), + e -> Optional.empty()), + rr -> seg.unify(e -> Optional.empty(), + r -> rOps.subSequence(r, start).map(Either::right))); } @Override - public Either subSequence(Either seg, int start) { - return seg.map(l -> lOps.subSequence(l, start), - r -> rOps.subSequence(r, start)); + public S defaultStyle() { + return choice == StyleChoice.LEFT + ? lOps.defaultStyle() + : rOps.defaultStyle(); } @Override 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..a8c3eb61b 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SimpleEditableStyledDocument.java @@ -7,6 +7,6 @@ public final class SimpleEditableStyledDocument extends GenericEditableStyledDocumentBase, S> { public SimpleEditableStyledDocument(PS initialParagraphStyle, S initialStyle) { - super(initialParagraphStyle, initialStyle, StyledText.textOps()); + super(initialParagraphStyle, initialStyle, StyledText.textOps(initialStyle)); } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java index 600d32a69..e12e077f8 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java @@ -8,7 +8,7 @@ public class StyledText { - public static TextOps, S> textOps() { + public static TextOps, S> textOps(S defaultStyle) { return new TextOps, S>() { @Override @@ -27,13 +27,18 @@ public String getText(StyledText styledText) { } @Override - public StyledText subSequence(StyledText styledText, int start, int end) { - return new StyledText<>(styledText.getText().substring(start, end), styledText.getStyle()); + public Optional> subSequence(StyledText styledText, int start, int end) { + return Optional.of(new StyledText<>(styledText.getText().substring(start, end), styledText.getStyle())); } @Override - public StyledText subSequence(StyledText styledText, int start) { - return new StyledText<>(styledText.getText().substring(start), styledText.getStyle()); + public Optional> subSequence(StyledText styledText, int start) { + return Optional.of(new StyledText<>(styledText.getText().substring(start), styledText.getStyle())); + } + + @Override + public S defaultStyle() { + return defaultStyle; } @Override 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 fb00fc0b9..e0bd43eed 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java @@ -6,15 +6,19 @@ public interface TextOps extends SegmentOps { public SEG create(String text, S style); public default TextOps, S> _or(SegmentOps rOps) { - return eitherL(this, rOps); + return _or(rOps, EitherSegmentOps.StyleChoice.LEFT); } - public static TextOps, S> eitherL(TextOps lOps, SegmentOps rOps) { - return new LeftTextOps<>(lOps, rOps); + public default TextOps, S> _or(SegmentOps rOps, EitherSegmentOps.StyleChoice choice) { + return eitherL(this, rOps, choice); } - public static TextOps, S> eitherR(SegmentOps lOps, TextOps rOps) { - return new RightTextOps<>(lOps, rOps); + public static TextOps, S> eitherL(TextOps lOps, SegmentOps rOps, EitherSegmentOps.StyleChoice choice) { + return new LeftTextOps<>(lOps, rOps, choice); + } + + public static TextOps, S> eitherR(SegmentOps lOps, TextOps rOps, EitherSegmentOps.StyleChoice choice) { + return new RightTextOps<>(lOps, rOps, choice); } } @@ -22,8 +26,8 @@ class LeftTextOps extends EitherSegmentOps implements TextOps< private final TextOps lOps; - LeftTextOps(TextOps lOps, SegmentOps rOps) { - super(lOps, rOps); + LeftTextOps(TextOps lOps, SegmentOps rOps, StyleChoice choice) { + super(lOps, rOps, choice); this.lOps = lOps; } @@ -38,8 +42,8 @@ class RightTextOps extends EitherSegmentOps implements TextOps private final TextOps rOps; - RightTextOps(SegmentOps lOps, TextOps rOps) { - super(lOps, rOps); + RightTextOps(SegmentOps lOps, TextOps rOps, StyleChoice choice) { + super(lOps, rOps, choice); this.rOps = rOps; } 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 d7ff9ec32..6dd52326a 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ParagraphTest.java @@ -1,6 +1,6 @@ package org.fxmisc.richtext.model; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; import org.junit.Test; @@ -11,9 +11,9 @@ 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, Boolean> segOps = StyledText.textOps(false); + Paragraph, Boolean> p1 = new NonEmptyParagraph<>(null, segOps, segOps.create("", true)); + Paragraph, Boolean> p2 = new NonEmptyParagraph<>(null, segOps, segOps.create("", false)); Paragraph, Boolean> p = p1.concat(p2); 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 888c782e0..318961093 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java @@ -13,7 +13,7 @@ public class ReadOnlyStyledDocumentTest { @Test public void testUndo() { - TextOps, String> segOps = StyledText.textOps(); + TextOps, String> segOps = StyledText.textOps(""); ReadOnlyStyledDocument, String> doc0 = fromString("", "X", "X", segOps); doc0.replace(0, 0, fromString("abcd", "Y", "Y", segOps)).exec((doc1, chng1, pchng1) -> { @@ -30,16 +30,16 @@ public void testUndo() { @Test public void deleteNewlineTest() { - TextOps, Void> segOps = StyledText.textOps(); + TextOps, Void> segOps = StyledText.textOps(null); 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(); 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)); + assertEquals(new NonEmptyParagraph, Void>(null, segOps, segOps.create("Foo", null)), removed.get(0)); + assertEquals(new NonEmptyParagraph, Void>(null, segOps, segOps.create("Bar", null)), removed.get(1)); assertEquals(1, added.size()); - assertEquals(new Paragraph, Void>(null, segOps, segOps.create("FooBar", null)), added.get(0)); + assertEquals(new NonEmptyParagraph, Void>(null, segOps, segOps.create("FooBar", null)), added.get(0)); }); } @@ -48,7 +48,7 @@ public void testRestyle() { final String fooBar = "Foo Bar"; final String and = " and "; final String helloWorld = "Hello World"; - TextOps, String> segOps = StyledText.textOps(); + TextOps, String> segOps = StyledText.textOps(""); SimpleEditableStyledDocument doc0 = new SimpleEditableStyledDocument<>("", ""); 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 8c41c9479..e27f9084f 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java @@ -5,7 +5,7 @@ public class SimpleEditableStyledDocumentTest { - private final TextOps, String> segOps = StyledText.textOps(); + private final TextOps, String> segOps = StyledText.textOps(""); /** * The style of the inserted text will be the style at position diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java index b8df9131d..46207df34 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/StyledTextAreaModelTest.java @@ -11,7 +11,7 @@ public class StyledTextAreaModelTest { @Test public void testUndoWithWinNewlines() { - final TextOps>, Collection> segOps = StyledText.textOps(); + final TextOps>, Collection> segOps = StyledText.textOps(Collections.emptyList()); String text1 = "abc\r\ndef"; String text2 = "A\r\nB\r\nC"; @@ -32,7 +32,7 @@ public void testUndoWithWinNewlines() { @Test public void testForBug216() { - final TextOps, Boolean> segOps = StyledText.textOps(); + final TextOps, Boolean> segOps = StyledText.textOps(false); // set up area with some styled text content boolean initialStyle = false; From 07c9ccff39e2925b46f924c1ccd45b2e0f3c0714 Mon Sep 17 00:00:00 2001 From: Andreas Fester Date: Thu, 17 Nov 2016 14:41:03 +0100 Subject: [PATCH 5/9] Cleanup after commit consolidation --- .../demo/customobject/CircleObject.java | 58 - .../demo/customobject/CustomObjectDemo.java | 61 - .../demo/customobject/RectangleObject.java | 65 - .../org/fxmisc/richtext/ClipboardActions.java | 3 +- .../org/fxmisc/richtext/CssProperties.java | 2 +- .../fxmisc/richtext/GenericRichtextArea.java | 1295 ----------------- .../org/fxmisc/richtext/ParagraphText.java | 2 +- .../fxmisc/richtext/model/CustomObject.java | 105 -- .../model/EditableStyledDocument.java | 2 +- .../fxmisc/richtext/model/LinkedImage.java | 81 -- .../model/ReadOnlyStyledDocument.java | 5 - .../org/fxmisc/richtext/model/Segment.java | 40 - .../fxmisc/richtext/model/StyledDocument.java | 1 + .../fxmisc/richtext/model/StyledText.java.bck | 127 -- .../richtext/model/StyledTextAreaModel.java | 2 +- .../richtext/model/CustomObjectTest.java | 103 -- .../model/ReadOnlyStyledDocumentTest.java | 10 +- .../SimpleEditableStyledDocumentTest.java | 1 - 18 files changed, 12 insertions(+), 1951 deletions(-) delete mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java delete mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java delete mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck delete mode 100644 richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java deleted file mode 100644 index 596f05c9e..000000000 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.fxmisc.richtext.demo.customobject; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -import org.fxmisc.richtext.model.Codec; -import org.fxmisc.richtext.model.CustomObject; - -import javafx.scene.Node; -import javafx.scene.shape.Circle; - -/** - * A custom object which represents a circle. - */ -public class CircleObject extends CustomObject> { - - private double radius; - - public CircleObject() {} - - public CircleObject(double radius) { - super(new ArrayList()); - this.radius = radius; - } - - public double getRadius() { - return radius; - } - - @Override - public void encode(DataOutputStream os) throws IOException { - Codec.STRING_CODEC.encode(os, Double.toString(radius)); - } - - @Override - public void decode(DataInputStream is) throws IOException { - try { - radius = Double.parseDouble(Codec.STRING_CODEC.decode(is)); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } - - @Override - public Node createNode() { - Circle result = new Circle(getRadius()); - return result; - } - - @Override - public String toString() { - return String.format("CircleObject[radius=%s]", radius); - } - -} \ No newline at end of file diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java deleted file mode 100644 index 3be7b5c49..000000000 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.fxmisc.richtext.demo.customobject; - -import java.util.ArrayList; -import java.util.Collection; - -import org.fxmisc.flowless.VirtualizedScrollPane; -import org.fxmisc.richtext.StyleClassedTextArea; -import org.fxmisc.richtext.model.LinkedImage; -import org.fxmisc.richtext.model.ReadOnlyStyledDocument; - -import javafx.application.Application; -import javafx.scene.Scene; -import javafx.scene.layout.StackPane; -import javafx.stage.Stage; - - -/** - * This demo shows how to register custom objects with the RichTextFX editor. - * It creates a sample document with some text, a custom node with a circle, a custom node - * with a rectangle and also adds an image to show that images are supported without - * explicitly implementing and registering them as custom objects. - */ -public class CustomObjectDemo extends Application { - - public static void main(String[] args) { - launch(args); - } - - - @Override - public void start(Stage primaryStage) { - StyleClassedTextArea textArea = new StyleClassedTextArea(); - textArea.setWrapText(true); - - // create the sample document - textArea.replaceText(0, 0, "This example shows how to add custom nodes, for example Rectangles "); - ReadOnlyStyledDocument, Collection> d1 = - ReadOnlyStyledDocument.from(new RectangleObject(20, 10), - new ArrayList()); - textArea.append(d1); - textArea.appendText(" or Circles "); - - ReadOnlyStyledDocument, Collection> d2 = - ReadOnlyStyledDocument.from(new CircleObject(5), - new ArrayList()); - textArea.append(d2); - - textArea.appendText("\nImages are supported by default: "); - ReadOnlyStyledDocument, Collection> d3 = - ReadOnlyStyledDocument.from(new LinkedImage>("sample.png", new ArrayList()), - new ArrayList()); - textArea.append(d3); - - textArea.appendText("\nNow, select some text from above (including one or more of the custom objects) using CTRL-C, and paste it somewhere in the document with CTRL-V."); - - Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(textArea)), 600, 400); - primaryStage.setScene(scene); - primaryStage.setTitle("Custom Object demo"); - primaryStage.show(); - } -} diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java deleted file mode 100644 index 220a365c4..000000000 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.fxmisc.richtext.demo.customobject; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -import org.fxmisc.richtext.model.Codec; -import org.fxmisc.richtext.model.CustomObject; - -import javafx.scene.Node; -import javafx.scene.shape.Rectangle; - -/** - * A custom object which represents a rectangle. - */ -public class RectangleObject extends CustomObject> { - - private double width; - private double height; - - public RectangleObject() {} - - public RectangleObject(double width, double height) { - super(new ArrayList()); - this.width = width; - this.height = height; - } - - public double getWidth() { - return width; - } - - public double getHeight() { - return height; - } - - @Override - public void encode(DataOutputStream os) throws IOException { - Codec.STRING_CODEC.encode(os, Double.toString(width)); - Codec.STRING_CODEC.encode(os, Double.toString(height)); - } - - @Override - public void decode(DataInputStream is) throws IOException { - try { - width = Double.parseDouble(Codec.STRING_CODEC.decode(is)); - height = Double.parseDouble(Codec.STRING_CODEC.decode(is)); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } - - @Override - public Node createNode() { - Rectangle result = new Rectangle(getWidth(), getHeight()); - return result; - } - - @Override - public String toString() { - return String.format("RectangleObject[width=%s, height=%s]", width, height); - } -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java b/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java index 1c852fca6..b9667a5e8 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java @@ -28,6 +28,7 @@ public interface ClipboardActions extends EditActions { Optional, Codec>> getStyleCodecs(); + SegmentOps getSegOps(); /** @@ -118,4 +119,4 @@ static DataFormat dataFormat(String name) { return new DataFormat(name); } } -} +} \ No newline at end of file diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java b/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java index 5219f7df5..4ed204729 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java @@ -12,7 +12,7 @@ /** - * CSS stuff related to {@link GenericRichtextArea}. + * CSS stuff related to {@link GenericStyledArea}. */ class CssProperties { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java deleted file mode 100644 index a1f473aff..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java +++ /dev/null @@ -1,1295 +0,0 @@ -package org.fxmisc.richtext; - -import static javafx.util.Duration.*; -import static org.fxmisc.richtext.PopupAlignment.*; -import static org.reactfx.EventStreams.*; -import static org.reactfx.util.Tuples.*; - -import java.time.Duration; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.IntConsumer; -import java.util.function.IntFunction; -import java.util.function.IntSupplier; -import java.util.function.IntUnaryOperator; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import javafx.beans.binding.Binding; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.ObjectBinding; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ObservableSet; -import javafx.css.PseudoClass; -import javafx.css.StyleableObjectProperty; -import javafx.event.Event; -import javafx.geometry.BoundingBox; -import javafx.geometry.Bounds; -import javafx.geometry.Insets; -import javafx.geometry.Point2D; -import javafx.scene.Node; -import javafx.scene.control.IndexRange; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.Region; -import javafx.scene.paint.Color; -import javafx.scene.paint.Paint; -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; -import javafx.stage.PopupWindow; - -import org.fxmisc.flowless.Cell; -import org.fxmisc.flowless.VirtualFlow; -import org.fxmisc.flowless.VirtualFlowHit; -import org.fxmisc.flowless.Virtualized; -import org.fxmisc.flowless.VirtualizedScrollPane; -import org.fxmisc.richtext.CssProperties.EditableProperty; -import org.fxmisc.richtext.model.Codec; -import org.fxmisc.richtext.model.EditActions; -import org.fxmisc.richtext.model.EditableStyledDocument; -import org.fxmisc.richtext.model.NavigationActions; -import org.fxmisc.richtext.model.Paragraph; -import org.fxmisc.richtext.model.PlainTextChange; -import org.fxmisc.richtext.model.RichTextChange; -import org.fxmisc.richtext.model.SegmentOps; -import org.fxmisc.richtext.model.SimpleEditableStyledDocument; -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyledDocument; -import org.fxmisc.richtext.model.StyledTextAreaModel; -import org.fxmisc.richtext.model.TextEditingArea; -import org.fxmisc.richtext.model.TwoDimensional; -import org.fxmisc.richtext.model.TwoLevelNavigator; -import org.fxmisc.richtext.model.UndoActions; -import org.fxmisc.undo.UndoManager; -import org.fxmisc.undo.UndoManagerFactory; -import org.reactfx.EventStream; -import org.reactfx.EventStreams; -import org.reactfx.StateMachine; -import org.reactfx.Subscription; -import org.reactfx.collection.LiveList; -import org.reactfx.util.Tuple2; -import org.reactfx.value.Val; -import org.reactfx.value.Var; - -/** - * Text editing control. Accepts user input (keyboard, mouse) and - * provides API to assign style to text ranges. It is suitable for - * syntax highlighting and rich-text editors. - * - *

Subclassing is allowed to define the type of style, e.g. inline - * style or style classes.

- * - *

Note: Scroll bars no longer appear when the content spans outside - * of the viewport. To add scroll bars, the area needs to be wrapped in - * a {@link VirtualizedScrollPane}. For example,

- *
- * {@code
- * // shows area without scroll bars
- * InlineCssTextArea area = new InlineCssTextArea();
- *
- * // add scroll bars that will display as needed
- * VirtualizedScrollPane vsPane = new VirtualizedScrollPane(area);
- *
- * Parent parent = //;
- * parent.getChildren().add(vsPane)
- * }
- * 
- * - *

Overriding keyboard shortcuts

- * - * {@code StyledTextArea} uses {@code KEY_TYPED} handler to handle ordinary - * character input and {@code KEY_PRESSED} handler to handle control key - * combinations (including Enter and Tab). To add or override some keyboard - * shortcuts, while keeping the rest in place, you would combine the default - * event handler with a new one that adds or overrides some of the default - * key combinations. This is how to bind {@code Ctrl+S} to the {@code save()} - * operation: - *
- * {@code
- * import static javafx.scene.input.KeyCode.*;
- * import static javafx.scene.input.KeyCombination.*;
- * import static org.fxmisc.wellbehaved.event.EventPattern.*;
- * import static org.fxmisc.wellbehaved.event.InputMap.*;
- *
- * import org.fxmisc.wellbehaved.event.Nodes;
- *
- * Nodes.addInputMap(area, consume(keyPressed(S, CONTROL_DOWN), event -> save()));
- * }
- * 
- * - * @param type of style that can be applied to text. - */ -public class GenericRichtextArea extends Region - implements - TextEditingArea, - EditActions, - ClipboardActions, - NavigationActions, - UndoActions, - TwoDimensional, - Virtualized { - - /** - * Index range [0, 0). - */ - public static final IndexRange EMPTY_RANGE = new IndexRange(0, 0); - - private static final PseudoClass HAS_CARET = PseudoClass.getPseudoClass("has-caret"); - private static final PseudoClass FIRST_PAR = PseudoClass.getPseudoClass("first-paragraph"); - private static final PseudoClass LAST_PAR = PseudoClass.getPseudoClass("last-paragraph"); - - - /* ********************************************************************** * - * * - * Properties * - * * - * Properties affect behavior and/or appearance of this control. * - * * - * They are readable and writable by the client code and never change by * - * other means, i.e. they contain either the default value or the value * - * set by the client code. * - * * - * ********************************************************************** */ - - /** - * Background fill for highlighted text. - */ - private final StyleableObjectProperty highlightFill - = new CssProperties.HighlightFillProperty(this, Color.DODGERBLUE); - - /** - * Text color for highlighted text. - */ - private final StyleableObjectProperty highlightTextFill - = new CssProperties.HighlightTextFillProperty(this, Color.WHITE); - - /** - * Controls the blink rate of the caret, when one is displayed. Setting - * the duration to zero disables blinking. - */ - private final StyleableObjectProperty caretBlinkRate - = new CssProperties.CaretBlinkRateProperty(this, javafx.util.Duration.millis(500)); - - // editable property - /** - * Indicates whether this text area can be edited by the user. - * Note that this property doesn't affect editing through the API. - */ - private final BooleanProperty editable = new EditableProperty<>(this); - public final boolean isEditable() { return editable.get(); } - public final void setEditable(boolean value) { editable.set(value); } - public final BooleanProperty editableProperty() { return editable; } - - // wrapText property - /** - * When a run of text exceeds the width of the text region, - * then this property indicates whether the text should wrap - * onto another line. - */ - private final BooleanProperty wrapText = new SimpleBooleanProperty(this, "wrapText"); - public final boolean isWrapText() { return wrapText.get(); } - public final void setWrapText(boolean value) { wrapText.set(value); } - public final BooleanProperty wrapTextProperty() { return wrapText; } - - // showCaret property - /** - * Indicates when this text area should display a caret. - */ - private final Var showCaret = Var.newSimpleVar(CaretVisibility.AUTO); - public final CaretVisibility getShowCaret() { return showCaret.getValue(); } - public final void setShowCaret(CaretVisibility value) { showCaret.setValue(value); } - public final Var showCaretProperty() { return showCaret; } - - public static enum CaretVisibility { - /** Caret is displayed. */ - ON, - /** Caret is displayed when area is focused, enabled, and editable. */ - AUTO, - /** Caret is not displayed. */ - OFF - } - - // undo manager - @Override public UndoManager getUndoManager() { return model.getUndoManager(); } - @Override public void setUndoManager(UndoManagerFactory undoManagerFactory) { - model.setUndoManager(undoManagerFactory); - } - - /** - * Popup window that will be positioned by this text area relative to the - * caret or selection. Use {@link #popupAlignmentProperty()} to specify - * how the popup should be positioned relative to the caret or selection. - * Use {@link #popupAnchorOffsetProperty()} or - * {@link #popupAnchorAdjustmentProperty()} to further adjust the position. - */ - private final ObjectProperty popupWindow = new SimpleObjectProperty<>(); - public void setPopupWindow(PopupWindow popup) { popupWindow.set(popup); } - public PopupWindow getPopupWindow() { return popupWindow.get(); } - public ObjectProperty popupWindowProperty() { return popupWindow; } - - /** @deprecated Use {@link #setPopupWindow(PopupWindow)}. */ - @Deprecated - public void setPopupAtCaret(PopupWindow popup) { popupWindow.set(popup); } - /** @deprecated Use {@link #getPopupWindow()}. */ - @Deprecated - public PopupWindow getPopupAtCaret() { return popupWindow.get(); } - /** @deprecated Use {@link #popupWindowProperty()}. */ - @Deprecated - public ObjectProperty popupAtCaretProperty() { return popupWindow; } - - /** - * Specifies further offset (in pixels) of the popup window from the - * position specified by {@link #popupAlignmentProperty()}. - * - *

If {@link #popupAnchorAdjustmentProperty()} is also specified, then - * it overrides the offset set by this property. - */ - private final ObjectProperty popupAnchorOffset = new SimpleObjectProperty<>(); - public void setPopupAnchorOffset(Point2D offset) { popupAnchorOffset.set(offset); } - public Point2D getPopupAnchorOffset() { return popupAnchorOffset.get(); } - public ObjectProperty popupAnchorOffsetProperty() { return popupAnchorOffset; } - - /** - * Specifies how to adjust the popup window's anchor point. The given - * operator is invoked with the screen position calculated according to - * {@link #popupAlignmentProperty()} and should return a new screen - * position. This position will be used as the popup window's anchor point. - * - *

Setting this property overrides {@link #popupAnchorOffsetProperty()}. - */ - private final ObjectProperty> popupAnchorAdjustment = new SimpleObjectProperty<>(); - public void setPopupAnchorAdjustment(UnaryOperator f) { popupAnchorAdjustment.set(f); } - public UnaryOperator getPopupAnchorAdjustment() { return popupAnchorAdjustment.get(); } - public ObjectProperty> popupAnchorAdjustmentProperty() { return popupAnchorAdjustment; } - - /** - * Defines where the popup window given in {@link #popupWindowProperty()} - * is anchored, i.e. where its anchor point is positioned. This position - * can further be adjusted by {@link #popupAnchorOffsetProperty()} or - * {@link #popupAnchorAdjustmentProperty()}. - */ - private final ObjectProperty popupAlignment = new SimpleObjectProperty<>(CARET_TOP); - public void setPopupAlignment(PopupAlignment pos) { popupAlignment.set(pos); } - public PopupAlignment getPopupAlignment() { return popupAlignment.get(); } - public ObjectProperty popupAlignmentProperty() { return popupAlignment; } - - /** - * Defines how long the mouse has to stay still over the text before a - * {@link MouseOverTextEvent} of type {@code MOUSE_OVER_TEXT_BEGIN} is - * fired on this text area. When set to {@code null}, no - * {@code MouseOverTextEvent}s are fired on this text area. - * - *

Default value is {@code null}. - */ - private final ObjectProperty mouseOverTextDelay = new SimpleObjectProperty<>(null); - public void setMouseOverTextDelay(Duration delay) { mouseOverTextDelay.set(delay); } - public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); } - public ObjectProperty mouseOverTextDelayProperty() { return mouseOverTextDelay; } - - /** - * Defines how to handle an event in which the user has selected some text, dragged it to a - * new location within the area, and released the mouse at some character {@code index} - * within the area. - * - *

By default, this will relocate the selected text to the character index where the mouse - * was released. To override it, use {@link #setOnSelectionDrop(IntConsumer)}. - */ - private Property onSelectionDrop = new SimpleObjectProperty<>(this::moveSelectedText); - public final void setOnSelectionDrop(IntConsumer consumer) { onSelectionDrop.setValue(consumer); } - public final IntConsumer getOnSelectionDrop() { return onSelectionDrop.getValue(); } - - private final ObjectProperty> paragraphGraphicFactory = new SimpleObjectProperty<>(null); - public void setParagraphGraphicFactory(IntFunction factory) { paragraphGraphicFactory.set(factory); } - public IntFunction getParagraphGraphicFactory() { return paragraphGraphicFactory.get(); } - public ObjectProperty> paragraphGraphicFactoryProperty() { return paragraphGraphicFactory; } - - /** - * Indicates whether the initial style should also be used for plain text - * inserted into this text area. When {@code false}, the style immediately - * preceding the insertion position is used. Default value is {@code false}. - */ - public BooleanProperty useInitialStyleForInsertionProperty() { return model.useInitialStyleForInsertionProperty(); } - public void setUseInitialStyleForInsertion(boolean value) { model.setUseInitialStyleForInsertion(value); } - public boolean getUseInitialStyleForInsertion() { return model.getUseInitialStyleForInsertion(); } - - private Optional, Codec>> styleCodecs = Optional.empty(); - /** - * Sets codecs to encode/decode style information to/from binary format. - * Providing codecs enables clipboard actions to retain the style information. - */ - public void setStyleCodecs(Codec paragraphStyleCodec, Codec textStyleCodec) { - styleCodecs = Optional.of(t(paragraphStyleCodec, textStyleCodec)); - } - @Override - public Optional, Codec>> getStyleCodecs() { - return styleCodecs; - } - - /** - * The estimated scrollX value. This can be set in order to scroll the content. - * Value is only accurate when area does not wrap lines and uses the same font size - * throughout the entire area. - */ - @Override - public Var estimatedScrollXProperty() { return virtualFlow.estimatedScrollXProperty(); } - public double getEstimatedScrollX() { return virtualFlow.estimatedScrollXProperty().getValue(); } - public void setEstimatedScrollX(double value) { virtualFlow.estimatedScrollXProperty().setValue(value); } - - /** - * The estimated scrollY value. This can be set in order to scroll the content. - * Value is only accurate when area does not wrap lines and uses the same font size - * throughout the entire area. - */ - @Override - public Var estimatedScrollYProperty() { return virtualFlow.estimatedScrollYProperty(); } - public double getEstimatedScrollY() { return virtualFlow.estimatedScrollYProperty().getValue(); } - public void setEstimatedScrollY(double value) { virtualFlow.estimatedScrollYProperty().setValue(value); } - - - /* ********************************************************************** * - * * - * Observables * - * * - * Observables are "dynamic" (i.e. changing) characteristics of this * - * control. They are not directly settable by the client code, but change * - * in response to user input and/or API actions. * - * * - * ********************************************************************** */ - - // text - @Override public final String getText() { return model.getText(); } - @Override public final ObservableValue textProperty() { return model.textProperty(); } - - // rich text - @Override public final StyledDocument getDocument() { return model.getDocument(); } - - // length - @Override public final int getLength() { return model.getLength(); } - @Override public final ObservableValue lengthProperty() { return model.lengthProperty(); } - - // caret position - @Override public final int getCaretPosition() { return model.getCaretPosition(); } - @Override public final ObservableValue caretPositionProperty() { return model.caretPositionProperty(); } - - // selection anchor - @Override public final int getAnchor() { return model.getAnchor(); } - @Override public final ObservableValue anchorProperty() { return model.anchorProperty(); } - - // selection - @Override public final IndexRange getSelection() { return model.getSelection(); } - @Override public final ObservableValue selectionProperty() { return model.selectionProperty(); } - - // selected text - @Override public final String getSelectedText() { return model.getSelectedText(); } - @Override public final ObservableValue selectedTextProperty() { return model.selectedTextProperty(); } - - // current paragraph index - @Override public final int getCurrentParagraph() { return model.getCurrentParagraph(); } - @Override public final ObservableValue currentParagraphProperty() { return model.currentParagraphProperty(); } - - // caret column - @Override public final int getCaretColumn() { return model.getCaretColumn(); } - @Override public final ObservableValue caretColumnProperty() { return model.caretColumnProperty(); } - - // paragraphs - @Override public LiveList> getParagraphs() { return model.getParagraphs(); } - - // beingUpdated - public ObservableBooleanValue beingUpdatedProperty() { return model.beingUpdatedProperty(); } - public boolean isBeingUpdated() { return model.isBeingUpdated(); } - - // total width estimate - /** - * The estimated width of the entire document. Accurate when area does not wrap lines and - * uses the same font size throughout the entire area. Value is only supposed to be set by - * the skin, not the user. - */ - @Override - public Val totalWidthEstimateProperty() { return virtualFlow.totalWidthEstimateProperty(); } - public double getTotalWidthEstimate() { return virtualFlow.totalWidthEstimateProperty().getValue(); } - - // total height estimate - /** - * The estimated height of the entire document. Accurate when area does not wrap lines and - * uses the same font size throughout the entire area. Value is only supposed to be set by - * the skin, not the user. - */ - @Override - public Val totalHeightEstimateProperty() { return virtualFlow.totalHeightEstimateProperty(); } - public double getTotalHeightEstimate() { return virtualFlow.totalHeightEstimateProperty().getValue(); } - - /* ********************************************************************** * - * * - * Event streams * - * * - * ********************************************************************** */ - - // text changes - @Override public final EventStream plainTextChanges() { return model.plainTextChanges(); } - - // rich text changes - @Override public final EventStream> richChanges() { return model.richChanges(); } - - /* ********************************************************************** * - * * - * Private fields * - * * - * ********************************************************************** */ - - private final StyledTextAreaBehavior behavior; - - private Subscription subscriptions = () -> {}; - - private final Binding caretVisible; - - private final Val> _popupAnchorAdjustment; - - private final VirtualFlow, Cell, ParagraphBox>> virtualFlow; - - // used for two-level navigation, where on the higher level are - // paragraphs and on the lower level are lines within a paragraph - private final TwoLevelNavigator navigator; - - private boolean followCaretRequested = false; - - /** - * model - */ - private final StyledTextAreaModel model; - - /** - * @return this area's {@link StyledTextAreaModel} - */ - final StyledTextAreaModel getModel() { - return model; - } - - /* ********************************************************************** * - * * - * Fields necessary for Cloning * - * * - * ********************************************************************** */ - - /** - * The underlying document that can be displayed by multiple {@code StyledTextArea}s. - */ - public final EditableStyledDocument getContent() { return model.getContent(); } - - /** - * Style used by default when no other style is provided. - */ - public final S getInitialTextStyle() { return model.getInitialTextStyle(); } - - /** - * Style used by default when no other style is provided. - */ - public final PS getInitialParagraphStyle() { return model.getInitialParagraphStyle(); } - - /** - * Style applicator used by the default skin. - */ - private final BiConsumer applyStyle; - public final BiConsumer getApplyStyle() { return applyStyle; } - - /** - * Style applicator used by the default skin. - */ - private final BiConsumer applyParagraphStyle; - public final BiConsumer getApplyParagraphStyle() { return applyParagraphStyle; } - - /** - * Indicates whether style should be preserved on undo/redo, - * copy/paste and text move. - * TODO: Currently, only undo/redo respect this flag. - */ - public final boolean isPreserveStyle() { return model.isPreserveStyle(); } - - private final SegmentOps segmentOps; - public final SegmentOps getSegmentOps() { return segmentOps; } - - - /* ********************************************************************** * - * * - * Constructors * - * * - * ********************************************************************** */ - - /** - * Creates a text area with empty text content. - * - * @param initialTextStyle style to use in places where no other style is - * specified (yet). - * @param applyStyle function that, given a {@link Text} node and - * a style, applies the style to the text node. This function is - * used by the default skin to apply style to text nodes. - * @param initialParagraphStyle style to use in places where no other style is - * specified (yet). - * @param applyParagraphStyle function that, given a {@link TextFlow} node and - * a style, applies the style to the paragraph node. This function is - * used by the default skin to apply style to paragraph nodes. - */ - public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, SegmentOps segmentOps, BiConsumer applyStyle - ) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, true); - } - - public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, SegmentOps segmentOps, BiConsumer applyStyle, - boolean preserveStyle - ) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, - new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), preserveStyle); - } - - /** - * The same as {@link #StyledTextArea(Object, BiConsumer, Object, BiConsumer)} except that - * this constructor can be used to create another {@code StyledTextArea} object that - * shares the same {@link EditableStyledDocument}. - */ - public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, - SegmentOps segmentOps, BiConsumer applyStyle, - EditableStyledDocument document) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, document, true); - } - - public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, - SegmentOps segmentOps, - BiConsumer applyStyle, - EditableStyledDocument document, boolean preserveStyle) { - this.model = new StyledTextAreaModel<>(initialParagraphStyle, initialTextStyle, segmentOps, document, preserveStyle); - this.applyStyle = applyStyle; - this.applyParagraphStyle = applyParagraphStyle; - this.segmentOps = segmentOps; - - // allow tab traversal into area - setFocusTraversable(true); - - this.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); - getStyleClass().add("styled-text-area"); - getStylesheets().add(GenericRichtextArea.class.getResource("styled-text-area.css").toExternalForm()); - - // keeps track of currently used non-empty cells - @SuppressWarnings("unchecked") - ObservableSet> nonEmptyCells = FXCollections.observableSet(); - - // Initialize content - virtualFlow = VirtualFlow.createVertical( - getParagraphs(), - par -> { - Cell, ParagraphBox> cell = createCell( - par, - applyStyle, - applyParagraphStyle); - nonEmptyCells.add(cell.getNode()); - return cell.beforeReset(() -> nonEmptyCells.remove(cell.getNode())) - .afterUpdateItem(p -> nonEmptyCells.add(cell.getNode())); - }); - getChildren().add(virtualFlow); - - // initialize navigator - IntSupplier cellCount = () -> getParagraphs().size(); - IntUnaryOperator cellLength = i -> virtualFlow.getCell(i).getNode().getLineCount(); - navigator = new TwoLevelNavigator(cellCount, cellLength); - - // follow the caret every time the caret position or paragraphs change - EventStream caretPosDirty = invalidationsOf(caretPositionProperty()); - EventStream paragraphsDirty = invalidationsOf(getParagraphs()); - EventStream selectionDirty = invalidationsOf(selectionProperty()); - // need to reposition popup even when caret hasn't moved, but selection has changed (been deselected) - EventStream caretDirty = merge(caretPosDirty, paragraphsDirty, selectionDirty); - subscribeTo(caretDirty, x -> requestFollowCaret()); - - // whether or not to display the caret - EventStream blinkCaret = EventStreams.valuesOf(showCaretProperty()) - .flatMap(mode -> { - switch (mode) { - case ON: - return EventStreams.valuesOf(Val.constant(true)); - case OFF: - return EventStreams.valuesOf(Val.constant(false)); - default: - case AUTO: - return EventStreams.valuesOf(focusedProperty() - .and(editableProperty()) - .and(disabledProperty().not())); - } - }); - - // the rate at which to display the caret - EventStream blinkRate = EventStreams.valuesOf(caretBlinkRate); - - // The caret is visible in periodic intervals, - // but only when blinkCaret is true. - caretVisible = EventStreams.combine(blinkCaret, blinkRate) - .flatMap(tuple -> { - Boolean blink = tuple.get1(); - javafx.util.Duration rate = tuple.get2(); - if(blink) { - return rate.lessThanOrEqualTo(ZERO) - ? EventStreams.valuesOf(Val.constant(true)) - : booleanPulse(rate, caretDirty); - } else { - return EventStreams.valuesOf(Val.constant(false)); - } - }) - .toBinding(false); - manageBinding(caretVisible); - - // Adjust popup anchor by either a user-provided function, - // or user-provided offset, or don't adjust at all. - Val> userOffset = Val.map( - popupAnchorOffsetProperty(), - offset -> anchor -> anchor.add(offset)); - _popupAnchorAdjustment = - Val.orElse( - popupAnchorAdjustmentProperty(), - userOffset) - .orElseConst(UnaryOperator.identity()); - - // dispatch MouseOverTextEvents when mouseOverTextDelay is not null - EventStreams.valuesOf(mouseOverTextDelayProperty()) - .flatMap(delay -> delay != null - ? mouseOverTextEvents(nonEmptyCells, delay) - : EventStreams.never()) - .subscribe(evt -> Event.fireEvent(this, evt)); - - behavior = new StyledTextAreaBehavior(this); - } - - - /* ********************************************************************** * - * * - * Queries * - * * - * Queries are parameterized observables. * - * * - * ********************************************************************** */ - - /** - * Returns caret bounds relative to the viewport, i.e. the visual bounds - * of the embedded VirtualFlow. - */ - Optional getCaretBounds() { - return virtualFlow.getCellIfVisible(getCurrentParagraph()) - .map(c -> { - Bounds cellBounds = c.getNode().getCaretBounds(); - return virtualFlow.cellToViewport(c, cellBounds); - }); - } - - /** - * Returns x coordinate of the caret in the current paragraph. - */ - ParagraphBox.CaretOffsetX getCaretOffsetX() { - int idx = getCurrentParagraph(); - return getCell(idx).getCaretOffsetX(); - } - - double getViewportHeight() { - return virtualFlow.getHeight(); - } - - CharacterHit hit(ParagraphBox.CaretOffsetX x, TwoDimensional.Position targetLine) { - int parIdx = targetLine.getMajor(); - ParagraphBox cell = virtualFlow.getCell(parIdx).getNode(); - CharacterHit parHit = cell.hitTextLine(x, targetLine.getMinor()); - return parHit.offset(getParagraphOffset(parIdx)); - } - - CharacterHit hit(ParagraphBox.CaretOffsetX x, double y) { - VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(0.0, y); - if(hit.isBeforeCells()) { - return CharacterHit.insertionAt(0); - } else if(hit.isAfterCells()) { - return CharacterHit.insertionAt(getLength()); - } else { - int parIdx = hit.getCellIndex(); - int parOffset = getParagraphOffset(parIdx); - ParagraphBox cell = hit.getCell().getNode(); - Point2D cellOffset = hit.getCellOffset(); - CharacterHit parHit = cell.hitText(x, cellOffset.getY()); - return parHit.offset(parOffset); - } - } - - /** - * Helpful for determining which letter is at point x, y: - *

-     *     {@code
-     *     StyledTextArea area = // creation code
-     *     area.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent e) -> {
-     *         CharacterHit hit = area.hit(e.getX(), e.getY());
-     *         int characterPosition = hit.getInsertionIndex();
-     *
-     *         // move the caret to that character's position
-     *         area.moveTo(characterPosition, SelectionPolicy.CLEAR);
-     *     }}
-     * 
- */ - public CharacterHit hit(double x, double y) { - VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(x, y); - if(hit.isBeforeCells()) { - return CharacterHit.insertionAt(0); - } else if(hit.isAfterCells()) { - return CharacterHit.insertionAt(getLength()); - } else { - int parIdx = hit.getCellIndex(); - int parOffset = getParagraphOffset(parIdx); - ParagraphBox cell = hit.getCell().getNode(); - Point2D cellOffset = hit.getCellOffset(); - CharacterHit parHit = cell.hit(cellOffset); - return parHit.offset(parOffset); - } - } - - /** - * Returns the current line as a two-level index. - * The major number is the paragraph index, the minor - * number is the line number within the paragraph. - * - *

This method has a side-effect of bringing the current - * paragraph to the viewport if it is not already visible. - */ - TwoDimensional.Position currentLine() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - int lineIdx = cell.getNode().getCurrentLineIndex(); - return _position(parIdx, lineIdx); - } - - TwoDimensional.Position _position(int par, int line) { - return navigator.position(par, line); - } - - @Override - public final String getText(int start, int end) { - return model.getText(start, end); - } - - @Override - public String getText(int paragraph) { - return model.getText(paragraph); - } - - public Paragraph getParagraph(int index) { - return model.getParagraph(index); - } - - @Override - public StyledDocument subDocument(int start, int end) { - return model.subDocument(start, end); - } - - @Override - public StyledDocument subDocument(int paragraphIndex) { - return model.subDocument(paragraphIndex); - } - - /** - * Returns the selection range in the given paragraph. - */ - public IndexRange getParagraphSelection(int paragraph) { - return model.getParagraphSelection(paragraph); - } - - /** - * Returns the style of the character with the given index. - * If {@code index} points to a line terminator character, - * the last style used in the paragraph terminated by that - * line terminator is returned. - */ - public S getStyleOfChar(int index) { - return model.getStyleOfChar(index); - } - - /** - * Returns the style at the given position. That is the style of the - * character immediately preceding {@code position}, except when - * {@code position} points to a paragraph boundary, in which case it - * is the style at the beginning of the latter paragraph. - * - *

In other words, most of the time {@code getStyleAtPosition(p)} - * is equivalent to {@code getStyleOfChar(p-1)}, except when {@code p} - * points to a paragraph boundary, in which case it is equivalent to - * {@code getStyleOfChar(p)}. - */ - public S getStyleAtPosition(int position) { - return model.getStyleAtPosition(position); - } - - /** - * 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. If {@code position} - * points to a boundary between two paragraphs, then the first styled range - * of the latter paragraph is returned. - */ - public IndexRange getStyleRangeAtPosition(int position) { - return model.getStyleRangeAtPosition(position); - } - - /** - * Returns the styles in the given character range. - */ - public StyleSpans getStyleSpans(int from, int to) { - return model.getStyleSpans(from, to); - } - - /** - * Returns the styles in the given character range. - */ - public StyleSpans getStyleSpans(IndexRange range) { - return getStyleSpans(range.getStart(), range.getEnd()); - } - - /** - * Returns the style of the character with the given index in the given - * paragraph. If {@code index} is beyond the end of the paragraph, the - * style at the end of line is returned. If {@code index} is negative, it - * is the same as if it was 0. - */ - public S getStyleOfChar(int paragraph, int index) { - return model.getStyleOfChar(paragraph, index); - } - - /** - * Returns the style at the given position in the given paragraph. - * This is equivalent to {@code getStyleOfChar(paragraph, position-1)}. - */ - public S getStyleAtPosition(int paragraph, int position) { - return model.getStyleAtPosition(paragraph, position); - } - - /** - * Returns the range of homogeneous style that includes the given position - * in the given paragraph. If {@code position} points to a boundary between - * two styled ranges, then the range preceding {@code position} is returned. - */ - public IndexRange getStyleRangeAtPosition(int paragraph, int position) { - return model.getStyleRangeAtPosition(paragraph, position); - } - - /** - * Returns styles of the whole paragraph. - */ - public StyleSpans getStyleSpans(int paragraph) { - return model.getStyleSpans(paragraph); - } - - /** - * Returns the styles in the given character range of the given paragraph. - */ - public StyleSpans getStyleSpans(int paragraph, int from, int to) { - return model.getStyleSpans(paragraph, from, to); - } - - /** - * Returns the styles in the given character range of the given paragraph. - */ - public StyleSpans getStyleSpans(int paragraph, IndexRange range) { - return getStyleSpans(paragraph, range.getStart(), range.getEnd()); - } - - @Override - public int getAbsolutePosition(int paragraphIndex, int columnIndex) { - return model.getAbsolutePosition(paragraphIndex, columnIndex); - } - - @Override - public Position position(int row, int col) { - return model.position(row, col); - } - - @Override - public Position offsetToPosition(int charOffset, Bias bias) { - return model.offsetToPosition(charOffset, bias); - } - - - /* ********************************************************************** * - * * - * Actions * - * * - * Actions change the state of this control. They typically cause a * - * change of one or more observables and/or produce an event. * - * * - * ********************************************************************** */ - - void scrollBy(Point2D deltas) { - virtualFlow.scrollXBy(deltas.getX()); - virtualFlow.scrollYBy(deltas.getY()); - } - - void show(double y) { - virtualFlow.show(y); - } - - void showCaretAtBottom() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double y = caretBounds.getMaxY(); - virtualFlow.showAtOffset(parIdx, getViewportHeight() - y); - } - - void showCaretAtTop() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double y = caretBounds.getMinY(); - virtualFlow.showAtOffset(parIdx, -y); - } - - void requestFollowCaret() { - followCaretRequested = true; - requestLayout(); - } - - private void followCaret() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double graphicWidth = cell.getNode().getGraphicPrefWidth(); - Bounds region = extendLeft(caretBounds, graphicWidth); - virtualFlow.show(parIdx, region); - } - - /** - * Sets style for the given character range. - */ - public void setStyle(int from, int to, S style) { - model.setStyle(from, to, style); - } - - /** - * Sets style for the whole paragraph. - */ - public void setStyle(int paragraph, S style) { - model.setStyle(paragraph, style); - } - - /** - * Sets style for the given range relative in the given paragraph. - */ - public void setStyle(int paragraph, int from, int to, S style) { - model.setStyle(paragraph, from, to, style); - } - - /** - * Set multiple style ranges at once. This is equivalent to - *

-     * for(StyleSpan{@code } span: styleSpans) {
-     *     setStyle(from, from + span.getLength(), span.getStyle());
-     *     from += span.getLength();
-     * }
-     * 
- * but the actual implementation is more efficient. - */ - public void setStyleSpans(int from, StyleSpans styleSpans) { - model.setStyleSpans(from, styleSpans); - } - - /** - * Set multiple style ranges of a paragraph at once. This is equivalent to - *
-     * for(StyleSpan{@code } span: styleSpans) {
-     *     setStyle(paragraph, from, from + span.getLength(), span.getStyle());
-     *     from += span.getLength();
-     * }
-     * 
- * but the actual implementation is more efficient. - */ - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { - model.setStyleSpans(paragraph, from, styleSpans); - } - - /** - * Sets style for the whole paragraph. - */ - public void setParagraphStyle(int paragraph, PS paragraphStyle) { - model.setParagraphStyle(paragraph, paragraphStyle); - } - - /** - * Resets the style of the given range to the initial style. - */ - public void clearStyle(int from, int to) { - model.clearStyle(from, to); - } - - /** - * Resets the style of the given paragraph to the initial style. - */ - public void clearStyle(int paragraph) { - model.clearStyle(paragraph); - } - - /** - * Resets the style of the given range in the given paragraph - * to the initial style. - */ - public void clearStyle(int paragraph, int from, int to) { - model.clearStyle(paragraph, from, to); - } - - /** - * Resets the style of the given paragraph to the initial style. - */ - public void clearParagraphStyle(int paragraph) { - model.clearParagraphStyle(paragraph); - } - - @Override - public void replaceText(int start, int end, String text) { - model.replaceText(start, end, text); - } - - @Override - public void replace(int start, int end, StyledDocument replacement) { - model.replace(start, end, replacement); - } - - @Override - public void selectRange(int anchor, int caretPosition) { - model.selectRange(anchor, caretPosition); - } - - /** - * {@inheritDoc} - * @deprecated You probably meant to use {@link #moveTo(int)}. This method will be made - * package-private in the future - */ - @Deprecated - @Override - public void positionCaret(int pos) { - model.positionCaret(pos); - } - - /* ********************************************************************** * - * * - * Public API * - * * - * ********************************************************************** */ - - public void dispose() { - subscriptions.unsubscribe(); - model.dispose(); - virtualFlow.dispose(); - } - - /* ********************************************************************** * - * * - * Layout * - * * - * ********************************************************************** */ - - @Override - protected void layoutChildren() { - virtualFlow.resize(getWidth(), getHeight()); - if(followCaretRequested) { - followCaretRequested = false; - followCaret(); - } - - // position popup - PopupWindow popup = getPopupWindow(); - PopupAlignment alignment = getPopupAlignment(); - UnaryOperator adjustment = _popupAnchorAdjustment.getValue(); - if(popup != null) { - positionPopup(popup, alignment, adjustment); - } - } - - /* ********************************************************************** * - * * - * Private methods * - * * - * ********************************************************************** */ - - private Cell, ParagraphBox> createCell( - Paragraph paragraph, - BiConsumer applyStyle, - BiConsumer applyParagraphStyle) { - - ParagraphBox box = new ParagraphBox<>(paragraph, applyParagraphStyle, applyStyle, segmentOps); - - box.highlightFillProperty().bind(highlightFill); - box.highlightTextFillProperty().bind(highlightTextFill); - box.wrapTextProperty().bind(wrapTextProperty()); - box.graphicFactoryProperty().bind(paragraphGraphicFactoryProperty()); - box.graphicOffset.bind(virtualFlow.breadthOffsetProperty()); - - Val hasCaret = Val.combine( - box.indexProperty(), - currentParagraphProperty(), - (bi, cp) -> bi.intValue() == cp.intValue()); - - Subscription hasCaretPseudoClass = hasCaret.values().subscribe(value -> box.pseudoClassStateChanged(HAS_CARET, value)); - Subscription firstParPseudoClass = box.indexProperty().values().subscribe(idx -> box.pseudoClassStateChanged(FIRST_PAR, idx == 0)); - Subscription lastParPseudoClass = EventStreams.combine( - box.indexProperty().values(), - getParagraphs().sizeProperty().values() - ).subscribe(in -> in.exec((i, n) -> box.pseudoClassStateChanged(LAST_PAR, i == n-1))); - - // caret is visible only in the paragraph with the caret - Val cellCaretVisible = hasCaret.flatMap(x -> x ? caretVisible : Val.constant(false)); - box.caretVisibleProperty().bind(cellCaretVisible); - - // bind cell's caret position to area's caret column, - // when the cell is the one with the caret - box.caretPositionProperty().bind(hasCaret.flatMap(has -> has - ? caretColumnProperty() - : Val.constant(0))); - - // keep paragraph selection updated - ObjectBinding cellSelection = Bindings.createObjectBinding(() -> { - int idx = box.getIndex(); - return idx != -1 - ? getParagraphSelection(idx) - : GenericRichtextArea.EMPTY_RANGE; - }, selectionProperty(), box.indexProperty()); - box.selectionProperty().bind(cellSelection); - - return new Cell, ParagraphBox>() { - @Override - public ParagraphBox getNode() { - return box; - } - - @Override - public void updateIndex(int index) { - box.setIndex(index); - } - - @Override - public void dispose() { - box.highlightFillProperty().unbind(); - box.highlightTextFillProperty().unbind(); - box.wrapTextProperty().unbind(); - box.graphicFactoryProperty().unbind(); - box.graphicOffset.unbind(); - - hasCaretPseudoClass.unsubscribe(); - firstParPseudoClass.unsubscribe(); - lastParPseudoClass.unsubscribe(); - - box.caretVisibleProperty().unbind(); - box.caretPositionProperty().unbind(); - - box.selectionProperty().unbind(); - cellSelection.dispose(); - } - }; - } - - private ParagraphBox getCell(int index) { - return virtualFlow.getCell(index).getNode(); - } - - private EventStream mouseOverTextEvents(ObservableSet> cells, Duration delay) { - return merge(cells, c -> c.stationaryIndices(delay).map(e -> e.unify( - l -> l.map((pos, charIdx) -> MouseOverTextEvent.beginAt(c.localToScreen(pos), getParagraphOffset(c.getIndex()) + charIdx)), - r -> MouseOverTextEvent.end()))); - } - - private int getParagraphOffset(int parIdx) { - return position(parIdx, 0).toOffset(); - } - - private void positionPopup( - PopupWindow popup, - PopupAlignment alignment, - UnaryOperator adjustment) { - Optional bounds = null; - switch(alignment.getAnchorObject()) { - case CARET: bounds = getCaretBoundsOnScreen(); break; - case SELECTION: bounds = getSelectionBoundsOnScreen(); break; - } - bounds.ifPresent(b -> { - double x = 0, y = 0; - switch(alignment.getHorizontalAlignment()) { - case LEFT: x = b.getMinX(); break; - case H_CENTER: x = (b.getMinX() + b.getMaxX()) / 2; break; - case RIGHT: x = b.getMaxX(); break; - } - switch(alignment.getVerticalAlignment()) { - case TOP: y = b.getMinY(); - case V_CENTER: y = (b.getMinY() + b.getMaxY()) / 2; break; - case BOTTOM: y = b.getMaxY(); break; - } - Point2D anchor = adjustment.apply(new Point2D(x, y)); - popup.setAnchorX(anchor.getX()); - popup.setAnchorY(anchor.getY()); - }); - } - - private Optional getCaretBoundsOnScreen() { - return virtualFlow.getCellIfVisible(getCurrentParagraph()) - .map(c -> c.getNode().getCaretBoundsOnScreen()); - } - - private Optional getSelectionBoundsOnScreen() { - IndexRange selection = getSelection(); - if(selection.getLength() == 0) { - return getCaretBoundsOnScreen(); - } - - Bounds[] bounds = virtualFlow.visibleCells().stream() - .map(c -> c.getNode().getSelectionBoundsOnScreen()) - .filter(Optional::isPresent) - .map(Optional::get) - .toArray(Bounds[]::new); - - if(bounds.length == 0) { - return Optional.empty(); - } - double minX = Stream.of(bounds).mapToDouble(Bounds::getMinX).min().getAsDouble(); - double maxX = Stream.of(bounds).mapToDouble(Bounds::getMaxX).max().getAsDouble(); - double minY = Stream.of(bounds).mapToDouble(Bounds::getMinY).min().getAsDouble(); - double maxY = Stream.of(bounds).mapToDouble(Bounds::getMaxY).max().getAsDouble(); - return Optional.of(new BoundingBox(minX, minY, maxX-minX, maxY-minY)); - } - - private void subscribeTo(EventStream src, Consumer cOnsumer) { - manageSubscription(src.subscribe(cOnsumer)); - } - - private void manageSubscription(Subscription subscription) { - subscriptions = subscriptions.and(subscription); - } - - private void manageBinding(Binding binding) { - subscriptions = subscriptions.and(binding::dispose); - } - - private static Bounds extendLeft(Bounds b, double w) { - if(w == 0) { - return b; - } else { - return new BoundingBox( - b.getMinX() - w, b.getMinY(), - b.getWidth() + w, b.getHeight()); - } - } - - private static EventStream booleanPulse(javafx.util.Duration javafxDuration, EventStream restartImpulse) { - Duration duration = Duration.ofMillis(Math.round(javafxDuration.toMillis())); - EventStream ticks = EventStreams.restartableTicks(duration, restartImpulse); - return StateMachine.init(false) - .on(restartImpulse.withDefaultEvent(null)).transition((state, impulse) -> true) - .on(ticks).transition((state, tick) -> !state) - .toStateStream(); - } -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java index 1062d9e01..4554af545 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java @@ -37,7 +37,7 @@ public ObjectProperty highlightTextFillProperty() { public void setCaretPosition(int pos) { caretPosition.setValue(pos); } private final Val clampedCaretPosition; - private final ObjectProperty selection = new SimpleObjectProperty<>(GenericRichtextArea.EMPTY_RANGE); + private final ObjectProperty selection = new SimpleObjectProperty<>(StyledTextArea.EMPTY_RANGE); public ObjectProperty selectionProperty() { return selection; } public void setSelection(IndexRange sel) { selection.set(sel); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java deleted file mode 100644 index c300c31ca..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.fxmisc.richtext.model; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** - * This is the base class for custom objects in the model layer. - * Its String representation is always one character long and contains - * the "object replacement character" (\ufffc). - */ -public abstract class CustomObject implements Segment { - - protected S style; - - protected CustomObject() {} - - public CustomObject(S style) { - this.style = style; - } - - - @Override - public Segment subSequence(int start, int end) { - if (start == 0 && end == 1) { - return this; - } - return new StyledText<>("", style); - } - - - @Override - public Segment subSequence(int start) { - if (start == 1) { - return new StyledText<>("", style); - } - return this; - } - - - @Override - public CustomObject append(String str) { - throw new UnsupportedOperationException(); - // return new StyledText<>(text + str, style); - } - - - @Override - public CustomObject spliced(int from, int to, CharSequence replacement) { - throw new UnsupportedOperationException(); -/* String left = text.substring(0, from); - String right = text.substring(to); - return new StyledText<>(left + replacement + right, style);*/ - } - - - @Override - public int length() { - return 1; - } - - - @Override - public char charAt(int index) { - return getText().charAt(0); - } - - - @Override - public String getText() { - return "\ufffc"; - } - - - @Override - public S getStyle() { - return style; - } - - @Override - public void setStyle(S style) { - this.style = style; - } - - public abstract void encode(DataOutputStream os) throws IOException; - - @Override - public final void encode(DataOutputStream os, Codec styleCodec) throws IOException { - encode(os); - styleCodec.encode(os, style); - } - - public abstract void decode(DataInputStream is) throws IOException; - - @Override - public final void decode(DataInputStream is, Codec styleCodec) throws IOException { - decode(is); - style = styleCodec.decode(is); - } - - @Override - public boolean canJoin(Segment right) { - return false; - } -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java index 562715c17..f932e377e 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java @@ -8,7 +8,7 @@ import org.reactfx.value.Val; /** - * Content model for {@link org.fxmisc.richtext.GenericRichtextArea}. Implements edit operations + * Content model for {@link org.fxmisc.richtext.GenericStyledArea}. Implements edit operations * on styled text, but not worrying about additional aspects such as * caret or selection, which are handled by {@link StyledTextAreaModel}. */ diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java deleted file mode 100644 index 7d4887add..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.fxmisc.richtext.model; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.util.function.Function; - -import javafx.scene.Node; - - -/** - * A custom object which contains a file path to an image file. - * When rendered in the rich text editor, the image is loaded from the - * specified file. - */ -public class LinkedImage extends CustomObject { - - private String imagePath; - - LinkedImage() {} - - /** - * 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 LinkedImage(String imagePath, S style) { - super(style); - - // if the image is below the current working directory, - // then store as relative path name. - String currentDir = System.getProperty("user.dir") + File.separatorChar; - if (imagePath.startsWith(currentDir)) { - imagePath = imagePath.substring(currentDir.length()); - } - - this.imagePath = imagePath; - } - - - /** - * @return The path of the image to render. - */ - public String getImagePath() { - return imagePath; - } - - - @Override - public void encode(DataOutputStream os) throws IOException { - Codec.STRING_CODEC.encode(os, imagePath); - } - - - @Override - public void decode(DataInputStream is) throws IOException { - imagePath = Codec.STRING_CODEC.decode(is); - } - - - @Override - public String toString() { - return String.format("LinkedImage[path=%s]", imagePath); - } - - - @SuppressWarnings("rawtypes") - private static Function nodeFactory; - - @Override - public Node createNode() { - return nodeFactory.apply(this); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static void setNodeFactory(Function, Node> nodeFactory) { - LinkedImage.nodeFactory = (Function) (Object) nodeFactory; - } -} 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 c1cceb00c..48ef3b5da 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java @@ -248,11 +248,6 @@ public StyledDocument subSequence(int start, int end) { return split(end)._1.split(start)._2; } - @Override - public StyledDocument subDocument(int paragraphIndex) { - return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex)), segmentOps); - } - public Tuple3, RichTextChange, MaterializedListModification>> replace( int from, int to, ReadOnlyStyledDocument replacement) { return replace(from, to, x -> replacement); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java deleted file mode 100644 index 46d71c85a..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.fxmisc.richtext.model; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -import javafx.scene.Node; - -/** - * An interface to segment types, like StyledText or LinkedImage. - */ -public interface Segment { - - int length(); - - char charAt(int index); - - String getText(); // each segment has a string associated with it - for custom objects - // this is the replacement character \ufffc - - Segment subSequence(int start, int end); - - Segment subSequence(int start); - - Segment append(String str); - - Segment spliced(int from, int to, CharSequence replacement); - - S getStyle(); - - void encode(DataOutputStream os, Codec styleCodec) throws IOException; - - void decode(DataInputStream is, Codec styleCodec) throws IOException; - - Node createNode(); - - boolean canJoin(Segment right); - - void setStyle(S style); -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java index 436188597..07a507358 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java @@ -3,6 +3,7 @@ import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javafx.scene.control.IndexRange; diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck deleted file mode 100644 index 918c81abd..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck +++ /dev/null @@ -1,127 +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.function.Function; - -import javafx.scene.Node; - -public class StyledText implements Segment { - private String text; - private S style; - - StyledText() {} - - public StyledText(String text, S style) { - this.text = text; - this.style = style; - } - - @Override - public int length() { - return text.length(); - } - - @Override - public char charAt(int index) { - return text.charAt(index); - } - - @Override - public String getText() { - return text; - } - - @Override - public StyledText subSequence(int start, int end) { - return new StyledText<>(text.substring(start, end), style); - } - - @Override - public StyledText subSequence(int start) { - return new StyledText<>(text.substring(start), style); - } - - @Override - public StyledText append(String str) { - return new StyledText<>(text + str, style); - } - - @Override - public StyledText spliced(int from, int to, CharSequence replacement) { - String left = text.substring(0, from); - String right = text.substring(to); - return new StyledText<>(left + replacement + right, style); - } - - @Override - public S getStyle() { - return style; - } - - @Override - public void setStyle(S style) { - 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 Objects.equals(this.text, that.text) - && Objects.equals(this.style, that.style); - } else { - return false; - } - } - - @Override - public int hashCode() { - return Objects.hash(text, style); - } - - - @Override - public boolean canJoin(Segment right) { - - if (right instanceof StyledText) { - return Objects.equals(getStyle(), right.getStyle()); - } - - return false; - } - - - @Override - public void encode(DataOutputStream os, Codec styleCodec) throws IOException { - Codec.STRING_CODEC.encode(os, getText()); - styleCodec.encode(os, style); - } - - @Override - public void decode(DataInputStream is, Codec styleCodec) throws IOException { - text = Codec.STRING_CODEC.decode(is); - style = styleCodec.decode(is); - } - - @SuppressWarnings("rawtypes") - private static Function nodeFactory; - - @Override - public Node createNode() { - return nodeFactory.apply(this); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static void setNodeFactory(Function, Node> nodeFactory) { - StyledText.nodeFactory = (Function) (Object) nodeFactory; - } - -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index c5fd6892f..a91b37018 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -28,7 +28,7 @@ import org.reactfx.value.Var; /** - * Model for {@link org.fxmisc.richtext.GenericRichtextArea} + * Model for {@link org.fxmisc.richtext.GenericStyledArea} * * @param type of style that can be applied to text. * @param type of style that can be applied to Paragraph diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java deleted file mode 100644 index d383e16d1..000000000 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.fxmisc.richtext.model; - -import static org.junit.Assert.*; - -import java.util.List; - -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; - -public class CustomObjectTest { - - @Test - public void testLinkedImageCreation() { - SimpleEditableStyledDocument doc = - new SimpleEditableStyledDocument<>(true, ""); - StyledDocument customObj = - ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); - doc.replace(0, 0, customObj); - assertEquals(1, doc.getLength()); - - Paragraph para = doc.getParagraphs().get(0); - Object x = para.getSegments().get(0); - assertThat(x, instanceOf(LinkedImage.class)); - } - - @Test - public void testMultipleSegments() { - SimpleEditableStyledDocument doc = new SimpleEditableStyledDocument<>(true, ""); - - final String helloWorld = "Hello World"; - final String helloMoon = "Hello Moon"; - - StyledDocument text = ReadOnlyStyledDocument.fromString(helloWorld, true, ""); - doc.replace(0, 0, text); - - StyledDocument customObj = ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); - doc.replace(doc.getLength(), doc.getLength(), customObj); - - StyledDocument text2 = ReadOnlyStyledDocument.fromString(helloMoon, true, ""); - doc.replace(doc.getLength(), doc.getLength(), text2); - - // Assert that the document now contains one paragraph with three segments - - assertThat(doc.getParagraphs().size(), equalTo(1)); - - Paragraph p = doc.getParagraphs().get(0); - List> segs = p.getSegments(); - - assertThat(segs.size(), equalTo(3)); - assertThat(segs.get(0).getText(), equalTo(helloWorld)); - assertThat(segs.get(1).getText(), equalTo("\ufffc")); - assertThat(segs.get(2).getText(), equalTo(helloMoon)); - } - - - @Test - public void testRestyle() { - SimpleEditableStyledDocument doc = new SimpleEditableStyledDocument<>(true, ""); - - final String helloWorld = "Hello World"; - final String helloMoon = "Hello Moon"; - - StyledDocument text = ReadOnlyStyledDocument.fromString(helloWorld, true, "bold"); - doc.replace(0, 0, text); - - StyledDocument customObj = ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); - doc.replace(doc.getLength(), doc.getLength(), customObj); - - StyledDocument text2 = ReadOnlyStyledDocument.fromString(helloMoon, true, "bold"); - doc.replace(doc.getLength(), doc.getLength(), text2); - - // The document now contains one paragraph with three segments - // Restyle part of the document: - - StyleSpans styles = doc.getStyleSpans(6, 17); - assertThat("Invalid number of Spans", styles.getSpanCount(), equalTo(3)); - - StyleSpans newStyles = styles.mapStyles(style -> "italic"); - doc.setStyleSpans(6, newStyles); - - // Assert that the document now contains one paragraph with five segments - // StyledText[text="Hello ", style=bold] - // StyledText[text="World", style=italic] - // LinkedImage[path=sample.png] - // StyledText[text="Hello", style=italic] - // StyledText[text=" Moon", style=bold] - - assertThat(doc.getParagraphs().size(), equalTo(1)); - List> segs = doc.getParagraphs().get(0).getSegments(); - - assertThat(segs.size(), equalTo(5)); - assertThat(segs.get(0).getText(), equalTo("Hello ")); - assertThat(segs.get(1).getText(), equalTo("World")); - assertThat(segs.get(2).getText(), equalTo("\ufffc")); - assertThat(segs.get(2), instanceOf(LinkedImage.class)); - assertThat(segs.get(3).getText(), equalTo("Hello")); - assertThat(segs.get(4).getText(), equalTo(" Moon")); - } - -} 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 318961093..57fd61afd 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java @@ -18,7 +18,7 @@ public void testUndo() { doc0.replace(0, 0, fromString("abcd", "Y", "Y", segOps)).exec((doc1, chng1, pchng1) -> { // undo chng1 - doc1.replace(chng1.getPosition(), chng1.getInsertionEnd(), from(chng1.getRemoved(), ops)).exec((doc2, chng2, pchng2) -> { + doc1.replace(chng1.getPosition(), chng1.getInsertionEnd(), from(chng1.getRemoved())).exec((doc2, chng2, pchng2) -> { // we should have arrived at the original document assertEquals(doc0, doc2); @@ -52,13 +52,13 @@ public void testRestyle() { SimpleEditableStyledDocument doc0 = new SimpleEditableStyledDocument<>("", ""); - ReadOnlyStyledDocument text = fromString(fooBar, "", "bold"); + ReadOnlyStyledDocument, String> text = fromString(fooBar, "", "bold", segOps); doc0.replace(doc0.getLength(), doc0.getLength(), text); - text = fromString(and, "", ""); + text = fromString(and, "", "", segOps); doc0.replace(doc0.getLength(), doc0.getLength(), text); - text = fromString(helloWorld, "", "bold"); + text = fromString(helloWorld, "", "bold", segOps); doc0.replace(doc0.getLength(), doc0.getLength(), text); StyleSpans styles = doc0.getStyleSpans(4, 17); @@ -71,7 +71,7 @@ public void testRestyle() { // StyledText[text="Foo ", style=bold] // StyledText[text="Bar and Hello", style=italic] // StyledText[text=" World", style=bold] - List> result = doc0.getParagraphs().get(0).getSegments(); + 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")); 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 e27f9084f..c94ada0ae 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java @@ -10,7 +10,6 @@ public class SimpleEditableStyledDocumentTest { /** * The style of the inserted text will be the style at position * {@code start} in the current document. - * @param ops */ private void replaceText(EditableStyledDocument, String> doc, int start, int end, String text) { StyledDocument, String> styledDoc = ReadOnlyStyledDocument.fromString( From c2e852968a976cce05b83a995d12a32d9bfeb49c Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Mon, 28 Nov 2016 20:46:05 -0800 Subject: [PATCH 6/9] Cleanup after commit consolidation --- .../demo/customobject/CircleObject.java | 58 - .../demo/customobject/CustomObjectDemo.java | 61 - .../demo/customobject/RectangleObject.java | 65 - .../org/fxmisc/richtext/CssProperties.java | 2 +- .../fxmisc/richtext/GenericRichtextArea.java | 1295 ----------------- .../org/fxmisc/richtext/ParagraphText.java | 2 +- .../fxmisc/richtext/model/CustomObject.java | 105 -- .../model/EditableStyledDocument.java | 2 +- .../fxmisc/richtext/model/LinkedImage.java | 81 -- .../model/ReadOnlyStyledDocument.java | 5 - .../org/fxmisc/richtext/model/Segment.java | 40 - .../fxmisc/richtext/model/StyledDocument.java | 1 + .../fxmisc/richtext/model/StyledText.java.bck | 127 -- .../richtext/model/StyledTextAreaModel.java | 2 +- .../richtext/model/CustomObjectTest.java | 103 -- 15 files changed, 5 insertions(+), 1944 deletions(-) delete mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java delete mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java delete mode 100644 richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java delete mode 100644 richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck delete mode 100644 richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java deleted file mode 100644 index 596f05c9e..000000000 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CircleObject.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.fxmisc.richtext.demo.customobject; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -import org.fxmisc.richtext.model.Codec; -import org.fxmisc.richtext.model.CustomObject; - -import javafx.scene.Node; -import javafx.scene.shape.Circle; - -/** - * A custom object which represents a circle. - */ -public class CircleObject extends CustomObject> { - - private double radius; - - public CircleObject() {} - - public CircleObject(double radius) { - super(new ArrayList()); - this.radius = radius; - } - - public double getRadius() { - return radius; - } - - @Override - public void encode(DataOutputStream os) throws IOException { - Codec.STRING_CODEC.encode(os, Double.toString(radius)); - } - - @Override - public void decode(DataInputStream is) throws IOException { - try { - radius = Double.parseDouble(Codec.STRING_CODEC.decode(is)); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } - - @Override - public Node createNode() { - Circle result = new Circle(getRadius()); - return result; - } - - @Override - public String toString() { - return String.format("CircleObject[radius=%s]", radius); - } - -} \ No newline at end of file diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java deleted file mode 100644 index 3be7b5c49..000000000 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/CustomObjectDemo.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.fxmisc.richtext.demo.customobject; - -import java.util.ArrayList; -import java.util.Collection; - -import org.fxmisc.flowless.VirtualizedScrollPane; -import org.fxmisc.richtext.StyleClassedTextArea; -import org.fxmisc.richtext.model.LinkedImage; -import org.fxmisc.richtext.model.ReadOnlyStyledDocument; - -import javafx.application.Application; -import javafx.scene.Scene; -import javafx.scene.layout.StackPane; -import javafx.stage.Stage; - - -/** - * This demo shows how to register custom objects with the RichTextFX editor. - * It creates a sample document with some text, a custom node with a circle, a custom node - * with a rectangle and also adds an image to show that images are supported without - * explicitly implementing and registering them as custom objects. - */ -public class CustomObjectDemo extends Application { - - public static void main(String[] args) { - launch(args); - } - - - @Override - public void start(Stage primaryStage) { - StyleClassedTextArea textArea = new StyleClassedTextArea(); - textArea.setWrapText(true); - - // create the sample document - textArea.replaceText(0, 0, "This example shows how to add custom nodes, for example Rectangles "); - ReadOnlyStyledDocument, Collection> d1 = - ReadOnlyStyledDocument.from(new RectangleObject(20, 10), - new ArrayList()); - textArea.append(d1); - textArea.appendText(" or Circles "); - - ReadOnlyStyledDocument, Collection> d2 = - ReadOnlyStyledDocument.from(new CircleObject(5), - new ArrayList()); - textArea.append(d2); - - textArea.appendText("\nImages are supported by default: "); - ReadOnlyStyledDocument, Collection> d3 = - ReadOnlyStyledDocument.from(new LinkedImage>("sample.png", new ArrayList()), - new ArrayList()); - textArea.append(d3); - - textArea.appendText("\nNow, select some text from above (including one or more of the custom objects) using CTRL-C, and paste it somewhere in the document with CTRL-V."); - - Scene scene = new Scene(new StackPane(new VirtualizedScrollPane<>(textArea)), 600, 400); - primaryStage.setScene(scene); - primaryStage.setTitle("Custom Object demo"); - primaryStage.show(); - } -} diff --git a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java b/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java deleted file mode 100644 index 220a365c4..000000000 --- a/richtextfx-demos/src/main/java/org/fxmisc/richtext/demo/customobject/RectangleObject.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.fxmisc.richtext.demo.customobject; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -import org.fxmisc.richtext.model.Codec; -import org.fxmisc.richtext.model.CustomObject; - -import javafx.scene.Node; -import javafx.scene.shape.Rectangle; - -/** - * A custom object which represents a rectangle. - */ -public class RectangleObject extends CustomObject> { - - private double width; - private double height; - - public RectangleObject() {} - - public RectangleObject(double width, double height) { - super(new ArrayList()); - this.width = width; - this.height = height; - } - - public double getWidth() { - return width; - } - - public double getHeight() { - return height; - } - - @Override - public void encode(DataOutputStream os) throws IOException { - Codec.STRING_CODEC.encode(os, Double.toString(width)); - Codec.STRING_CODEC.encode(os, Double.toString(height)); - } - - @Override - public void decode(DataInputStream is) throws IOException { - try { - width = Double.parseDouble(Codec.STRING_CODEC.decode(is)); - height = Double.parseDouble(Codec.STRING_CODEC.decode(is)); - } catch (NumberFormatException e) { - e.printStackTrace(); - } - } - - @Override - public Node createNode() { - Rectangle result = new Rectangle(getWidth(), getHeight()); - return result; - } - - @Override - public String toString() { - return String.format("RectangleObject[width=%s, height=%s]", width, height); - } -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java b/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java index 5219f7df5..4ed204729 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/CssProperties.java @@ -12,7 +12,7 @@ /** - * CSS stuff related to {@link GenericRichtextArea}. + * CSS stuff related to {@link GenericStyledArea}. */ class CssProperties { diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java deleted file mode 100644 index a1f473aff..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericRichtextArea.java +++ /dev/null @@ -1,1295 +0,0 @@ -package org.fxmisc.richtext; - -import static javafx.util.Duration.*; -import static org.fxmisc.richtext.PopupAlignment.*; -import static org.reactfx.EventStreams.*; -import static org.reactfx.util.Tuples.*; - -import java.time.Duration; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.IntConsumer; -import java.util.function.IntFunction; -import java.util.function.IntSupplier; -import java.util.function.IntUnaryOperator; -import java.util.function.UnaryOperator; -import java.util.stream.Stream; - -import javafx.beans.binding.Binding; -import javafx.beans.binding.Bindings; -import javafx.beans.binding.ObjectBinding; -import javafx.beans.property.BooleanProperty; -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.Property; -import javafx.beans.property.SimpleBooleanProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.beans.value.ObservableBooleanValue; -import javafx.beans.value.ObservableValue; -import javafx.collections.FXCollections; -import javafx.collections.ObservableSet; -import javafx.css.PseudoClass; -import javafx.css.StyleableObjectProperty; -import javafx.event.Event; -import javafx.geometry.BoundingBox; -import javafx.geometry.Bounds; -import javafx.geometry.Insets; -import javafx.geometry.Point2D; -import javafx.scene.Node; -import javafx.scene.control.IndexRange; -import javafx.scene.layout.Background; -import javafx.scene.layout.BackgroundFill; -import javafx.scene.layout.CornerRadii; -import javafx.scene.layout.Region; -import javafx.scene.paint.Color; -import javafx.scene.paint.Paint; -import javafx.scene.text.Text; -import javafx.scene.text.TextFlow; -import javafx.stage.PopupWindow; - -import org.fxmisc.flowless.Cell; -import org.fxmisc.flowless.VirtualFlow; -import org.fxmisc.flowless.VirtualFlowHit; -import org.fxmisc.flowless.Virtualized; -import org.fxmisc.flowless.VirtualizedScrollPane; -import org.fxmisc.richtext.CssProperties.EditableProperty; -import org.fxmisc.richtext.model.Codec; -import org.fxmisc.richtext.model.EditActions; -import org.fxmisc.richtext.model.EditableStyledDocument; -import org.fxmisc.richtext.model.NavigationActions; -import org.fxmisc.richtext.model.Paragraph; -import org.fxmisc.richtext.model.PlainTextChange; -import org.fxmisc.richtext.model.RichTextChange; -import org.fxmisc.richtext.model.SegmentOps; -import org.fxmisc.richtext.model.SimpleEditableStyledDocument; -import org.fxmisc.richtext.model.StyleSpans; -import org.fxmisc.richtext.model.StyledDocument; -import org.fxmisc.richtext.model.StyledTextAreaModel; -import org.fxmisc.richtext.model.TextEditingArea; -import org.fxmisc.richtext.model.TwoDimensional; -import org.fxmisc.richtext.model.TwoLevelNavigator; -import org.fxmisc.richtext.model.UndoActions; -import org.fxmisc.undo.UndoManager; -import org.fxmisc.undo.UndoManagerFactory; -import org.reactfx.EventStream; -import org.reactfx.EventStreams; -import org.reactfx.StateMachine; -import org.reactfx.Subscription; -import org.reactfx.collection.LiveList; -import org.reactfx.util.Tuple2; -import org.reactfx.value.Val; -import org.reactfx.value.Var; - -/** - * Text editing control. Accepts user input (keyboard, mouse) and - * provides API to assign style to text ranges. It is suitable for - * syntax highlighting and rich-text editors. - * - *

Subclassing is allowed to define the type of style, e.g. inline - * style or style classes.

- * - *

Note: Scroll bars no longer appear when the content spans outside - * of the viewport. To add scroll bars, the area needs to be wrapped in - * a {@link VirtualizedScrollPane}. For example,

- *
- * {@code
- * // shows area without scroll bars
- * InlineCssTextArea area = new InlineCssTextArea();
- *
- * // add scroll bars that will display as needed
- * VirtualizedScrollPane vsPane = new VirtualizedScrollPane(area);
- *
- * Parent parent = //;
- * parent.getChildren().add(vsPane)
- * }
- * 
- * - *

Overriding keyboard shortcuts

- * - * {@code StyledTextArea} uses {@code KEY_TYPED} handler to handle ordinary - * character input and {@code KEY_PRESSED} handler to handle control key - * combinations (including Enter and Tab). To add or override some keyboard - * shortcuts, while keeping the rest in place, you would combine the default - * event handler with a new one that adds or overrides some of the default - * key combinations. This is how to bind {@code Ctrl+S} to the {@code save()} - * operation: - *
- * {@code
- * import static javafx.scene.input.KeyCode.*;
- * import static javafx.scene.input.KeyCombination.*;
- * import static org.fxmisc.wellbehaved.event.EventPattern.*;
- * import static org.fxmisc.wellbehaved.event.InputMap.*;
- *
- * import org.fxmisc.wellbehaved.event.Nodes;
- *
- * Nodes.addInputMap(area, consume(keyPressed(S, CONTROL_DOWN), event -> save()));
- * }
- * 
- * - * @param type of style that can be applied to text. - */ -public class GenericRichtextArea extends Region - implements - TextEditingArea, - EditActions, - ClipboardActions, - NavigationActions, - UndoActions, - TwoDimensional, - Virtualized { - - /** - * Index range [0, 0). - */ - public static final IndexRange EMPTY_RANGE = new IndexRange(0, 0); - - private static final PseudoClass HAS_CARET = PseudoClass.getPseudoClass("has-caret"); - private static final PseudoClass FIRST_PAR = PseudoClass.getPseudoClass("first-paragraph"); - private static final PseudoClass LAST_PAR = PseudoClass.getPseudoClass("last-paragraph"); - - - /* ********************************************************************** * - * * - * Properties * - * * - * Properties affect behavior and/or appearance of this control. * - * * - * They are readable and writable by the client code and never change by * - * other means, i.e. they contain either the default value or the value * - * set by the client code. * - * * - * ********************************************************************** */ - - /** - * Background fill for highlighted text. - */ - private final StyleableObjectProperty highlightFill - = new CssProperties.HighlightFillProperty(this, Color.DODGERBLUE); - - /** - * Text color for highlighted text. - */ - private final StyleableObjectProperty highlightTextFill - = new CssProperties.HighlightTextFillProperty(this, Color.WHITE); - - /** - * Controls the blink rate of the caret, when one is displayed. Setting - * the duration to zero disables blinking. - */ - private final StyleableObjectProperty caretBlinkRate - = new CssProperties.CaretBlinkRateProperty(this, javafx.util.Duration.millis(500)); - - // editable property - /** - * Indicates whether this text area can be edited by the user. - * Note that this property doesn't affect editing through the API. - */ - private final BooleanProperty editable = new EditableProperty<>(this); - public final boolean isEditable() { return editable.get(); } - public final void setEditable(boolean value) { editable.set(value); } - public final BooleanProperty editableProperty() { return editable; } - - // wrapText property - /** - * When a run of text exceeds the width of the text region, - * then this property indicates whether the text should wrap - * onto another line. - */ - private final BooleanProperty wrapText = new SimpleBooleanProperty(this, "wrapText"); - public final boolean isWrapText() { return wrapText.get(); } - public final void setWrapText(boolean value) { wrapText.set(value); } - public final BooleanProperty wrapTextProperty() { return wrapText; } - - // showCaret property - /** - * Indicates when this text area should display a caret. - */ - private final Var showCaret = Var.newSimpleVar(CaretVisibility.AUTO); - public final CaretVisibility getShowCaret() { return showCaret.getValue(); } - public final void setShowCaret(CaretVisibility value) { showCaret.setValue(value); } - public final Var showCaretProperty() { return showCaret; } - - public static enum CaretVisibility { - /** Caret is displayed. */ - ON, - /** Caret is displayed when area is focused, enabled, and editable. */ - AUTO, - /** Caret is not displayed. */ - OFF - } - - // undo manager - @Override public UndoManager getUndoManager() { return model.getUndoManager(); } - @Override public void setUndoManager(UndoManagerFactory undoManagerFactory) { - model.setUndoManager(undoManagerFactory); - } - - /** - * Popup window that will be positioned by this text area relative to the - * caret or selection. Use {@link #popupAlignmentProperty()} to specify - * how the popup should be positioned relative to the caret or selection. - * Use {@link #popupAnchorOffsetProperty()} or - * {@link #popupAnchorAdjustmentProperty()} to further adjust the position. - */ - private final ObjectProperty popupWindow = new SimpleObjectProperty<>(); - public void setPopupWindow(PopupWindow popup) { popupWindow.set(popup); } - public PopupWindow getPopupWindow() { return popupWindow.get(); } - public ObjectProperty popupWindowProperty() { return popupWindow; } - - /** @deprecated Use {@link #setPopupWindow(PopupWindow)}. */ - @Deprecated - public void setPopupAtCaret(PopupWindow popup) { popupWindow.set(popup); } - /** @deprecated Use {@link #getPopupWindow()}. */ - @Deprecated - public PopupWindow getPopupAtCaret() { return popupWindow.get(); } - /** @deprecated Use {@link #popupWindowProperty()}. */ - @Deprecated - public ObjectProperty popupAtCaretProperty() { return popupWindow; } - - /** - * Specifies further offset (in pixels) of the popup window from the - * position specified by {@link #popupAlignmentProperty()}. - * - *

If {@link #popupAnchorAdjustmentProperty()} is also specified, then - * it overrides the offset set by this property. - */ - private final ObjectProperty popupAnchorOffset = new SimpleObjectProperty<>(); - public void setPopupAnchorOffset(Point2D offset) { popupAnchorOffset.set(offset); } - public Point2D getPopupAnchorOffset() { return popupAnchorOffset.get(); } - public ObjectProperty popupAnchorOffsetProperty() { return popupAnchorOffset; } - - /** - * Specifies how to adjust the popup window's anchor point. The given - * operator is invoked with the screen position calculated according to - * {@link #popupAlignmentProperty()} and should return a new screen - * position. This position will be used as the popup window's anchor point. - * - *

Setting this property overrides {@link #popupAnchorOffsetProperty()}. - */ - private final ObjectProperty> popupAnchorAdjustment = new SimpleObjectProperty<>(); - public void setPopupAnchorAdjustment(UnaryOperator f) { popupAnchorAdjustment.set(f); } - public UnaryOperator getPopupAnchorAdjustment() { return popupAnchorAdjustment.get(); } - public ObjectProperty> popupAnchorAdjustmentProperty() { return popupAnchorAdjustment; } - - /** - * Defines where the popup window given in {@link #popupWindowProperty()} - * is anchored, i.e. where its anchor point is positioned. This position - * can further be adjusted by {@link #popupAnchorOffsetProperty()} or - * {@link #popupAnchorAdjustmentProperty()}. - */ - private final ObjectProperty popupAlignment = new SimpleObjectProperty<>(CARET_TOP); - public void setPopupAlignment(PopupAlignment pos) { popupAlignment.set(pos); } - public PopupAlignment getPopupAlignment() { return popupAlignment.get(); } - public ObjectProperty popupAlignmentProperty() { return popupAlignment; } - - /** - * Defines how long the mouse has to stay still over the text before a - * {@link MouseOverTextEvent} of type {@code MOUSE_OVER_TEXT_BEGIN} is - * fired on this text area. When set to {@code null}, no - * {@code MouseOverTextEvent}s are fired on this text area. - * - *

Default value is {@code null}. - */ - private final ObjectProperty mouseOverTextDelay = new SimpleObjectProperty<>(null); - public void setMouseOverTextDelay(Duration delay) { mouseOverTextDelay.set(delay); } - public Duration getMouseOverTextDelay() { return mouseOverTextDelay.get(); } - public ObjectProperty mouseOverTextDelayProperty() { return mouseOverTextDelay; } - - /** - * Defines how to handle an event in which the user has selected some text, dragged it to a - * new location within the area, and released the mouse at some character {@code index} - * within the area. - * - *

By default, this will relocate the selected text to the character index where the mouse - * was released. To override it, use {@link #setOnSelectionDrop(IntConsumer)}. - */ - private Property onSelectionDrop = new SimpleObjectProperty<>(this::moveSelectedText); - public final void setOnSelectionDrop(IntConsumer consumer) { onSelectionDrop.setValue(consumer); } - public final IntConsumer getOnSelectionDrop() { return onSelectionDrop.getValue(); } - - private final ObjectProperty> paragraphGraphicFactory = new SimpleObjectProperty<>(null); - public void setParagraphGraphicFactory(IntFunction factory) { paragraphGraphicFactory.set(factory); } - public IntFunction getParagraphGraphicFactory() { return paragraphGraphicFactory.get(); } - public ObjectProperty> paragraphGraphicFactoryProperty() { return paragraphGraphicFactory; } - - /** - * Indicates whether the initial style should also be used for plain text - * inserted into this text area. When {@code false}, the style immediately - * preceding the insertion position is used. Default value is {@code false}. - */ - public BooleanProperty useInitialStyleForInsertionProperty() { return model.useInitialStyleForInsertionProperty(); } - public void setUseInitialStyleForInsertion(boolean value) { model.setUseInitialStyleForInsertion(value); } - public boolean getUseInitialStyleForInsertion() { return model.getUseInitialStyleForInsertion(); } - - private Optional, Codec>> styleCodecs = Optional.empty(); - /** - * Sets codecs to encode/decode style information to/from binary format. - * Providing codecs enables clipboard actions to retain the style information. - */ - public void setStyleCodecs(Codec paragraphStyleCodec, Codec textStyleCodec) { - styleCodecs = Optional.of(t(paragraphStyleCodec, textStyleCodec)); - } - @Override - public Optional, Codec>> getStyleCodecs() { - return styleCodecs; - } - - /** - * The estimated scrollX value. This can be set in order to scroll the content. - * Value is only accurate when area does not wrap lines and uses the same font size - * throughout the entire area. - */ - @Override - public Var estimatedScrollXProperty() { return virtualFlow.estimatedScrollXProperty(); } - public double getEstimatedScrollX() { return virtualFlow.estimatedScrollXProperty().getValue(); } - public void setEstimatedScrollX(double value) { virtualFlow.estimatedScrollXProperty().setValue(value); } - - /** - * The estimated scrollY value. This can be set in order to scroll the content. - * Value is only accurate when area does not wrap lines and uses the same font size - * throughout the entire area. - */ - @Override - public Var estimatedScrollYProperty() { return virtualFlow.estimatedScrollYProperty(); } - public double getEstimatedScrollY() { return virtualFlow.estimatedScrollYProperty().getValue(); } - public void setEstimatedScrollY(double value) { virtualFlow.estimatedScrollYProperty().setValue(value); } - - - /* ********************************************************************** * - * * - * Observables * - * * - * Observables are "dynamic" (i.e. changing) characteristics of this * - * control. They are not directly settable by the client code, but change * - * in response to user input and/or API actions. * - * * - * ********************************************************************** */ - - // text - @Override public final String getText() { return model.getText(); } - @Override public final ObservableValue textProperty() { return model.textProperty(); } - - // rich text - @Override public final StyledDocument getDocument() { return model.getDocument(); } - - // length - @Override public final int getLength() { return model.getLength(); } - @Override public final ObservableValue lengthProperty() { return model.lengthProperty(); } - - // caret position - @Override public final int getCaretPosition() { return model.getCaretPosition(); } - @Override public final ObservableValue caretPositionProperty() { return model.caretPositionProperty(); } - - // selection anchor - @Override public final int getAnchor() { return model.getAnchor(); } - @Override public final ObservableValue anchorProperty() { return model.anchorProperty(); } - - // selection - @Override public final IndexRange getSelection() { return model.getSelection(); } - @Override public final ObservableValue selectionProperty() { return model.selectionProperty(); } - - // selected text - @Override public final String getSelectedText() { return model.getSelectedText(); } - @Override public final ObservableValue selectedTextProperty() { return model.selectedTextProperty(); } - - // current paragraph index - @Override public final int getCurrentParagraph() { return model.getCurrentParagraph(); } - @Override public final ObservableValue currentParagraphProperty() { return model.currentParagraphProperty(); } - - // caret column - @Override public final int getCaretColumn() { return model.getCaretColumn(); } - @Override public final ObservableValue caretColumnProperty() { return model.caretColumnProperty(); } - - // paragraphs - @Override public LiveList> getParagraphs() { return model.getParagraphs(); } - - // beingUpdated - public ObservableBooleanValue beingUpdatedProperty() { return model.beingUpdatedProperty(); } - public boolean isBeingUpdated() { return model.isBeingUpdated(); } - - // total width estimate - /** - * The estimated width of the entire document. Accurate when area does not wrap lines and - * uses the same font size throughout the entire area. Value is only supposed to be set by - * the skin, not the user. - */ - @Override - public Val totalWidthEstimateProperty() { return virtualFlow.totalWidthEstimateProperty(); } - public double getTotalWidthEstimate() { return virtualFlow.totalWidthEstimateProperty().getValue(); } - - // total height estimate - /** - * The estimated height of the entire document. Accurate when area does not wrap lines and - * uses the same font size throughout the entire area. Value is only supposed to be set by - * the skin, not the user. - */ - @Override - public Val totalHeightEstimateProperty() { return virtualFlow.totalHeightEstimateProperty(); } - public double getTotalHeightEstimate() { return virtualFlow.totalHeightEstimateProperty().getValue(); } - - /* ********************************************************************** * - * * - * Event streams * - * * - * ********************************************************************** */ - - // text changes - @Override public final EventStream plainTextChanges() { return model.plainTextChanges(); } - - // rich text changes - @Override public final EventStream> richChanges() { return model.richChanges(); } - - /* ********************************************************************** * - * * - * Private fields * - * * - * ********************************************************************** */ - - private final StyledTextAreaBehavior behavior; - - private Subscription subscriptions = () -> {}; - - private final Binding caretVisible; - - private final Val> _popupAnchorAdjustment; - - private final VirtualFlow, Cell, ParagraphBox>> virtualFlow; - - // used for two-level navigation, where on the higher level are - // paragraphs and on the lower level are lines within a paragraph - private final TwoLevelNavigator navigator; - - private boolean followCaretRequested = false; - - /** - * model - */ - private final StyledTextAreaModel model; - - /** - * @return this area's {@link StyledTextAreaModel} - */ - final StyledTextAreaModel getModel() { - return model; - } - - /* ********************************************************************** * - * * - * Fields necessary for Cloning * - * * - * ********************************************************************** */ - - /** - * The underlying document that can be displayed by multiple {@code StyledTextArea}s. - */ - public final EditableStyledDocument getContent() { return model.getContent(); } - - /** - * Style used by default when no other style is provided. - */ - public final S getInitialTextStyle() { return model.getInitialTextStyle(); } - - /** - * Style used by default when no other style is provided. - */ - public final PS getInitialParagraphStyle() { return model.getInitialParagraphStyle(); } - - /** - * Style applicator used by the default skin. - */ - private final BiConsumer applyStyle; - public final BiConsumer getApplyStyle() { return applyStyle; } - - /** - * Style applicator used by the default skin. - */ - private final BiConsumer applyParagraphStyle; - public final BiConsumer getApplyParagraphStyle() { return applyParagraphStyle; } - - /** - * Indicates whether style should be preserved on undo/redo, - * copy/paste and text move. - * TODO: Currently, only undo/redo respect this flag. - */ - public final boolean isPreserveStyle() { return model.isPreserveStyle(); } - - private final SegmentOps segmentOps; - public final SegmentOps getSegmentOps() { return segmentOps; } - - - /* ********************************************************************** * - * * - * Constructors * - * * - * ********************************************************************** */ - - /** - * Creates a text area with empty text content. - * - * @param initialTextStyle style to use in places where no other style is - * specified (yet). - * @param applyStyle function that, given a {@link Text} node and - * a style, applies the style to the text node. This function is - * used by the default skin to apply style to text nodes. - * @param initialParagraphStyle style to use in places where no other style is - * specified (yet). - * @param applyParagraphStyle function that, given a {@link TextFlow} node and - * a style, applies the style to the paragraph node. This function is - * used by the default skin to apply style to paragraph nodes. - */ - public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, SegmentOps segmentOps, BiConsumer applyStyle - ) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, true); - } - - public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, SegmentOps segmentOps, BiConsumer applyStyle, - boolean preserveStyle - ) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, - new SimpleEditableStyledDocument<>(initialParagraphStyle, initialTextStyle, segmentOps), preserveStyle); - } - - /** - * The same as {@link #StyledTextArea(Object, BiConsumer, Object, BiConsumer)} except that - * this constructor can be used to create another {@code StyledTextArea} object that - * shares the same {@link EditableStyledDocument}. - */ - public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, - SegmentOps segmentOps, BiConsumer applyStyle, - EditableStyledDocument document) { - this(initialParagraphStyle, applyParagraphStyle, initialTextStyle, segmentOps, applyStyle, document, true); - } - - public GenericRichtextArea(PS initialParagraphStyle, BiConsumer applyParagraphStyle, - S initialTextStyle, - SegmentOps segmentOps, - BiConsumer applyStyle, - EditableStyledDocument document, boolean preserveStyle) { - this.model = new StyledTextAreaModel<>(initialParagraphStyle, initialTextStyle, segmentOps, document, preserveStyle); - this.applyStyle = applyStyle; - this.applyParagraphStyle = applyParagraphStyle; - this.segmentOps = segmentOps; - - // allow tab traversal into area - setFocusTraversable(true); - - this.setBackground(new Background(new BackgroundFill(Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY))); - getStyleClass().add("styled-text-area"); - getStylesheets().add(GenericRichtextArea.class.getResource("styled-text-area.css").toExternalForm()); - - // keeps track of currently used non-empty cells - @SuppressWarnings("unchecked") - ObservableSet> nonEmptyCells = FXCollections.observableSet(); - - // Initialize content - virtualFlow = VirtualFlow.createVertical( - getParagraphs(), - par -> { - Cell, ParagraphBox> cell = createCell( - par, - applyStyle, - applyParagraphStyle); - nonEmptyCells.add(cell.getNode()); - return cell.beforeReset(() -> nonEmptyCells.remove(cell.getNode())) - .afterUpdateItem(p -> nonEmptyCells.add(cell.getNode())); - }); - getChildren().add(virtualFlow); - - // initialize navigator - IntSupplier cellCount = () -> getParagraphs().size(); - IntUnaryOperator cellLength = i -> virtualFlow.getCell(i).getNode().getLineCount(); - navigator = new TwoLevelNavigator(cellCount, cellLength); - - // follow the caret every time the caret position or paragraphs change - EventStream caretPosDirty = invalidationsOf(caretPositionProperty()); - EventStream paragraphsDirty = invalidationsOf(getParagraphs()); - EventStream selectionDirty = invalidationsOf(selectionProperty()); - // need to reposition popup even when caret hasn't moved, but selection has changed (been deselected) - EventStream caretDirty = merge(caretPosDirty, paragraphsDirty, selectionDirty); - subscribeTo(caretDirty, x -> requestFollowCaret()); - - // whether or not to display the caret - EventStream blinkCaret = EventStreams.valuesOf(showCaretProperty()) - .flatMap(mode -> { - switch (mode) { - case ON: - return EventStreams.valuesOf(Val.constant(true)); - case OFF: - return EventStreams.valuesOf(Val.constant(false)); - default: - case AUTO: - return EventStreams.valuesOf(focusedProperty() - .and(editableProperty()) - .and(disabledProperty().not())); - } - }); - - // the rate at which to display the caret - EventStream blinkRate = EventStreams.valuesOf(caretBlinkRate); - - // The caret is visible in periodic intervals, - // but only when blinkCaret is true. - caretVisible = EventStreams.combine(blinkCaret, blinkRate) - .flatMap(tuple -> { - Boolean blink = tuple.get1(); - javafx.util.Duration rate = tuple.get2(); - if(blink) { - return rate.lessThanOrEqualTo(ZERO) - ? EventStreams.valuesOf(Val.constant(true)) - : booleanPulse(rate, caretDirty); - } else { - return EventStreams.valuesOf(Val.constant(false)); - } - }) - .toBinding(false); - manageBinding(caretVisible); - - // Adjust popup anchor by either a user-provided function, - // or user-provided offset, or don't adjust at all. - Val> userOffset = Val.map( - popupAnchorOffsetProperty(), - offset -> anchor -> anchor.add(offset)); - _popupAnchorAdjustment = - Val.orElse( - popupAnchorAdjustmentProperty(), - userOffset) - .orElseConst(UnaryOperator.identity()); - - // dispatch MouseOverTextEvents when mouseOverTextDelay is not null - EventStreams.valuesOf(mouseOverTextDelayProperty()) - .flatMap(delay -> delay != null - ? mouseOverTextEvents(nonEmptyCells, delay) - : EventStreams.never()) - .subscribe(evt -> Event.fireEvent(this, evt)); - - behavior = new StyledTextAreaBehavior(this); - } - - - /* ********************************************************************** * - * * - * Queries * - * * - * Queries are parameterized observables. * - * * - * ********************************************************************** */ - - /** - * Returns caret bounds relative to the viewport, i.e. the visual bounds - * of the embedded VirtualFlow. - */ - Optional getCaretBounds() { - return virtualFlow.getCellIfVisible(getCurrentParagraph()) - .map(c -> { - Bounds cellBounds = c.getNode().getCaretBounds(); - return virtualFlow.cellToViewport(c, cellBounds); - }); - } - - /** - * Returns x coordinate of the caret in the current paragraph. - */ - ParagraphBox.CaretOffsetX getCaretOffsetX() { - int idx = getCurrentParagraph(); - return getCell(idx).getCaretOffsetX(); - } - - double getViewportHeight() { - return virtualFlow.getHeight(); - } - - CharacterHit hit(ParagraphBox.CaretOffsetX x, TwoDimensional.Position targetLine) { - int parIdx = targetLine.getMajor(); - ParagraphBox cell = virtualFlow.getCell(parIdx).getNode(); - CharacterHit parHit = cell.hitTextLine(x, targetLine.getMinor()); - return parHit.offset(getParagraphOffset(parIdx)); - } - - CharacterHit hit(ParagraphBox.CaretOffsetX x, double y) { - VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(0.0, y); - if(hit.isBeforeCells()) { - return CharacterHit.insertionAt(0); - } else if(hit.isAfterCells()) { - return CharacterHit.insertionAt(getLength()); - } else { - int parIdx = hit.getCellIndex(); - int parOffset = getParagraphOffset(parIdx); - ParagraphBox cell = hit.getCell().getNode(); - Point2D cellOffset = hit.getCellOffset(); - CharacterHit parHit = cell.hitText(x, cellOffset.getY()); - return parHit.offset(parOffset); - } - } - - /** - * Helpful for determining which letter is at point x, y: - *

-     *     {@code
-     *     StyledTextArea area = // creation code
-     *     area.addEventHandler(MouseEvent.MOUSE_PRESSED, (MouseEvent e) -> {
-     *         CharacterHit hit = area.hit(e.getX(), e.getY());
-     *         int characterPosition = hit.getInsertionIndex();
-     *
-     *         // move the caret to that character's position
-     *         area.moveTo(characterPosition, SelectionPolicy.CLEAR);
-     *     }}
-     * 
- */ - public CharacterHit hit(double x, double y) { - VirtualFlowHit, ParagraphBox>> hit = virtualFlow.hit(x, y); - if(hit.isBeforeCells()) { - return CharacterHit.insertionAt(0); - } else if(hit.isAfterCells()) { - return CharacterHit.insertionAt(getLength()); - } else { - int parIdx = hit.getCellIndex(); - int parOffset = getParagraphOffset(parIdx); - ParagraphBox cell = hit.getCell().getNode(); - Point2D cellOffset = hit.getCellOffset(); - CharacterHit parHit = cell.hit(cellOffset); - return parHit.offset(parOffset); - } - } - - /** - * Returns the current line as a two-level index. - * The major number is the paragraph index, the minor - * number is the line number within the paragraph. - * - *

This method has a side-effect of bringing the current - * paragraph to the viewport if it is not already visible. - */ - TwoDimensional.Position currentLine() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - int lineIdx = cell.getNode().getCurrentLineIndex(); - return _position(parIdx, lineIdx); - } - - TwoDimensional.Position _position(int par, int line) { - return navigator.position(par, line); - } - - @Override - public final String getText(int start, int end) { - return model.getText(start, end); - } - - @Override - public String getText(int paragraph) { - return model.getText(paragraph); - } - - public Paragraph getParagraph(int index) { - return model.getParagraph(index); - } - - @Override - public StyledDocument subDocument(int start, int end) { - return model.subDocument(start, end); - } - - @Override - public StyledDocument subDocument(int paragraphIndex) { - return model.subDocument(paragraphIndex); - } - - /** - * Returns the selection range in the given paragraph. - */ - public IndexRange getParagraphSelection(int paragraph) { - return model.getParagraphSelection(paragraph); - } - - /** - * Returns the style of the character with the given index. - * If {@code index} points to a line terminator character, - * the last style used in the paragraph terminated by that - * line terminator is returned. - */ - public S getStyleOfChar(int index) { - return model.getStyleOfChar(index); - } - - /** - * Returns the style at the given position. That is the style of the - * character immediately preceding {@code position}, except when - * {@code position} points to a paragraph boundary, in which case it - * is the style at the beginning of the latter paragraph. - * - *

In other words, most of the time {@code getStyleAtPosition(p)} - * is equivalent to {@code getStyleOfChar(p-1)}, except when {@code p} - * points to a paragraph boundary, in which case it is equivalent to - * {@code getStyleOfChar(p)}. - */ - public S getStyleAtPosition(int position) { - return model.getStyleAtPosition(position); - } - - /** - * 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. If {@code position} - * points to a boundary between two paragraphs, then the first styled range - * of the latter paragraph is returned. - */ - public IndexRange getStyleRangeAtPosition(int position) { - return model.getStyleRangeAtPosition(position); - } - - /** - * Returns the styles in the given character range. - */ - public StyleSpans getStyleSpans(int from, int to) { - return model.getStyleSpans(from, to); - } - - /** - * Returns the styles in the given character range. - */ - public StyleSpans getStyleSpans(IndexRange range) { - return getStyleSpans(range.getStart(), range.getEnd()); - } - - /** - * Returns the style of the character with the given index in the given - * paragraph. If {@code index} is beyond the end of the paragraph, the - * style at the end of line is returned. If {@code index} is negative, it - * is the same as if it was 0. - */ - public S getStyleOfChar(int paragraph, int index) { - return model.getStyleOfChar(paragraph, index); - } - - /** - * Returns the style at the given position in the given paragraph. - * This is equivalent to {@code getStyleOfChar(paragraph, position-1)}. - */ - public S getStyleAtPosition(int paragraph, int position) { - return model.getStyleAtPosition(paragraph, position); - } - - /** - * Returns the range of homogeneous style that includes the given position - * in the given paragraph. If {@code position} points to a boundary between - * two styled ranges, then the range preceding {@code position} is returned. - */ - public IndexRange getStyleRangeAtPosition(int paragraph, int position) { - return model.getStyleRangeAtPosition(paragraph, position); - } - - /** - * Returns styles of the whole paragraph. - */ - public StyleSpans getStyleSpans(int paragraph) { - return model.getStyleSpans(paragraph); - } - - /** - * Returns the styles in the given character range of the given paragraph. - */ - public StyleSpans getStyleSpans(int paragraph, int from, int to) { - return model.getStyleSpans(paragraph, from, to); - } - - /** - * Returns the styles in the given character range of the given paragraph. - */ - public StyleSpans getStyleSpans(int paragraph, IndexRange range) { - return getStyleSpans(paragraph, range.getStart(), range.getEnd()); - } - - @Override - public int getAbsolutePosition(int paragraphIndex, int columnIndex) { - return model.getAbsolutePosition(paragraphIndex, columnIndex); - } - - @Override - public Position position(int row, int col) { - return model.position(row, col); - } - - @Override - public Position offsetToPosition(int charOffset, Bias bias) { - return model.offsetToPosition(charOffset, bias); - } - - - /* ********************************************************************** * - * * - * Actions * - * * - * Actions change the state of this control. They typically cause a * - * change of one or more observables and/or produce an event. * - * * - * ********************************************************************** */ - - void scrollBy(Point2D deltas) { - virtualFlow.scrollXBy(deltas.getX()); - virtualFlow.scrollYBy(deltas.getY()); - } - - void show(double y) { - virtualFlow.show(y); - } - - void showCaretAtBottom() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double y = caretBounds.getMaxY(); - virtualFlow.showAtOffset(parIdx, getViewportHeight() - y); - } - - void showCaretAtTop() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double y = caretBounds.getMinY(); - virtualFlow.showAtOffset(parIdx, -y); - } - - void requestFollowCaret() { - followCaretRequested = true; - requestLayout(); - } - - private void followCaret() { - int parIdx = getCurrentParagraph(); - Cell, ParagraphBox> cell = virtualFlow.getCell(parIdx); - Bounds caretBounds = cell.getNode().getCaretBounds(); - double graphicWidth = cell.getNode().getGraphicPrefWidth(); - Bounds region = extendLeft(caretBounds, graphicWidth); - virtualFlow.show(parIdx, region); - } - - /** - * Sets style for the given character range. - */ - public void setStyle(int from, int to, S style) { - model.setStyle(from, to, style); - } - - /** - * Sets style for the whole paragraph. - */ - public void setStyle(int paragraph, S style) { - model.setStyle(paragraph, style); - } - - /** - * Sets style for the given range relative in the given paragraph. - */ - public void setStyle(int paragraph, int from, int to, S style) { - model.setStyle(paragraph, from, to, style); - } - - /** - * Set multiple style ranges at once. This is equivalent to - *

-     * for(StyleSpan{@code } span: styleSpans) {
-     *     setStyle(from, from + span.getLength(), span.getStyle());
-     *     from += span.getLength();
-     * }
-     * 
- * but the actual implementation is more efficient. - */ - public void setStyleSpans(int from, StyleSpans styleSpans) { - model.setStyleSpans(from, styleSpans); - } - - /** - * Set multiple style ranges of a paragraph at once. This is equivalent to - *
-     * for(StyleSpan{@code } span: styleSpans) {
-     *     setStyle(paragraph, from, from + span.getLength(), span.getStyle());
-     *     from += span.getLength();
-     * }
-     * 
- * but the actual implementation is more efficient. - */ - public void setStyleSpans(int paragraph, int from, StyleSpans styleSpans) { - model.setStyleSpans(paragraph, from, styleSpans); - } - - /** - * Sets style for the whole paragraph. - */ - public void setParagraphStyle(int paragraph, PS paragraphStyle) { - model.setParagraphStyle(paragraph, paragraphStyle); - } - - /** - * Resets the style of the given range to the initial style. - */ - public void clearStyle(int from, int to) { - model.clearStyle(from, to); - } - - /** - * Resets the style of the given paragraph to the initial style. - */ - public void clearStyle(int paragraph) { - model.clearStyle(paragraph); - } - - /** - * Resets the style of the given range in the given paragraph - * to the initial style. - */ - public void clearStyle(int paragraph, int from, int to) { - model.clearStyle(paragraph, from, to); - } - - /** - * Resets the style of the given paragraph to the initial style. - */ - public void clearParagraphStyle(int paragraph) { - model.clearParagraphStyle(paragraph); - } - - @Override - public void replaceText(int start, int end, String text) { - model.replaceText(start, end, text); - } - - @Override - public void replace(int start, int end, StyledDocument replacement) { - model.replace(start, end, replacement); - } - - @Override - public void selectRange(int anchor, int caretPosition) { - model.selectRange(anchor, caretPosition); - } - - /** - * {@inheritDoc} - * @deprecated You probably meant to use {@link #moveTo(int)}. This method will be made - * package-private in the future - */ - @Deprecated - @Override - public void positionCaret(int pos) { - model.positionCaret(pos); - } - - /* ********************************************************************** * - * * - * Public API * - * * - * ********************************************************************** */ - - public void dispose() { - subscriptions.unsubscribe(); - model.dispose(); - virtualFlow.dispose(); - } - - /* ********************************************************************** * - * * - * Layout * - * * - * ********************************************************************** */ - - @Override - protected void layoutChildren() { - virtualFlow.resize(getWidth(), getHeight()); - if(followCaretRequested) { - followCaretRequested = false; - followCaret(); - } - - // position popup - PopupWindow popup = getPopupWindow(); - PopupAlignment alignment = getPopupAlignment(); - UnaryOperator adjustment = _popupAnchorAdjustment.getValue(); - if(popup != null) { - positionPopup(popup, alignment, adjustment); - } - } - - /* ********************************************************************** * - * * - * Private methods * - * * - * ********************************************************************** */ - - private Cell, ParagraphBox> createCell( - Paragraph paragraph, - BiConsumer applyStyle, - BiConsumer applyParagraphStyle) { - - ParagraphBox box = new ParagraphBox<>(paragraph, applyParagraphStyle, applyStyle, segmentOps); - - box.highlightFillProperty().bind(highlightFill); - box.highlightTextFillProperty().bind(highlightTextFill); - box.wrapTextProperty().bind(wrapTextProperty()); - box.graphicFactoryProperty().bind(paragraphGraphicFactoryProperty()); - box.graphicOffset.bind(virtualFlow.breadthOffsetProperty()); - - Val hasCaret = Val.combine( - box.indexProperty(), - currentParagraphProperty(), - (bi, cp) -> bi.intValue() == cp.intValue()); - - Subscription hasCaretPseudoClass = hasCaret.values().subscribe(value -> box.pseudoClassStateChanged(HAS_CARET, value)); - Subscription firstParPseudoClass = box.indexProperty().values().subscribe(idx -> box.pseudoClassStateChanged(FIRST_PAR, idx == 0)); - Subscription lastParPseudoClass = EventStreams.combine( - box.indexProperty().values(), - getParagraphs().sizeProperty().values() - ).subscribe(in -> in.exec((i, n) -> box.pseudoClassStateChanged(LAST_PAR, i == n-1))); - - // caret is visible only in the paragraph with the caret - Val cellCaretVisible = hasCaret.flatMap(x -> x ? caretVisible : Val.constant(false)); - box.caretVisibleProperty().bind(cellCaretVisible); - - // bind cell's caret position to area's caret column, - // when the cell is the one with the caret - box.caretPositionProperty().bind(hasCaret.flatMap(has -> has - ? caretColumnProperty() - : Val.constant(0))); - - // keep paragraph selection updated - ObjectBinding cellSelection = Bindings.createObjectBinding(() -> { - int idx = box.getIndex(); - return idx != -1 - ? getParagraphSelection(idx) - : GenericRichtextArea.EMPTY_RANGE; - }, selectionProperty(), box.indexProperty()); - box.selectionProperty().bind(cellSelection); - - return new Cell, ParagraphBox>() { - @Override - public ParagraphBox getNode() { - return box; - } - - @Override - public void updateIndex(int index) { - box.setIndex(index); - } - - @Override - public void dispose() { - box.highlightFillProperty().unbind(); - box.highlightTextFillProperty().unbind(); - box.wrapTextProperty().unbind(); - box.graphicFactoryProperty().unbind(); - box.graphicOffset.unbind(); - - hasCaretPseudoClass.unsubscribe(); - firstParPseudoClass.unsubscribe(); - lastParPseudoClass.unsubscribe(); - - box.caretVisibleProperty().unbind(); - box.caretPositionProperty().unbind(); - - box.selectionProperty().unbind(); - cellSelection.dispose(); - } - }; - } - - private ParagraphBox getCell(int index) { - return virtualFlow.getCell(index).getNode(); - } - - private EventStream mouseOverTextEvents(ObservableSet> cells, Duration delay) { - return merge(cells, c -> c.stationaryIndices(delay).map(e -> e.unify( - l -> l.map((pos, charIdx) -> MouseOverTextEvent.beginAt(c.localToScreen(pos), getParagraphOffset(c.getIndex()) + charIdx)), - r -> MouseOverTextEvent.end()))); - } - - private int getParagraphOffset(int parIdx) { - return position(parIdx, 0).toOffset(); - } - - private void positionPopup( - PopupWindow popup, - PopupAlignment alignment, - UnaryOperator adjustment) { - Optional bounds = null; - switch(alignment.getAnchorObject()) { - case CARET: bounds = getCaretBoundsOnScreen(); break; - case SELECTION: bounds = getSelectionBoundsOnScreen(); break; - } - bounds.ifPresent(b -> { - double x = 0, y = 0; - switch(alignment.getHorizontalAlignment()) { - case LEFT: x = b.getMinX(); break; - case H_CENTER: x = (b.getMinX() + b.getMaxX()) / 2; break; - case RIGHT: x = b.getMaxX(); break; - } - switch(alignment.getVerticalAlignment()) { - case TOP: y = b.getMinY(); - case V_CENTER: y = (b.getMinY() + b.getMaxY()) / 2; break; - case BOTTOM: y = b.getMaxY(); break; - } - Point2D anchor = adjustment.apply(new Point2D(x, y)); - popup.setAnchorX(anchor.getX()); - popup.setAnchorY(anchor.getY()); - }); - } - - private Optional getCaretBoundsOnScreen() { - return virtualFlow.getCellIfVisible(getCurrentParagraph()) - .map(c -> c.getNode().getCaretBoundsOnScreen()); - } - - private Optional getSelectionBoundsOnScreen() { - IndexRange selection = getSelection(); - if(selection.getLength() == 0) { - return getCaretBoundsOnScreen(); - } - - Bounds[] bounds = virtualFlow.visibleCells().stream() - .map(c -> c.getNode().getSelectionBoundsOnScreen()) - .filter(Optional::isPresent) - .map(Optional::get) - .toArray(Bounds[]::new); - - if(bounds.length == 0) { - return Optional.empty(); - } - double minX = Stream.of(bounds).mapToDouble(Bounds::getMinX).min().getAsDouble(); - double maxX = Stream.of(bounds).mapToDouble(Bounds::getMaxX).max().getAsDouble(); - double minY = Stream.of(bounds).mapToDouble(Bounds::getMinY).min().getAsDouble(); - double maxY = Stream.of(bounds).mapToDouble(Bounds::getMaxY).max().getAsDouble(); - return Optional.of(new BoundingBox(minX, minY, maxX-minX, maxY-minY)); - } - - private void subscribeTo(EventStream src, Consumer cOnsumer) { - manageSubscription(src.subscribe(cOnsumer)); - } - - private void manageSubscription(Subscription subscription) { - subscriptions = subscriptions.and(subscription); - } - - private void manageBinding(Binding binding) { - subscriptions = subscriptions.and(binding::dispose); - } - - private static Bounds extendLeft(Bounds b, double w) { - if(w == 0) { - return b; - } else { - return new BoundingBox( - b.getMinX() - w, b.getMinY(), - b.getWidth() + w, b.getHeight()); - } - } - - private static EventStream booleanPulse(javafx.util.Duration javafxDuration, EventStream restartImpulse) { - Duration duration = Duration.ofMillis(Math.round(javafxDuration.toMillis())); - EventStream ticks = EventStreams.restartableTicks(duration, restartImpulse); - return StateMachine.init(false) - .on(restartImpulse.withDefaultEvent(null)).transition((state, impulse) -> true) - .on(ticks).transition((state, tick) -> !state) - .toStateStream(); - } -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java index 1062d9e01..4554af545 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/ParagraphText.java @@ -37,7 +37,7 @@ public ObjectProperty highlightTextFillProperty() { public void setCaretPosition(int pos) { caretPosition.setValue(pos); } private final Val clampedCaretPosition; - private final ObjectProperty selection = new SimpleObjectProperty<>(GenericRichtextArea.EMPTY_RANGE); + private final ObjectProperty selection = new SimpleObjectProperty<>(StyledTextArea.EMPTY_RANGE); public ObjectProperty selectionProperty() { return selection; } public void setSelection(IndexRange sel) { selection.set(sel); } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java deleted file mode 100644 index c300c31ca..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/CustomObject.java +++ /dev/null @@ -1,105 +0,0 @@ -package org.fxmisc.richtext.model; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -/** - * This is the base class for custom objects in the model layer. - * Its String representation is always one character long and contains - * the "object replacement character" (\ufffc). - */ -public abstract class CustomObject implements Segment { - - protected S style; - - protected CustomObject() {} - - public CustomObject(S style) { - this.style = style; - } - - - @Override - public Segment subSequence(int start, int end) { - if (start == 0 && end == 1) { - return this; - } - return new StyledText<>("", style); - } - - - @Override - public Segment subSequence(int start) { - if (start == 1) { - return new StyledText<>("", style); - } - return this; - } - - - @Override - public CustomObject append(String str) { - throw new UnsupportedOperationException(); - // return new StyledText<>(text + str, style); - } - - - @Override - public CustomObject spliced(int from, int to, CharSequence replacement) { - throw new UnsupportedOperationException(); -/* String left = text.substring(0, from); - String right = text.substring(to); - return new StyledText<>(left + replacement + right, style);*/ - } - - - @Override - public int length() { - return 1; - } - - - @Override - public char charAt(int index) { - return getText().charAt(0); - } - - - @Override - public String getText() { - return "\ufffc"; - } - - - @Override - public S getStyle() { - return style; - } - - @Override - public void setStyle(S style) { - this.style = style; - } - - public abstract void encode(DataOutputStream os) throws IOException; - - @Override - public final void encode(DataOutputStream os, Codec styleCodec) throws IOException { - encode(os); - styleCodec.encode(os, style); - } - - public abstract void decode(DataInputStream is) throws IOException; - - @Override - public final void decode(DataInputStream is, Codec styleCodec) throws IOException { - decode(is); - style = styleCodec.decode(is); - } - - @Override - public boolean canJoin(Segment right) { - return false; - } -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java index 562715c17..f932e377e 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/EditableStyledDocument.java @@ -8,7 +8,7 @@ import org.reactfx.value.Val; /** - * Content model for {@link org.fxmisc.richtext.GenericRichtextArea}. Implements edit operations + * Content model for {@link org.fxmisc.richtext.GenericStyledArea}. Implements edit operations * on styled text, but not worrying about additional aspects such as * caret or selection, which are handled by {@link StyledTextAreaModel}. */ diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java deleted file mode 100644 index 7d4887add..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/LinkedImage.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.fxmisc.richtext.model; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.File; -import java.io.IOException; -import java.util.function.Function; - -import javafx.scene.Node; - - -/** - * A custom object which contains a file path to an image file. - * When rendered in the rich text editor, the image is loaded from the - * specified file. - */ -public class LinkedImage extends CustomObject { - - private String imagePath; - - LinkedImage() {} - - /** - * 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 LinkedImage(String imagePath, S style) { - super(style); - - // if the image is below the current working directory, - // then store as relative path name. - String currentDir = System.getProperty("user.dir") + File.separatorChar; - if (imagePath.startsWith(currentDir)) { - imagePath = imagePath.substring(currentDir.length()); - } - - this.imagePath = imagePath; - } - - - /** - * @return The path of the image to render. - */ - public String getImagePath() { - return imagePath; - } - - - @Override - public void encode(DataOutputStream os) throws IOException { - Codec.STRING_CODEC.encode(os, imagePath); - } - - - @Override - public void decode(DataInputStream is) throws IOException { - imagePath = Codec.STRING_CODEC.decode(is); - } - - - @Override - public String toString() { - return String.format("LinkedImage[path=%s]", imagePath); - } - - - @SuppressWarnings("rawtypes") - private static Function nodeFactory; - - @Override - public Node createNode() { - return nodeFactory.apply(this); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static void setNodeFactory(Function, Node> nodeFactory) { - LinkedImage.nodeFactory = (Function) (Object) nodeFactory; - } -} 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 1187caed0..6eec29924 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/ReadOnlyStyledDocument.java @@ -248,11 +248,6 @@ public StyledDocument subSequence(int start, int end) { return split(end)._1.split(start)._2; } - @Override - public StyledDocument subDocument(int paragraphIndex) { - return new ReadOnlyStyledDocument<>(Collections.singletonList(getParagraphs().get(paragraphIndex)), segmentOps); - } - public Tuple3, RichTextChange, MaterializedListModification>> replace( int from, int to, ReadOnlyStyledDocument replacement) { return replace(from, to, x -> replacement); diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java deleted file mode 100644 index 46d71c85a..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Segment.java +++ /dev/null @@ -1,40 +0,0 @@ -package org.fxmisc.richtext.model; - -import java.io.DataInputStream; -import java.io.DataOutputStream; -import java.io.IOException; - -import javafx.scene.Node; - -/** - * An interface to segment types, like StyledText or LinkedImage. - */ -public interface Segment { - - int length(); - - char charAt(int index); - - String getText(); // each segment has a string associated with it - for custom objects - // this is the replacement character \ufffc - - Segment subSequence(int start, int end); - - Segment subSequence(int start); - - Segment append(String str); - - Segment spliced(int from, int to, CharSequence replacement); - - S getStyle(); - - void encode(DataOutputStream os, Codec styleCodec) throws IOException; - - void decode(DataInputStream is, Codec styleCodec) throws IOException; - - Node createNode(); - - boolean canJoin(Segment right); - - void setStyle(S style); -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java index 436188597..07a507358 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledDocument.java @@ -3,6 +3,7 @@ import static org.fxmisc.richtext.model.TwoDimensional.Bias.*; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import javafx.scene.control.IndexRange; diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck deleted file mode 100644 index 918c81abd..000000000 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java.bck +++ /dev/null @@ -1,127 +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.function.Function; - -import javafx.scene.Node; - -public class StyledText implements Segment { - private String text; - private S style; - - StyledText() {} - - public StyledText(String text, S style) { - this.text = text; - this.style = style; - } - - @Override - public int length() { - return text.length(); - } - - @Override - public char charAt(int index) { - return text.charAt(index); - } - - @Override - public String getText() { - return text; - } - - @Override - public StyledText subSequence(int start, int end) { - return new StyledText<>(text.substring(start, end), style); - } - - @Override - public StyledText subSequence(int start) { - return new StyledText<>(text.substring(start), style); - } - - @Override - public StyledText append(String str) { - return new StyledText<>(text + str, style); - } - - @Override - public StyledText spliced(int from, int to, CharSequence replacement) { - String left = text.substring(0, from); - String right = text.substring(to); - return new StyledText<>(left + replacement + right, style); - } - - @Override - public S getStyle() { - return style; - } - - @Override - public void setStyle(S style) { - 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 Objects.equals(this.text, that.text) - && Objects.equals(this.style, that.style); - } else { - return false; - } - } - - @Override - public int hashCode() { - return Objects.hash(text, style); - } - - - @Override - public boolean canJoin(Segment right) { - - if (right instanceof StyledText) { - return Objects.equals(getStyle(), right.getStyle()); - } - - return false; - } - - - @Override - public void encode(DataOutputStream os, Codec styleCodec) throws IOException { - Codec.STRING_CODEC.encode(os, getText()); - styleCodec.encode(os, style); - } - - @Override - public void decode(DataInputStream is, Codec styleCodec) throws IOException { - text = Codec.STRING_CODEC.decode(is); - style = styleCodec.decode(is); - } - - @SuppressWarnings("rawtypes") - private static Function nodeFactory; - - @Override - public Node createNode() { - return nodeFactory.apply(this); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - public static void setNodeFactory(Function, Node> nodeFactory) { - StyledText.nodeFactory = (Function) (Object) nodeFactory; - } - -} diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java index c5fd6892f..a91b37018 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledTextAreaModel.java @@ -28,7 +28,7 @@ import org.reactfx.value.Var; /** - * Model for {@link org.fxmisc.richtext.GenericRichtextArea} + * Model for {@link org.fxmisc.richtext.GenericStyledArea} * * @param type of style that can be applied to text. * @param type of style that can be applied to Paragraph diff --git a/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java b/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java deleted file mode 100644 index d383e16d1..000000000 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/CustomObjectTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package org.fxmisc.richtext.model; - -import static org.junit.Assert.*; - -import java.util.List; - -import org.junit.Test; - -import static org.hamcrest.CoreMatchers.instanceOf; -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.MatcherAssert.assertThat; - -public class CustomObjectTest { - - @Test - public void testLinkedImageCreation() { - SimpleEditableStyledDocument doc = - new SimpleEditableStyledDocument<>(true, ""); - StyledDocument customObj = - ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); - doc.replace(0, 0, customObj); - assertEquals(1, doc.getLength()); - - Paragraph para = doc.getParagraphs().get(0); - Object x = para.getSegments().get(0); - assertThat(x, instanceOf(LinkedImage.class)); - } - - @Test - public void testMultipleSegments() { - SimpleEditableStyledDocument doc = new SimpleEditableStyledDocument<>(true, ""); - - final String helloWorld = "Hello World"; - final String helloMoon = "Hello Moon"; - - StyledDocument text = ReadOnlyStyledDocument.fromString(helloWorld, true, ""); - doc.replace(0, 0, text); - - StyledDocument customObj = ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); - doc.replace(doc.getLength(), doc.getLength(), customObj); - - StyledDocument text2 = ReadOnlyStyledDocument.fromString(helloMoon, true, ""); - doc.replace(doc.getLength(), doc.getLength(), text2); - - // Assert that the document now contains one paragraph with three segments - - assertThat(doc.getParagraphs().size(), equalTo(1)); - - Paragraph p = doc.getParagraphs().get(0); - List> segs = p.getSegments(); - - assertThat(segs.size(), equalTo(3)); - assertThat(segs.get(0).getText(), equalTo(helloWorld)); - assertThat(segs.get(1).getText(), equalTo("\ufffc")); - assertThat(segs.get(2).getText(), equalTo(helloMoon)); - } - - - @Test - public void testRestyle() { - SimpleEditableStyledDocument doc = new SimpleEditableStyledDocument<>(true, ""); - - final String helloWorld = "Hello World"; - final String helloMoon = "Hello Moon"; - - StyledDocument text = ReadOnlyStyledDocument.fromString(helloWorld, true, "bold"); - doc.replace(0, 0, text); - - StyledDocument customObj = ReadOnlyStyledDocument.from(new LinkedImage("sample.png", ""), true); - doc.replace(doc.getLength(), doc.getLength(), customObj); - - StyledDocument text2 = ReadOnlyStyledDocument.fromString(helloMoon, true, "bold"); - doc.replace(doc.getLength(), doc.getLength(), text2); - - // The document now contains one paragraph with three segments - // Restyle part of the document: - - StyleSpans styles = doc.getStyleSpans(6, 17); - assertThat("Invalid number of Spans", styles.getSpanCount(), equalTo(3)); - - StyleSpans newStyles = styles.mapStyles(style -> "italic"); - doc.setStyleSpans(6, newStyles); - - // Assert that the document now contains one paragraph with five segments - // StyledText[text="Hello ", style=bold] - // StyledText[text="World", style=italic] - // LinkedImage[path=sample.png] - // StyledText[text="Hello", style=italic] - // StyledText[text=" Moon", style=bold] - - assertThat(doc.getParagraphs().size(), equalTo(1)); - List> segs = doc.getParagraphs().get(0).getSegments(); - - assertThat(segs.size(), equalTo(5)); - assertThat(segs.get(0).getText(), equalTo("Hello ")); - assertThat(segs.get(1).getText(), equalTo("World")); - assertThat(segs.get(2).getText(), equalTo("\ufffc")); - assertThat(segs.get(2), instanceOf(LinkedImage.class)); - assertThat(segs.get(3).getText(), equalTo("Hello")); - assertThat(segs.get(4).getText(), equalTo(" Moon")); - } - -} From a74f431068f2408d3cfecf7782671f790b0820dc Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Mon, 28 Nov 2016 21:28:45 -0800 Subject: [PATCH 7/9] Fix duplicate image inserted at end of paragraph bug - To insure paragraph creation always has at least 1 segment, added a method to SegmentOps interface to create an empty segment. - EitherOps defaults to left ops when creating an empty segment --- .../demo/richtext/LinkedImageOps.java | 21 +++++++++++++++++-- .../org/fxmisc/richtext/model/Paragraph.java | 6 ++++++ .../org/fxmisc/richtext/model/SegmentOps.java | 8 ++++++- .../org/fxmisc/richtext/model/StyledText.java | 5 +++++ .../org/fxmisc/richtext/model/TextOps.java | 1 + .../model/ReadOnlyStyledDocumentTest.java | 10 ++++----- .../SimpleEditableStyledDocumentTest.java | 1 - 7 files changed, 43 insertions(+), 9 deletions(-) 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 a8c7869f9..b8f1a6711 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 @@ -23,12 +23,25 @@ public String getText(LinkedImage seg) { @Override public LinkedImage subSequence(LinkedImage seg, int start, int end) { - return seg; + if (start < 0) { + throw new IllegalArgumentException("Start cannot be negative. Start = " + start); + } + if (end > length(seg)) { + throw new IllegalArgumentException("End cannot be greater than segment's length"); + } + return start == 0 && end == 1 + ? seg + : createEmpty(); } @Override public LinkedImage subSequence(LinkedImage seg, int start) { - return seg; + if (start < 0) { + throw new IllegalArgumentException("Start cannot be negative. Start = " + start); + } + return start == 0 + ? seg + : createEmpty(); } @Override @@ -46,4 +59,8 @@ public Optional> join(LinkedImage currentSeg, LinkedImage n return Optional.empty(); } + @Override + public LinkedImage createEmpty() { + return new LinkedImage("", null); + } } 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 989f4bd0c..ad985bb16 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/Paragraph.java @@ -133,6 +133,9 @@ public Paragraph trim(int length) { List segs = new ArrayList<>(segIdx + 1); segs.addAll(segments.subList(0, segIdx)); segs.add(segmentOps.subSequence(segments.get(segIdx), 0, pos.getMinor())); + if (segs.isEmpty()) { + segs.add(segmentOps.createEmpty()); + } return new Paragraph<>(paragraphStyle, segmentOps, segs); } } @@ -148,6 +151,9 @@ public Paragraph subSequence(int start) { List segs = new ArrayList<>(segments.size() - segIdx); segs.add(segmentOps.subSequence(segments.get(segIdx), pos.getMinor())); segs.addAll(segments.subList(segIdx + 1, segments.size())); + if (segs.isEmpty()) { + segs.add(segmentOps.createEmpty()); + } return new Paragraph<>(paragraphStyle, segmentOps, segs); } else { throw new IndexOutOfBoundsException(start + " not in [0, " + length() + "]"); 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 8c0f72a8d..e63815b40 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java @@ -27,6 +27,8 @@ public interface SegmentOps { public Optional join(SEG currentSeg, SEG nextSeg); + public SEG createEmpty(); + public default SegmentOps, S> or(SegmentOps rOps) { return either(this, rOps); } @@ -36,7 +38,7 @@ public default TextOps, S> or_(TextOps rOps) { } public static SegmentOps, S> either(SegmentOps lOps, SegmentOps rOps) { - return new EitherSegmentOps(lOps, rOps); + return new EitherSegmentOps<>(lOps, rOps); } } @@ -96,4 +98,8 @@ 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 Either createEmpty() { + return Either.left(lOps.createEmpty()); + } } \ No newline at end of file diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java index 600d32a69..83acf0888 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java @@ -53,6 +53,11 @@ public Optional> join(StyledText left, StyledText right) { : Optional.empty(); } + @Override + public StyledText createEmpty() { + return new StyledText<>("", null); + } + @Override public StyledText create(String text, S style) { return new StyledText<>(text, style); 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 fb00fc0b9..077f23d54 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/TextOps.java @@ -16,6 +16,7 @@ public static TextOps, S> eitherL(TextOps lOps, Seg public static TextOps, S> eitherR(SegmentOps lOps, TextOps rOps) { return new RightTextOps<>(lOps, rOps); } + } class LeftTextOps extends EitherSegmentOps implements TextOps, S> { 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 888c782e0..6f70e41cb 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/ReadOnlyStyledDocumentTest.java @@ -18,7 +18,7 @@ public void testUndo() { doc0.replace(0, 0, fromString("abcd", "Y", "Y", segOps)).exec((doc1, chng1, pchng1) -> { // undo chng1 - doc1.replace(chng1.getPosition(), chng1.getInsertionEnd(), from(chng1.getRemoved(), ops)).exec((doc2, chng2, pchng2) -> { + doc1.replace(chng1.getPosition(), chng1.getInsertionEnd(), from(chng1.getRemoved())).exec((doc2, chng2, pchng2) -> { // we should have arrived at the original document assertEquals(doc0, doc2); @@ -52,13 +52,13 @@ public void testRestyle() { SimpleEditableStyledDocument doc0 = new SimpleEditableStyledDocument<>("", ""); - ReadOnlyStyledDocument text = fromString(fooBar, "", "bold"); + ReadOnlyStyledDocument, String> text = fromString(fooBar, "", "bold", segOps); doc0.replace(doc0.getLength(), doc0.getLength(), text); - text = fromString(and, "", ""); + text = fromString(and, "", "", segOps); doc0.replace(doc0.getLength(), doc0.getLength(), text); - text = fromString(helloWorld, "", "bold"); + text = fromString(helloWorld, "", "bold", segOps); doc0.replace(doc0.getLength(), doc0.getLength(), text); StyleSpans styles = doc0.getStyleSpans(4, 17); @@ -71,7 +71,7 @@ public void testRestyle() { // StyledText[text="Foo ", style=bold] // StyledText[text="Bar and Hello", style=italic] // StyledText[text=" World", style=bold] - List> result = doc0.getParagraphs().get(0).getSegments(); + 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")); 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 8c41c9479..6d68369b3 100644 --- a/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java +++ b/richtextfx/src/test/java/org/fxmisc/richtext/model/SimpleEditableStyledDocumentTest.java @@ -10,7 +10,6 @@ public class SimpleEditableStyledDocumentTest { /** * The style of the inserted text will be the style at position * {@code start} in the current document. - * @param ops */ private void replaceText(EditableStyledDocument, String> doc, int start, int end, String text) { StyledDocument, String> styledDoc = ReadOnlyStyledDocument.fromString( From fcaff7c89b8ffb3c0d511dc6e2a531ab0780d248 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Mon, 28 Nov 2016 21:46:15 -0800 Subject: [PATCH 8/9] Use method reference --- .../src/main/java/org/fxmisc/richtext/model/SegmentOps.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 e63815b40..6a6281d04 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/SegmentOps.java @@ -83,8 +83,8 @@ public Either subSequence(Either seg, int start) { @Override public S getStyle(Either seg) { - return seg.unify(l -> lOps.getStyle(l), - r -> rOps.getStyle(r)); + return seg.unify(lOps::getStyle, + rOps::getStyle); } @Override From c1dbaba055077c5cc479c18cb9d25d29de4420c8 Mon Sep 17 00:00:00 2001 From: Jordan Martinez Date: Wed, 30 Nov 2016 10:39:15 -0800 Subject: [PATCH 9/9] Fix bug: length does not account for an empty segment; reduce memory usage --- .../richtext/demo/richtext/LinkedImageOps.java | 16 +++++++++------- .../org/fxmisc/richtext/model/StyledText.java | 12 +++++++----- 2 files changed, 16 insertions(+), 12 deletions(-) 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 b8f1a6711..c286c14ef 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 @@ -6,19 +6,21 @@ public class LinkedImageOps implements SegmentOps, S> { + private final LinkedImage emptySeg = new LinkedImage<>("", null); + @Override public int length(LinkedImage seg) { - return 1; + return seg == emptySeg ? 0 : 1; } @Override public char charAt(LinkedImage seg, int index) { - return '\ufffc'; + return seg == emptySeg ? '\0' : '\ufffc'; } @Override public String getText(LinkedImage seg) { - return "\ufffc"; + return seg == emptySeg ? "" : "\ufffc"; } @Override @@ -31,7 +33,7 @@ public LinkedImage subSequence(LinkedImage seg, int start, int end) { } return start == 0 && end == 1 ? seg - : createEmpty(); + : emptySeg; } @Override @@ -41,7 +43,7 @@ public LinkedImage subSequence(LinkedImage seg, int start) { } return start == 0 ? seg - : createEmpty(); + : emptySeg; } @Override @@ -51,7 +53,7 @@ public S getStyle(LinkedImage seg) { @Override public LinkedImage setStyle(LinkedImage seg, S style) { - return seg.setStyle(style); + return seg == emptySeg ? emptySeg : seg.setStyle(style); } @Override @@ -61,6 +63,6 @@ public Optional> join(LinkedImage currentSeg, LinkedImage n @Override public LinkedImage createEmpty() { - return new LinkedImage("", null); + return emptySeg; } } diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java index 83acf0888..14fd9c06a 100644 --- a/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java +++ b/richtextfx/src/main/java/org/fxmisc/richtext/model/StyledText.java @@ -11,6 +11,8 @@ public class StyledText { public static TextOps, S> textOps() { return new TextOps, S>() { + private final StyledText emptySeg = new StyledText<>("", null); + @Override public int length(StyledText styledText) { return styledText.getText().length(); @@ -18,7 +20,7 @@ public int length(StyledText styledText) { @Override public char charAt(StyledText styledText, int index) { - return styledText.getText().charAt(index); + return styledText == emptySeg ? '\0' : styledText.getText().charAt(index); } @Override @@ -28,12 +30,12 @@ public String getText(StyledText styledText) { @Override public StyledText subSequence(StyledText styledText, int start, int end) { - return new StyledText<>(styledText.getText().substring(start, end), styledText.getStyle()); + return styledText == emptySeg ? emptySeg : new StyledText<>(styledText.getText().substring(start, end), styledText.getStyle()); } @Override public StyledText subSequence(StyledText styledText, int start) { - return new StyledText<>(styledText.getText().substring(start), styledText.getStyle()); + return styledText == emptySeg ? emptySeg : new StyledText<>(styledText.getText().substring(start), styledText.getStyle()); } @Override @@ -43,7 +45,7 @@ public S getStyle(StyledText styledText) { @Override public StyledText setStyle(StyledText seg, S style) { - return seg.setStyle(style); + return seg == emptySeg ? emptySeg : seg.setStyle(style); } @Override @@ -55,7 +57,7 @@ public Optional> join(StyledText left, StyledText right) { @Override public StyledText createEmpty() { - return new StyledText<>("", null); + return emptySeg; } @Override