Skip to content

Commit

Permalink
Support copy/paste with style, via Codec<Style>.
Browse files Browse the repository at this point in the history
Resolves #17.
  • Loading branch information
TomasMikula committed Sep 9, 2015
1 parent 7cbcd72 commit fe26405
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 6 deletions.
69 changes: 66 additions & 3 deletions richtextfx/src/main/java/org/fxmisc/richtext/ClipboardActions.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
package org.fxmisc.richtext;

import static org.fxmisc.richtext.ClipboardHelper.*;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Optional;

import javafx.scene.control.IndexRange;
import javafx.scene.input.Clipboard;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;

/**
* Clipboard actions for {@link TextEditingArea}.
*/
public interface ClipboardActions<S> extends EditActions<S> {

Optional<Codec<S>> getStyleCodec();

/**
* Transfers the currently selected text to the clipboard,
* removing the current selection.
Expand All @@ -24,10 +36,27 @@ default void cut() {
* leaving the current selection.
*/
default void copy() {
String selectedText = getSelectedText();
if (selectedText.length() > 0) {
IndexRange selection = getSelection();
if(selection.getLength() > 0) {
ClipboardContent content = new ClipboardContent();
content.putString(selectedText);

content.putString(getSelectedText());

getStyleCodec().ifPresent(styleCodec -> {
Codec<StyledDocument<S>> codec = ReadOnlyStyledDocument.codec(styleCodec);
DataFormat format = dataFormat(codec.getName());
StyledDocument<S> doc = subDocument(selection.getStart(), selection.getEnd());
ByteArrayOutputStream os = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(os);
try {
codec.encode(dos, doc);
content.put(format, os.toByteArray());
} catch (IOException e) {
System.err.println("Codec error: Exception in encoding '" + codec.getName() + "':");
e.printStackTrace();
}
});

Clipboard.getSystemClipboard().setContent(content);
}
}
Expand All @@ -39,6 +68,29 @@ default void copy() {
*/
default void paste() {
Clipboard clipboard = Clipboard.getSystemClipboard();

if(getStyleCodec().isPresent()) {
Codec<S> styleCodec = getStyleCodec().get();
Codec<StyledDocument<S>> codec = ReadOnlyStyledDocument.codec(styleCodec);
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<S> doc = null;
try {
doc = codec.decode(dis);
} catch (IOException e) {
System.err.println("Codec error: Failed to decode '" + codec.getName() + "':");
e.printStackTrace();
}
if(doc != null) {
replaceSelection(doc);
return;
}
}
}

if (clipboard.hasString()) {
String text = clipboard.getString();
if (text != null) {
Expand All @@ -47,3 +99,14 @@ default void paste() {
}
}
}

class ClipboardHelper {
static DataFormat dataFormat(String name) {
DataFormat format = DataFormat.lookupMimeType(name);
if(format != null) {
return format;
} else {
return new DataFormat(name);
}
}
}
83 changes: 83 additions & 0 deletions richtextfx/src/main/java/org/fxmisc/richtext/Codec.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.fxmisc.richtext;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public interface Codec<T> {

String getName();
void encode(DataOutputStream os, T t) throws IOException;
T decode(DataInputStream is) throws IOException;


static final Codec<String> STRING_CODEC = new Codec<String>() {
private final Charset UTF8 = Charset.forName("UTF-8");

@Override
public String getName() {
return "string";
}

@Override
public void encode(DataOutputStream os, String s) throws IOException {
os.writeUTF(s);
}

@Override
public String decode(DataInputStream is) throws IOException {
return is.readUTF();
}
};

static <T> Codec<List<T>> listCodec(Codec<T> elemCodec) {
return SuperCodec.collectionListCodec(elemCodec);
}
}

interface SuperCodec<S, T extends S> extends Codec<T> {
void encodeSuper(DataOutputStream os, S s) throws IOException;

@Override
default void encode(DataOutputStream os, T t) throws IOException {
encodeSuper(os, t);
}

@SuppressWarnings("unchecked")
static <S, U extends S, T extends U> SuperCodec<S, U> upCast(SuperCodec<S, T> sc) {
return (SuperCodec<S, U>) sc;
}

static <T> SuperCodec<Collection<T>, List<T>> collectionListCodec(Codec<T> elemCodec) {
return new SuperCodec<Collection<T>, List<T>>() {

@Override
public String getName() {
return "list<" + elemCodec.getName() + ">";
}

@Override
public void encodeSuper(DataOutputStream os, Collection<T> col) throws IOException {
os.writeInt(col.size());
for(T t: col) {
elemCodec.encode(os, t);
}
}

@Override
public List<T> decode(DataInputStream is) throws IOException {
int size = is.readInt();
List<T> elems = new ArrayList<>(size);
for(int i = 0; i < size; ++i) {
T t = elemCodec.decode(is);
elems.add(t);
}
return elems;
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public InlineCssTextArea(String text) {
getUndoManager().forgetHistory();
getUndoManager().mark();

setStyleCodec(Codec.STRING_CODEC);

// position the caret at the beginning
selectRange(0, 0);
}
Expand Down
4 changes: 2 additions & 2 deletions richtextfx/src/main/java/org/fxmisc/richtext/Paragraph.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ private Paragraph(StyledText<S> text, Optional<LineTerminator> terminator) {
this(Arrays.asList(text), terminator);
}

private Paragraph(List<StyledText<S>> segments, LineTerminator terminator) {
Paragraph(List<StyledText<S>> segments, LineTerminator terminator) {
this(segments, Optional.of(terminator));
}

private Paragraph(List<StyledText<S>> segments, Optional<LineTerminator> terminator) {
Paragraph(List<StyledText<S>> segments, Optional<LineTerminator> terminator) {
assert !segments.isEmpty();
this.segments = segments;
this.terminator = terminator;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
package org.fxmisc.richtext;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

public class ReadOnlyStyledDocument<S> extends StyledDocumentBase<S, List<Paragraph<S>>> {

Expand All @@ -11,6 +15,96 @@ static enum ParagraphsPolicy {
COPY,
}

static <S> Codec<StyledDocument<S>> codec(Codec<S> styleCodec) {
return new Codec<StyledDocument<S>>() {
private final Codec<List<Paragraph<S>>> codec = Codec.listCodec(paragraphCodec(styleCodec));

@Override
public String getName() {
return "application/richtextfx-styled-document<" + styleCodec.getName() + ">";
}

@Override
public void encode(DataOutputStream os, StyledDocument<S> doc) throws IOException {
codec.encode(os, doc.getParagraphs());
}

@Override
public StyledDocument<S> decode(DataInputStream is) throws IOException {
return new ReadOnlyStyledDocument<>(
codec.decode(is),
ParagraphsPolicy.ADOPT);
}

};
}

private static <S> Codec<Paragraph<S>> paragraphCodec(Codec<S> styleCodec) {
return new Codec<Paragraph<S>>() {
private final Codec<List<StyledText<S>>> segmentsCodec = Codec.listCodec(styledTextCodec(styleCodec));
private final byte LT_NONE = 0;
private final byte LT_CR = 1;
private final byte LT_LF = 2;
private final byte LT_CRLF = 3;

@Override
public String getName() {
return "paragraph<" + styleCodec.getName() + ">";
}

@Override
public void encode(DataOutputStream os, Paragraph<S> p) throws IOException {
segmentsCodec.encode(os, p.getSegments());
if(p.isTerminated()) {
switch(p.getLineTerminator().get()) {
case CR: os.writeByte(LT_CR); break;
case LF: os.writeByte(LT_LF); break;
case CRLF: os.writeByte(LT_CRLF); break;
}
} else {
os.writeByte(LT_NONE);
}
}

@Override
public Paragraph<S> decode(DataInputStream is) throws IOException {
List<StyledText<S>> segments = segmentsCodec.decode(is);
byte lt = is.readByte();
switch(lt) {
case LT_NONE: return new Paragraph<>(segments, Optional.empty());
case LT_CR: return new Paragraph<>(segments, LineTerminator.CR);
case LT_LF: return new Paragraph<>(segments, LineTerminator.LF);
case LT_CRLF: return new Paragraph<>(segments, LineTerminator.CRLF);
}
throw new IllegalArgumentException("unexpected byte value");
}
};
}

private static <S> Codec<StyledText<S>> styledTextCodec(Codec<S> styleCodec) {
return new Codec<StyledText<S>>() {

@Override
public String getName() {
return "styledtext<" + styleCodec.getName() + ">";
}

@Override
public void encode(DataOutputStream os, StyledText<S> t) throws IOException {
STRING_CODEC.encode(os, t.toString());
styleCodec.encode(os, t.getStyle());
}

@Override
public StyledText<S> decode(DataInputStream is) throws IOException {
String text = STRING_CODEC.decode(is);
S style = styleCodec.decode(is);
return new StyledText<>(text, style);
}

};
}

private int length = -1;

private String text = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@
*/
public class StyleClassedTextArea extends StyledTextArea<Collection<String>> {

public <C> StyleClassedTextArea(boolean preserveStyle) {
public StyleClassedTextArea(boolean preserveStyle) {
super(Collections.<String>emptyList(),
(text, styleClasses) -> text.getStyleClass().addAll(styleClasses),
preserveStyle);

setStyleCodec(SuperCodec.upCast(SuperCodec.collectionListCodec(Codec.STRING_CODEC)));
}

/**
Expand Down
13 changes: 13 additions & 0 deletions richtextfx/src/main/java/org/fxmisc/richtext/StyledTextArea.java
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,19 @@ public void setUndoManager(UndoManagerFactory undoManagerFactory) {
public boolean getUseInitialStyleForInsertion() { return content.useInitialStyleForInsertion.get(); }
public BooleanProperty useInitialStyleForInsertionProperty() { return content.useInitialStyleForInsertion; }

private Optional<Codec<S>> styleCodec = Optional.empty();
/**
* Sets the codec to encode/decode style information to/from binary format.
* Providing a codec enables clipboard actions to retain the style information.
*/
public void setStyleCodec(Codec<S> codec) {
styleCodec = Optional.of(codec);
}
@Override
public Optional<Codec<S>> getStyleCodec() {
return styleCodec;
}


/* ********************************************************************** *
* *
Expand Down

0 comments on commit fe26405

Please sign in to comment.