diff --git a/rta/src/main/java/com/gluonhq/richtextarea/model/PieceTable.java b/rta/src/main/java/com/gluonhq/richtextarea/model/PieceTable.java index 9fc20fa..f721994 100644 --- a/rta/src/main/java/com/gluonhq/richtextarea/model/PieceTable.java +++ b/rta/src/main/java/com/gluonhq/richtextarea/model/PieceTable.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Gluon + * Copyright (c) 2022, 2024, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -59,6 +59,7 @@ public final class PieceTable extends AbstractTextBuffer { private final PieceCharacterIterator pieceCharacterIterator; TextDecoration decorationAtCaret; + private DecorationModel dm = null; /** * Creates a piece table using the original text of a document, and @@ -191,12 +192,16 @@ public Selection getInternalSelection(Selection selection) { walkPieces((p, i, tp) -> { Unit unit = p.getUnit(); int sbMin = sb.length(); - int sbMax = sbMin + unit.getText().length(); + int deltaMin = unit instanceof TextUnit ? + Math.max(0, start - sbMin) : 0; + int sbMax = Math.min(end, sbMin + unit.getText().length()); + int deltaMax = unit instanceof TextUnit ? + Math.max(0, (sbMin + unit.getText().length()) - end) : 0; if (sbMin <= start && start <= sbMax) { - s0 = tp; + s0 = tp + deltaMin; } if (sbMin <= end && end <= sbMax) { - s1 = tp + p.length; + s1 = tp + p.length - deltaMax; } sb.append(unit.getText()); return (s0 > -1 && s1 > -1); @@ -204,24 +209,48 @@ public Selection getInternalSelection(Selection selection) { return new Selection(s0, s1); } + /** + * Gets the list of decoration models that decorate the text between a starting point + * and an ending position. + * + * @param start start position within text, inclusive + * @param end end position within text, exclusive + * @throws IllegalArgumentException if start or end are not in index range of the text + * @return a list of {@link DecorationModel} + */ @Override - public List getDecorationModelList() { + public List getDecorationModelList(int start, int end) { + if (getTextLength() > 0 && !inRange(start, 0, getTextLength())) { + throw new IllegalArgumentException("Start index " + start + " is not in range [0, " + getTextLength() + ")"); + } + if (end < 0) { + throw new IllegalArgumentException("End index is not in range"); + } List mergedList = new ArrayList<>(); if (!pieces.isEmpty()) { - AtomicInteger start = new AtomicInteger(); - DecorationModel dm = null; - for (Piece piece : pieces) { - int length = piece.getUnit().getText().length(); - if (mergedList.isEmpty()) { - dm = new DecorationModel(start.addAndGet(piece.start), length, piece.getDecoration(), piece.getParagraphDecoration()); - } else if (piece.getDecoration().equals(dm.getDecoration()) && piece.getParagraphDecoration().equals(dm.getParagraphDecoration())) { - mergedList.remove(mergedList.size() - 1); - dm = new DecorationModel(dm.getStart(), dm.getLength() + length, dm.getDecoration(), dm.getParagraphDecoration()); - } else { - dm = new DecorationModel(start.addAndGet(dm.getLength()), length, piece.getDecoration(), piece.getParagraphDecoration()); + AtomicInteger accum = new AtomicInteger(); + StringBuilder sb = new StringBuilder(); + walkPieces((p, i, tp) -> { + Unit unit = p.getUnit(); + sb.append(p.getInternalText()); + if (start <= tp + p.length && end > tp && !unit.isEmpty()) { + String text = sb.substring(Math.max(start, tp), Math.min(end, tp + p.length)); + int length = 0; + if (!text.isEmpty()) { + length = (unit instanceof TextUnit ? text : unit.getText()).length(); + } + if (mergedList.isEmpty()) { + dm = new DecorationModel(0, length, p.getDecoration(), p.getParagraphDecoration()); + } else if (p.getDecoration().equals(dm.getDecoration()) && p.getParagraphDecoration().equals(dm.getParagraphDecoration())) { + mergedList.remove(mergedList.size() - 1); + dm = new DecorationModel(dm.getStart(), dm.getLength() + length, dm.getDecoration(), dm.getParagraphDecoration()); + } else { + dm = new DecorationModel(accum.addAndGet(dm.getLength()), length, p.getDecoration(), p.getParagraphDecoration()); + } + mergedList.add(dm); } - mergedList.add(dm); - } + return (end <= tp); + }); } return mergedList; } diff --git a/rta/src/main/java/com/gluonhq/richtextarea/model/TextBuffer.java b/rta/src/main/java/com/gluonhq/richtextarea/model/TextBuffer.java index 37d14b2..2664e90 100644 --- a/rta/src/main/java/com/gluonhq/richtextarea/model/TextBuffer.java +++ b/rta/src/main/java/com/gluonhq/richtextarea/model/TextBuffer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023, Gluon + * Copyright (c) 2022, 2024, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -49,7 +49,7 @@ public interface TextBuffer { String getText(int start, int end); int getInternalPosition(int position); Selection getInternalSelection(Selection selection); - List getDecorationModelList(); + List getDecorationModelList(int start, int end); CharacterIterator getCharacterIterator(); char charAt(int pos); diff --git a/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdFactory.java b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdFactory.java index 6a34012..800534b 100644 --- a/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdFactory.java +++ b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdFactory.java @@ -119,6 +119,10 @@ public ActionCmd selectAndInsertBlock(Selection selection, Block block) { return new ActionCmdInsertBlock(block, selection); } + public ActionCmd pasteDocument(Document document) { + return new ActionCmdPasteDocument(document); + } + public ActionCmd insertTable(TableDecoration tableDecoration) { return new ActionCmdTable(tableDecoration); } diff --git a/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdPaste.java b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdPaste.java index 07f5a7b..b238fbc 100644 --- a/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdPaste.java +++ b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdPaste.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, Gluon + * Copyright (c) 2022, 2024, Gluon * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -41,7 +41,7 @@ public void apply(RichTextAreaViewModel viewModel) { @Override public BooleanBinding getDisabledBinding(RichTextAreaViewModel viewModel) { - return Bindings.createBooleanBinding(() -> !(viewModel.clipboardHasString() || viewModel.clipboardHasImage() || viewModel.clipboardHasUrl()) + return Bindings.createBooleanBinding(() -> !(viewModel.clipboardHasDocument() || viewModel.clipboardHasString() || viewModel.clipboardHasImage() || viewModel.clipboardHasUrl()) || !viewModel.isEditable(), viewModel.caretPositionProperty(), viewModel.editableProperty()); } diff --git a/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdPasteDocument.java b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdPasteDocument.java new file mode 100644 index 0000000..fb28ea8 --- /dev/null +++ b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/ActionCmdPasteDocument.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2024, Gluon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.gluonhq.richtextarea.viewmodel; + +import com.gluonhq.richtextarea.model.Document; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; + +import java.util.Objects; + +class ActionCmdPasteDocument implements ActionCmd { + + private final Document document; + + public ActionCmdPasteDocument(Document document) { + this.document = document; + } + + @Override + public void apply(RichTextAreaViewModel viewModel) { + if (viewModel.isEditable()) { + viewModel.getCommandManager().execute(new PasteDocumentCmd(Objects.requireNonNull(document))); + } + } + + @Override + public BooleanBinding getDisabledBinding(RichTextAreaViewModel viewModel) { + return Bindings.createBooleanBinding(() -> !viewModel.clipboardHasDocument() || !viewModel.isEditable(), viewModel.editableProperty()); + } +} diff --git a/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/PasteDocumentCmd.java b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/PasteDocumentCmd.java new file mode 100644 index 0000000..be758c7 --- /dev/null +++ b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/PasteDocumentCmd.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2024, Gluon + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GLUON BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.gluonhq.richtextarea.viewmodel; + +import com.gluonhq.richtextarea.Selection; +import com.gluonhq.richtextarea.model.Document; + +import java.util.Objects; + +class PasteDocumentCmd extends AbstractEditCmd { + + private final Document content; + + public PasteDocumentCmd(Document content) { + this.content = Objects.requireNonNull(content); + } + + @Override + public void doRedo(RichTextAreaViewModel viewModel) { + Objects.requireNonNull(viewModel); + // Go through all decorations: inserting unit and decorating it + content.getDecorations().forEach(dm -> { + int caretPosition = viewModel.getCaretPosition(); + int initialLength = viewModel.getTextLength(); + // 1. insert unit + String text = content.getText().substring(dm.getStart(), dm.getStart() + dm.getLength()); + viewModel.insert(text); + // 1. decorate unit + int addedLength = viewModel.getTextLength() - initialLength; + Selection newSelection = new Selection(caretPosition, Math.min(caretPosition + addedLength, viewModel.getTextLength())); + viewModel.setSelection(newSelection); + viewModel.decorate(dm.getDecoration()); + // For now: ignore paragraph decoration +// viewModel.decorate(dm.getParagraphDecoration()); + viewModel.setSelection(Selection.UNDEFINED); + viewModel.setCaretPosition(newSelection.getEnd()); + }); + + } + + @Override + public void doUndo(RichTextAreaViewModel viewModel) { + Objects.requireNonNull(viewModel); + + content.getDecorations().forEach(dm -> { + // 1. remove unit + viewModel.undo(); + // 2. delete decoration + viewModel.undoDecoration(); + }); + } + + @Override + public String toString() { + return "PasteDocumentCmd[" + super.toString() + ", " + content + "]"; + } +} diff --git a/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/RichTextAreaViewModel.java b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/RichTextAreaViewModel.java index 7b718c5..5c52c95 100644 --- a/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/RichTextAreaViewModel.java +++ b/rta/src/main/java/com/gluonhq/richtextarea/viewmodel/RichTextAreaViewModel.java @@ -30,6 +30,7 @@ import com.gluonhq.richtextarea.Selection; import com.gluonhq.richtextarea.Tools; import com.gluonhq.richtextarea.model.Decoration; +import com.gluonhq.richtextarea.model.DecorationModel; import com.gluonhq.richtextarea.model.Document; import com.gluonhq.richtextarea.model.ImageDecoration; import com.gluonhq.richtextarea.model.Paragraph; @@ -451,10 +452,10 @@ private boolean removeSelection() { void clipboardCopy(final boolean cutText) { Selection selection = getSelection(); if (selection.isDefined()) { - String selectedText = getTextBuffer().getText(selection.getStart(), selection.getEnd()); + Document currentDocument = getCurrentDocument(selection); final ClipboardContent content = new ClipboardContent(); - content.put(RTA_DATA_FORMAT, selectedText); - content.putString(selectedText.replaceAll(TextBuffer.ZERO_WIDTH_NO_BREAK_SPACE_TEXT, "")); + content.put(RTA_DATA_FORMAT, currentDocument); + content.putString(currentDocument.getText().replaceAll(TextBuffer.ZERO_WIDTH_NO_BREAK_SPACE_TEXT, "")); if (cutText) { commandManager.execute(new RemoveTextCmd(0)); } @@ -470,12 +471,18 @@ boolean clipboardHasString() { return Clipboard.getSystemClipboard().hasString(); } + boolean clipboardHasDocument() { + return Clipboard.getSystemClipboard().hasContent(RTA_DATA_FORMAT); + } + boolean clipboardHasUrl() { return Clipboard.getSystemClipboard().hasUrl(); } void clipboardPaste() { - if (clipboardHasImage()) { + if (clipboardHasDocument()) { + commandManager.execute(new PasteDocumentCmd((Document) Clipboard.getSystemClipboard().getContent(RTA_DATA_FORMAT))); + } else if (clipboardHasImage()) { final Image image = Clipboard.getSystemClipboard().getImage(); if (image != null) { String url = image.getUrl() != null ? image.getUrl() : Clipboard.getSystemClipboard().getUrl(); @@ -495,9 +502,7 @@ void clipboardPaste() { } } else { String text = null; - if (Clipboard.getSystemClipboard().hasContent(RTA_DATA_FORMAT)) { - text = (String) Clipboard.getSystemClipboard().getContent(RTA_DATA_FORMAT); - } else if (clipboardHasString()) { + if (clipboardHasString()) { text = Clipboard.getSystemClipboard().getString(); } if (text != null) { @@ -748,10 +753,12 @@ private void updateProperties() { redoStackSizeProperty.set(commandManager.getRedoStackSize()); } - private Document getCurrentDocument() { + private Document getCurrentDocument(Selection selection) { // text and indices should be based on the exportable text int caret = getTextBuffer().getText(0, getCaretPosition()).length(); - return new Document(getTextBuffer().getText(), getTextBuffer().getDecorationModelList(), caret); + int start = selection.isDefined() ? selection.getStart() : 0; + int end = selection.isDefined() ? selection.getEnd() : getTextLength(); + return new Document(getTextBuffer().getText(start, end), getTextBuffer().getDecorationModelList(start, end), caret); } void newDocument() { @@ -771,7 +778,7 @@ void open(Document document) { } void save() { - Document currentDocument = getCurrentDocument(); + Document currentDocument = getCurrentDocument(Selection.UNDEFINED); undoStackSizeWhenSaved = getUndoStackSize(); savedProperty.set(true); setDocument(currentDocument); diff --git a/rta/src/test/java/com/gluonhq/richtextarea/model/UnitBufferTests.java b/rta/src/test/java/com/gluonhq/richtextarea/model/UnitBufferTests.java index 43cb697..327930d 100644 --- a/rta/src/test/java/com/gluonhq/richtextarea/model/UnitBufferTests.java +++ b/rta/src/test/java/com/gluonhq/richtextarea/model/UnitBufferTests.java @@ -32,8 +32,11 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import static javafx.scene.text.FontWeight.BOLD; + public class UnitBufferTests { private static final Document FACE_MODEL = new Document("One \ud83d\ude00 Text \ufeff@name\ufeff!"); @@ -120,6 +123,72 @@ public void selection() { Assertions.assertEquals(internalSelection.getEnd(), "One \u2063 Text \ufffc".length()); } + @Test + @DisplayName("Unit: selection") + public void simpleTokenSelection() { + TextDecoration td1 = TextDecoration.builder().presets().build(); + TextDecoration td2 = TextDecoration.builder().presets().fontWeight(BOLD).build(); + ParagraphDecoration pd = ParagraphDecoration.builder().presets().build(); + PieceTable pt = new PieceTable(new Document("this is bold end", List.of( + new DecorationModel(0, 8, td1, pd), + new DecorationModel(8, 4, td2, pd), + new DecorationModel(12, 4, td1, pd)), 10)); + StringBuilder text = new StringBuilder(); + StringBuilder internalText = new StringBuilder(); + String[] tokens = {"this ", "is ", "bol", "d", "end"}; + String[] internalTokens = {"this ", "is ", "bol", "d", "end"}; + for (int i = 0; i < tokens.length; i++) { + String token = tokens[i]; + int start = text.length(); + Selection selection = new Selection(start, start + token.length()); + Selection internalSelection = pt.getInternalSelection(selection); + Assertions.assertEquals(internalSelection.getStart(), internalText.length()); + Assertions.assertEquals(internalSelection.getEnd(), internalText.length() + internalTokens[i].length()); + text.append(token); + internalText.append(internalTokens[i]); + } + } + + @Test + @DisplayName("Unit: selection") + public void tokenSelection() { + PieceTable pt = new PieceTable(new Document("ne \ud83d\ude00 Text \ufeff@name\ufeff!")); + StringBuilder text = new StringBuilder(); + StringBuilder internalText = new StringBuilder(); + String[] tokens = {"ne \ud83d\ude00 ", "Text ", "\ufeff@name\ufeff", "!"}; + String[] internalTokens = {"ne \u2063 ", "Text ", "\ufffc", "!"}; + for (int i = 0; i < tokens.length; i++) { + String token = tokens[i]; + int start = text.length(); + Selection selection = new Selection(start, start + token.length()); + Selection internalSelection = pt.getInternalSelection(selection); + Assertions.assertEquals(internalSelection.getStart(), internalText.length()); + Assertions.assertEquals(internalSelection.getEnd(), internalText.length() + internalTokens[i].length()); + text.append(token); + internalText.append(internalTokens[i]); + } + } + + @Test + @DisplayName("Unit: selection") + public void moreTokenSelection() { + PieceTable pt = new PieceTable(new Document("Emoji: \uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F! and \ufeff@name\ufeff!")); + StringBuilder text = new StringBuilder(); + StringBuilder internalText = new StringBuilder(); + String[] tokens = {"Emo", "ji: \uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F!", " and \ufeff@name\ufeff", "!"}; + String[] internalTokens = {"Emo", "ji: \u2063!", " and \ufffc", "!"}; + for (int i = 0; i < tokens.length; i++) { + String token = tokens[i]; + int start = text.length(); + Selection selection = new Selection(start, start + token.length()); + Selection internalSelection = pt.getInternalSelection(selection); + Assertions.assertEquals(internalSelection.getStart(), internalText.length()); + Assertions.assertEquals(internalSelection.getEnd(), internalText.length() + internalTokens[i].length()); + text.append(token); + internalText.append(internalTokens[i]); + } + } + @Test @DisplayName("Unit: insert unit") public void insertUnits() { diff --git a/rta/src/test/java/com/gluonhq/richtextarea/ui/RTATest.java b/rta/src/test/java/com/gluonhq/richtextarea/ui/RTATest.java index fbc8654..483a70b 100644 --- a/rta/src/test/java/com/gluonhq/richtextarea/ui/RTATest.java +++ b/rta/src/test/java/com/gluonhq/richtextarea/ui/RTATest.java @@ -32,6 +32,7 @@ import com.gluonhq.emoji.EmojiSkinTone; import com.gluonhq.richtextarea.RichTextArea; import com.gluonhq.richtextarea.Selection; +import com.gluonhq.richtextarea.Tools; import com.gluonhq.richtextarea.action.TextDecorateAction; import com.gluonhq.richtextarea.model.DecorationModel; import com.gluonhq.richtextarea.model.Document; @@ -43,6 +44,7 @@ import javafx.scene.Scene; import javafx.scene.control.ToggleButton; import javafx.scene.image.ImageView; +import javafx.scene.input.Clipboard; import javafx.scene.input.KeyCodeCombination; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; @@ -70,7 +72,16 @@ import java.util.stream.Collectors; import static com.gluonhq.emoji.EmojiData.getEmojiCollection; +import static com.gluonhq.richtextarea.RichTextArea.RTA_DATA_FORMAT; import static javafx.scene.input.KeyCode.A; +import static javafx.scene.input.KeyCode.END; +import static javafx.scene.input.KeyCode.HOME; +import static javafx.scene.input.KeyCode.LEFT; +import static javafx.scene.input.KeyCode.RIGHT; +import static javafx.scene.input.KeyCode.Z; +import static javafx.scene.input.KeyCombination.ALT_DOWN; +import static javafx.scene.input.KeyCombination.CONTROL_DOWN; +import static javafx.scene.input.KeyCombination.SHIFT_DOWN; import static javafx.scene.input.KeyCombination.SHORTCUT_DOWN; import static javafx.scene.text.FontPosture.ITALIC; import static javafx.scene.text.FontPosture.REGULAR; @@ -79,6 +90,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.testfx.api.FxAssert.verifyThat; import static org.testfx.util.WaitForAsyncUtils.sleep; import static org.testfx.util.WaitForAsyncUtils.waitForFxEvents; @@ -354,10 +366,25 @@ public void emojiDemoTest(FxRobot robot) { "\u2063, \u2063, \u2063, \u2063, \u2063, \u2063, \u2063, \u2063, \u2063, \u2063.\n" + "And this is another emoji with skin tone: \u2063"; assertEquals(208, internalText.length()); - PieceTable pt = new PieceTable(rta.getDocument()); - StringBuilder internalSb = new StringBuilder(); - pt.walkFragments((u, d) -> internalSb.append(u.getInternalText()), 0, 208); - assertEquals(internalText, internalSb.toString()); + assertEquals(internalText, getInternalText(rta.getDocument(), 208)); + + assertEquals(3, rta.getDocument().getDecorations().size()); + DecorationModel dm1 = rta.getDocument().getDecorations().get(0); + assertEquals(0, dm1.getStart()); + assertEquals(25, dm1.getLength()); + assertInstanceOf(TextDecoration.class, dm1.getDecoration()); + assertEquals(16, ((TextDecoration) dm1.getDecoration()).getFontSize()); + assertEquals(BOLD, ((TextDecoration) dm1.getDecoration()).getFontWeight()); + DecorationModel dm2 = rta.getDocument().getDecorations().get(1); + assertEquals(25, dm2.getStart()); + assertEquals(138, dm2.getLength()); + assertInstanceOf(TextDecoration.class, dm2.getDecoration()); + assertEquals(NORMAL, ((TextDecoration) dm2.getDecoration()).getFontWeight()); + DecorationModel dm3 = rta.getDocument().getDecorations().get(2); + assertEquals(163, dm3.getStart()); + assertEquals(104, dm3.getLength()); + assertInstanceOf(TextDecoration.class, dm3.getDecoration()); + assertEquals(ITALIC, ((TextDecoration) dm3.getDecoration()).getFontPosture()); assertEquals(15, robot.lookup(node -> node instanceof ImageView).queryAll().size()); AtomicInteger counter = new AtomicInteger(); @@ -415,6 +442,390 @@ public void emojiDemoTest(FxRobot robot) { } assertEquals(0, noToneEmojis); assertEquals(2, mediumDarkToneEmojis); + + run(() -> richTextArea.getActionFactory().selectAll().execute(new ActionEvent())); + waitForFxEvents(); + + run(() -> richTextArea.getActionFactory().save().execute(new ActionEvent())); + waitForFxEvents(); + Document oldDocument = rta.getDocument(); + + run(() -> { + richTextArea.getActionFactory().copy().execute(new ActionEvent()); + richTextArea.getActionFactory().newDocument().execute(new ActionEvent()); + }); + waitForFxEvents(); + + run(() -> richTextArea.getActionFactory().paste().execute(new ActionEvent())); + waitForFxEvents(); + + Document newDocument = rta.getDocument(); + assertEquals(oldDocument.getText(), newDocument.getText()); + assertEquals(oldDocument.getDecorations(), newDocument.getDecorations()); + assertEquals(oldDocument.getCaretPosition(), newDocument.getCaretPosition()); + assertEquals(oldDocument, newDocument); + } + + @Test + public void copyPasteTest(FxRobot robot) { + run(() -> { + String text = "One \ud83d\ude00 Text \ufeff@name\ufeff!!"; + TextDecoration textDecoration1 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(20).build(); + TextDecoration textDecoration2 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(16).fontWeight(BOLD).build(); + TextDecoration textDecoration3 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(14).foreground("red").build(); + ParagraphDecoration paragraphDecoration = ParagraphDecoration.builder().presets().build(); + Document document = new Document(text, + List.of(new DecorationModel(0, 7, textDecoration1, paragraphDecoration), + new DecorationModel(7, 5, textDecoration2, paragraphDecoration), + new DecorationModel(12, 7, textDecoration3, paragraphDecoration), + new DecorationModel(19, 2, TextDecoration.builder().presets().fontFamily("Arial").build(), paragraphDecoration)), text.length()); + richTextArea.getActionFactory().open(document).execute(new ActionEvent()); + richTextArea.setAutoSave(true); + }); + waitForFxEvents(); + + RichTextArea rta = robot.lookup(".rich-text-area").query(); + + assertEquals(14, rta.getTextLength()); + assertEquals(21, rta.getDocument().getText().length()); + assertEquals(21, rta.getCaretPosition()); + assertEquals(21, rta.getDocument().getCaretPosition()); + assertEquals(4, rta.getDocument().getDecorations().size()); + + Document document = rta.getDocument(); + + robot.push(new KeyCodeCombination(HOME)); + assertEquals(0, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(4, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(7, rta.getCaretPosition()); + + run(() -> richTextArea.getActionFactory().copy().execute(new ActionEvent())); + waitForFxEvents(); + run(() -> assertTrue(Clipboard.getSystemClipboard().hasContent(RTA_DATA_FORMAT))); + waitForFxEvents(); + run(() -> { + Document copyDoc = (Document) Clipboard.getSystemClipboard().getContent(RTA_DATA_FORMAT); + assertNotNull(copyDoc); + assertEquals("One \ud83d\ude00 ", copyDoc.getText()); + }); + waitForFxEvents(); + + robot.push(new KeyCodeCombination(END)); + assertEquals(21, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(LEFT)); + assertEquals(20, rta.getCaretPosition()); + + run(() -> richTextArea.getActionFactory().paste().execute(new ActionEvent())); + waitForFxEvents(); + + String newText = "One \ud83d\ude00 Text \ufeff@name\ufeff!One \ud83d\ude00 !"; + assertEquals(newText, rta.getDocument().getText()); + assertEquals(20, rta.getTextLength()); + assertEquals(28, rta.getDocument().getText().length()); + assertEquals(27, rta.getCaretPosition()); + assertEquals(27, rta.getDocument().getCaretPosition()); + assertEquals(6, rta.getDocument().getDecorations().size()); + + DecorationModel dm1 = rta.getDocument().getDecorations().get(4); + assertEquals(20, dm1.getStart()); + assertEquals(7, dm1.getLength()); + assertInstanceOf(TextDecoration.class, dm1.getDecoration()); + assertEquals(20, ((TextDecoration) dm1.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm1.getDecoration()).getFontWeight()); + DecorationModel dm2 = rta.getDocument().getDecorations().get(5); + assertEquals(27, dm2.getStart()); + assertEquals(1, dm2.getLength()); + assertInstanceOf(TextDecoration.class, dm2.getDecoration()); + assertEquals(NORMAL, ((TextDecoration) dm2.getDecoration()).getFontWeight()); + + Document newDocument = rta.getDocument(); + + robot.push(new KeyCodeCombination(Z, SHORTCUT_DOWN)); + assertEquals(document, rta.getDocument()); + robot.push(new KeyCodeCombination(Z, SHORTCUT_DOWN, SHIFT_DOWN)); + assertEquals(newDocument, rta.getDocument()); + + robot.push(new KeyCodeCombination(RIGHT)); + + run(() -> richTextArea.getActionFactory().paste().execute(new ActionEvent())); + waitForFxEvents(); + + newText = "One \ud83d\ude00 Text \ufeff@name\ufeff!One \ud83d\ude00 !One \ud83d\ude00 "; + assertEquals(newText, rta.getDocument().getText()); + assertEquals(26, rta.getTextLength()); + assertEquals(35, rta.getDocument().getText().length()); + assertEquals(35, rta.getCaretPosition()); + assertEquals(35, rta.getDocument().getCaretPosition()); + assertEquals(7, rta.getDocument().getDecorations().size()); + dm1 = rta.getDocument().getDecorations().get(6); + assertEquals(28, dm1.getStart()); + assertEquals(7, dm1.getLength()); + assertInstanceOf(TextDecoration.class, dm1.getDecoration()); + assertEquals(20, ((TextDecoration) dm1.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm1.getDecoration()).getFontWeight()); + } + + @Test + public void copyPaste2Test(FxRobot robot) { + run(() -> { + String text = "One \ud83d\ude00 Text \ufeff@name\ufeff!!"; + TextDecoration textDecoration1 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(20).build(); + TextDecoration textDecoration2 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(16).fontWeight(BOLD).build(); + TextDecoration textDecoration3 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(14).foreground("red").build(); + ParagraphDecoration paragraphDecoration = ParagraphDecoration.builder().presets().build(); + Document document = new Document(text, + List.of(new DecorationModel(0, 7, textDecoration1, paragraphDecoration), + new DecorationModel(7, 5, textDecoration2, paragraphDecoration), + new DecorationModel(12, 7, textDecoration3, paragraphDecoration), + new DecorationModel(19, 2, TextDecoration.builder().presets().fontFamily("Arial").build(), paragraphDecoration)), text.length()); + richTextArea.getActionFactory().open(document).execute(new ActionEvent()); + richTextArea.setAutoSave(true); + }); + waitForFxEvents(); + + RichTextArea rta = robot.lookup(".rich-text-area").query(); + + assertEquals(14, rta.getTextLength()); + assertEquals(21, rta.getDocument().getText().length()); + assertEquals(21, rta.getCaretPosition()); + assertEquals(21, rta.getDocument().getCaretPosition()); + assertEquals(4, rta.getDocument().getDecorations().size()); + + Document document = rta.getDocument(); + + String internalText = "One \u2063 Text \ufffc!!"; + assertEquals(14, internalText.length()); + assertEquals(internalText, getInternalText(rta.getDocument(), 14)); + + robot.push(new KeyCodeCombination(HOME)).push(new KeyCodeCombination(RIGHT)); + assertEquals(1, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(4, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(7, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(12, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(19, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN)); + assertEquals(20, rta.getCaretPosition()); + run(() -> richTextArea.getActionFactory().copy().execute(new ActionEvent())); + waitForFxEvents(); + run(() -> assertTrue(Clipboard.getSystemClipboard().hasContent(RTA_DATA_FORMAT))); + waitForFxEvents(); + run(() -> { + Document copyDoc = (Document) Clipboard.getSystemClipboard().getContent(RTA_DATA_FORMAT); + assertNotNull(copyDoc); + assertEquals("ne \ud83d\ude00 Text \ufeff@name\ufeff!", copyDoc.getText()); + assertEquals(19, copyDoc.getText().length()); + String textCopy = "ne \u2063 Text \ufffc!"; + assertEquals(12, textCopy.length()); + assertEquals(textCopy, getInternalText(copyDoc, 12)); + assertEquals(4, copyDoc.getDecorations().size()); + DecorationModel dm1 = copyDoc.getDecorations().get(0); + assertEquals(0, dm1.getStart()); + assertEquals(6, dm1.getLength()); + assertInstanceOf(TextDecoration.class, dm1.getDecoration()); + assertEquals(20, ((TextDecoration) dm1.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm1.getDecoration()).getFontWeight()); + DecorationModel dm2 = copyDoc.getDecorations().get(1); + assertEquals(6, dm2.getStart()); + assertEquals(5, dm2.getLength()); + assertInstanceOf(TextDecoration.class, dm2.getDecoration()); + assertEquals(16, ((TextDecoration) dm2.getDecoration()).getFontSize()); + assertEquals(BOLD, ((TextDecoration) dm2.getDecoration()).getFontWeight()); + DecorationModel dm3 = copyDoc.getDecorations().get(2); + assertEquals(11, dm3.getStart()); + assertEquals(7, dm3.getLength()); + assertInstanceOf(TextDecoration.class, dm3.getDecoration()); + assertEquals(14, ((TextDecoration) dm3.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm3.getDecoration()).getFontWeight()); + assertEquals("red", ((TextDecoration) dm3.getDecoration()).getForeground()); + DecorationModel dm4 = copyDoc.getDecorations().get(3); + assertEquals(18, dm4.getStart()); + assertEquals(1, dm4.getLength()); + assertInstanceOf(TextDecoration.class, dm4.getDecoration()); + assertEquals(14, ((TextDecoration) dm4.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm3.getDecoration()).getFontWeight()); + }); + waitForFxEvents(); + + robot.push(new KeyCodeCombination(END)); + assertEquals(21, rta.getCaretPosition()); + + run(() -> richTextArea.getActionFactory().paste().execute(new ActionEvent())); + waitForFxEvents(); + + String newText = "One \ud83d\ude00 Text \ufeff@name\ufeff!!ne \ud83d\ude00 Text \ufeff@name\ufeff!"; + assertEquals(newText, rta.getDocument().getText()); + assertEquals(26, rta.getTextLength()); + assertEquals(40, rta.getDocument().getText().length()); + assertEquals(40, rta.getCaretPosition()); + assertEquals(40, rta.getDocument().getCaretPosition()); + assertEquals(8, rta.getDocument().getDecorations().size()); + + internalText = "One \u2063 Text \ufffc!!ne \u2063 Text \ufffc!"; + assertEquals(26, internalText.length()); + assertEquals(internalText, getInternalText(rta.getDocument(), 26)); + DecorationModel dm1 = rta.getDocument().getDecorations().get(4); + assertEquals(21, dm1.getStart()); + assertEquals(6, dm1.getLength()); + assertInstanceOf(TextDecoration.class, dm1.getDecoration()); + assertEquals(20, ((TextDecoration) dm1.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm1.getDecoration()).getFontWeight()); + DecorationModel dm2 = rta.getDocument().getDecorations().get(5); + assertEquals(27, dm2.getStart()); + assertEquals(5, dm2.getLength()); + assertInstanceOf(TextDecoration.class, dm2.getDecoration()); + assertEquals(16, ((TextDecoration) dm2.getDecoration()).getFontSize()); + assertEquals(BOLD, ((TextDecoration) dm2.getDecoration()).getFontWeight()); + DecorationModel dm3 = rta.getDocument().getDecorations().get(6); + assertEquals(32, dm3.getStart()); + assertEquals(7, dm3.getLength()); + assertInstanceOf(TextDecoration.class, dm3.getDecoration()); + assertEquals(14, ((TextDecoration) dm3.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm3.getDecoration()).getFontWeight()); + assertEquals("red", ((TextDecoration) dm3.getDecoration()).getForeground()); + DecorationModel dm4 = rta.getDocument().getDecorations().get(7); + assertEquals(39, dm4.getStart()); + assertEquals(1, dm4.getLength()); + assertInstanceOf(TextDecoration.class, dm4.getDecoration()); + assertEquals(14, ((TextDecoration) dm4.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm4.getDecoration()).getFontWeight()); + + Document newDocument = rta.getDocument(); + + robot.push(new KeyCodeCombination(Z, SHORTCUT_DOWN)); + assertEquals(document, rta.getDocument()); + robot.push(new KeyCodeCombination(Z, SHORTCUT_DOWN, SHIFT_DOWN)); + assertEquals(newDocument, rta.getDocument()); + } + + + @Test + public void copyPaste3Test(FxRobot robot) { + run(() -> { + String text = "Emoji: \uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F! and \ufeff@name\ufeff!"; + TextDecoration textDecoration1 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(20).build(); + TextDecoration textDecoration2 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(16).fontWeight(BOLD).build(); + TextDecoration textDecoration3 = TextDecoration.builder().presets().fontFamily("Arial").fontSize(14).foreground("red").build(); + ParagraphDecoration paragraphDecoration = ParagraphDecoration.builder().presets().build(); + Document document = new Document(text, + List.of(new DecorationModel(0, 3, textDecoration1, paragraphDecoration), + new DecorationModel(3, 19, textDecoration2, paragraphDecoration), + new DecorationModel(22, 12, textDecoration3, paragraphDecoration), + new DecorationModel(34, 1, TextDecoration.builder().presets().fontFamily("Arial").build(), paragraphDecoration)), text.length()); + richTextArea.getActionFactory().open(document).execute(new ActionEvent()); + richTextArea.setAutoSave(true); + }); + waitForFxEvents(); + RichTextArea rta = robot.lookup(".rich-text-area").query(); + String internalText = "Emoji: \u2063! and \ufffc!"; + assertEquals(16, internalText.length()); + assertEquals(internalText, getInternalText(rta.getDocument(), 16)); + + assertEquals(16, rta.getTextLength()); + assertEquals(35, rta.getDocument().getText().length()); + assertEquals(35, rta.getCaretPosition()); + assertEquals(35, rta.getDocument().getCaretPosition()); + assertEquals(4, rta.getDocument().getDecorations().size()); + + Document document = rta.getDocument(); + + robot.push(new KeyCodeCombination(HOME)).push(new KeyCodeCombination(RIGHT)).push(new KeyCodeCombination(RIGHT)); + assertEquals(2, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(5, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(7, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(21, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN, Tools.MAC ? ALT_DOWN : CONTROL_DOWN)); + assertEquals(23, rta.getCaretPosition()); + robot.push(new KeyCodeCombination(RIGHT, SHIFT_DOWN)); + assertEquals(24, rta.getCaretPosition()); + run(() -> richTextArea.getActionFactory().copy().execute(new ActionEvent())); + waitForFxEvents(); + + run(() -> assertTrue(Clipboard.getSystemClipboard().hasContent(RTA_DATA_FORMAT))); + waitForFxEvents(); + run(() -> { + Document copyDoc = (Document) Clipboard.getSystemClipboard().getContent(RTA_DATA_FORMAT); + assertNotNull(copyDoc); + assertEquals("oji: \uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F! a", copyDoc.getText()); + assertEquals(22, copyDoc.getText().length()); + String textCopy = "oji: \u2063! a"; + assertEquals(9, textCopy.length()); + assertEquals(textCopy, getInternalText(copyDoc, 9)); + assertEquals(3, copyDoc.getDecorations().size()); + DecorationModel dm1 = copyDoc.getDecorations().get(0); + assertEquals(0, dm1.getStart()); + assertEquals(1, dm1.getLength()); + assertInstanceOf(TextDecoration.class, dm1.getDecoration()); + assertEquals(20, ((TextDecoration) dm1.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm1.getDecoration()).getFontWeight()); + DecorationModel dm2 = copyDoc.getDecorations().get(1); + assertEquals(1, dm2.getStart()); + assertEquals(19, dm2.getLength()); + assertInstanceOf(TextDecoration.class, dm2.getDecoration()); + assertEquals(16, ((TextDecoration) dm2.getDecoration()).getFontSize()); + assertEquals(BOLD, ((TextDecoration) dm2.getDecoration()).getFontWeight()); + DecorationModel dm3 = copyDoc.getDecorations().get(2); + assertEquals(20, dm3.getStart()); + assertEquals(2, dm3.getLength()); + assertInstanceOf(TextDecoration.class, dm3.getDecoration()); + assertEquals(14, ((TextDecoration) dm3.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm3.getDecoration()).getFontWeight()); + assertEquals("red", ((TextDecoration) dm3.getDecoration()).getForeground()); + }); + waitForFxEvents(); + + robot.push(new KeyCodeCombination(END)).push(new KeyCodeCombination(LEFT)); + assertEquals(34, rta.getCaretPosition()); + + run(() -> richTextArea.getActionFactory().paste().execute(new ActionEvent())); + waitForFxEvents(); + + String newText = "Emoji: \uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F! and \ufeff@name\ufeffoji: \uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC73\uDB40\uDC63\uDB40\uDC74\uDB40\uDC7F! a!"; + assertEquals(newText, rta.getDocument().getText()); + assertEquals(25, rta.getTextLength()); + assertEquals(57, rta.getDocument().getText().length()); + assertEquals(56, rta.getCaretPosition()); + assertEquals(56, rta.getDocument().getCaretPosition()); + assertEquals(7, rta.getDocument().getDecorations().size()); + + internalText = "Emoji: \u2063! and \ufffcoji: \u2063! a!"; + assertEquals(25, internalText.length()); + assertEquals(internalText, getInternalText(rta.getDocument(), 25)); + DecorationModel dm1 = rta.getDocument().getDecorations().get(3); + assertEquals(34, dm1.getStart()); + assertEquals(1, dm1.getLength()); + assertInstanceOf(TextDecoration.class, dm1.getDecoration()); + assertEquals(20, ((TextDecoration) dm1.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm1.getDecoration()).getFontWeight()); + assertEquals(NORMAL, ((TextDecoration) dm1.getDecoration()).getFontWeight()); + DecorationModel dm2 = rta.getDocument().getDecorations().get(4); + assertEquals(35, dm2.getStart()); + assertEquals(19, dm2.getLength()); + assertInstanceOf(TextDecoration.class, dm2.getDecoration()); + assertEquals(16, ((TextDecoration) dm2.getDecoration()).getFontSize()); + assertEquals(BOLD, ((TextDecoration) dm2.getDecoration()).getFontWeight()); + DecorationModel dm3 = rta.getDocument().getDecorations().get(5); + assertEquals(54, dm3.getStart()); + assertEquals(2, dm3.getLength()); + assertInstanceOf(TextDecoration.class, dm3.getDecoration()); + assertEquals(14, ((TextDecoration) dm3.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm3.getDecoration()).getFontWeight()); + assertEquals("red", ((TextDecoration) dm3.getDecoration()).getForeground()); + DecorationModel dm4 = rta.getDocument().getDecorations().get(6); + assertEquals(56, dm4.getStart()); + assertEquals(1, dm4.getLength()); + assertInstanceOf(TextDecoration.class, dm4.getDecoration()); + assertEquals(14, ((TextDecoration) dm4.getDecoration()).getFontSize()); + assertEquals(NORMAL, ((TextDecoration) dm4.getDecoration()).getFontWeight()); + assertEquals("black", ((TextDecoration) dm4.getDecoration()).getForeground()); } @Test @@ -641,6 +1052,13 @@ private TextDecoration getStyleFromMarker(String marker) { return builder.build(); } + private String getInternalText(Document document, int end) { + PieceTable pt = new PieceTable(document); + StringBuilder internalSb = new StringBuilder(); + pt.walkFragments((u, d) -> internalSb.append(u.getInternalText()), 0, end); + return internalSb.toString(); + } + private void run(Runnable runnable) { CountDownLatch countDownLatch = new CountDownLatch(1); Platform.runLater(() -> {