Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add pasteDocument action and test #322

Merged
merged 10 commits into from
May 27, 2024
65 changes: 47 additions & 18 deletions rta/src/main/java/com/gluonhq/richtextarea/model/PieceTable.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -191,37 +192,65 @@ 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);
});
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<DecorationModel> getDecorationModelList() {
public List<DecorationModel> 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<DecorationModel> 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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -49,7 +49,7 @@ public interface TextBuffer {
String getText(int start, int end);
int getInternalPosition(int position);
Selection getInternalSelection(Selection selection);
List<DecorationModel> getDecorationModelList();
List<DecorationModel> getDecorationModelList(int start, int end);

CharacterIterator getCharacterIterator();
char charAt(int pos);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
* 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());
}
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
* 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 + "]";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -438,10 +439,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));
}
Expand All @@ -457,12 +458,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();
Expand All @@ -482,9 +489,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) {
Expand Down Expand Up @@ -735,10 +740,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);
Comment on lines 758 to +761
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 3 calls to getTextBuffer() in this method. Wondering if we should instead create a variable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not needed. In this class getTextBuffer() it is used 30+ times, without extracting variables. After all, it is just a getter from a property, with a null check.

}

void newDocument() {
Expand All @@ -758,7 +765,7 @@ void open(Document document) {
}

void save() {
Document currentDocument = getCurrentDocument();
Document currentDocument = getCurrentDocument(Selection.UNDEFINED);
undoStackSizeWhenSaved = getUndoStackSize();
savedProperty.set(true);
setDocument(currentDocument);
Expand Down
Loading
Loading