Making a JEditorPane with html put correctly formatted text in clipboard
Asked Answered
L

3

15

I have this code to demonstrate the problem:

public static void main(String[] args) {
    JFrame frame = new JFrame();
    frame.getContentPane().add(new JEditorPane("text/html", "Hello cruel world<br>\n<font color=red>Goodbye cruel world</font><br>\n<br>\nHello again<br>\n"));
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
}

If you select all the text that appears in the frame once the app starts, you can copy it and paste it into MS Word, Apple's Pages, or Mail and the text is formatted correctly. But if you paste it into a pure text editor such as TextEdit, Smultron, or a Skype chat window all the pasted content is on one line.

What can I do to make the text copied to the clipboard able to be pasted with newlines preserved?

I'm running my code on Mac OS X 10.7

Luminosity answered 12/10, 2011 at 18:57 Comment(3)
Could it be that TextEdit simply doesn't render things like that? What happens when you paste it into another text editor?Antung
@Shakedown, the problem is with other plain text editors, such as SmultronLuminosity
+1 good question, answerSteddman
L
20

After getting no answers, I rolled up my sleeves and did a lot of research and learning. The solution is to make a custom TransferHandler for the component, and massage the HTML text manually. It wasn't easy to work all this out, which could account for the zero answers I got.

Here's a working solution:

import javax.swing.*;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.ParserDelegator;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;

public class ScratchSpace {

    public static void main(String[] args) {
        final JFrame frame = new JFrame();
        final JEditorPane pane = new JEditorPane("text/html", "<html><font color=red>Hello</font><br>\u2663<br>World");
        pane.setTransferHandler(new MyTransferHandler());
        frame.getContentPane().add(pane);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

}

class MyTransferHandler extends TransferHandler {

    protected Transferable createTransferable(JComponent c) {
        final JEditorPane pane = (JEditorPane) c;
        final String htmlText = pane.getText();
        final String plainText = extractText(new StringReader(htmlText));
        return new MyTransferable(plainText, htmlText);
    }

    public String extractText(Reader reader) {
        final ArrayList<String> list = new ArrayList<String>();

        HTMLEditorKit.ParserCallback parserCallback = new HTMLEditorKit.ParserCallback() {
            public void handleText(final char[] data, final int pos) {
                list.add(new String(data));
            }

            public void handleStartTag(HTML.Tag tag, MutableAttributeSet attribute, int pos) {
            }

            public void handleEndTag(HTML.Tag t, final int pos) {
            }

            public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, final int pos) {
                if (t.equals(HTML.Tag.BR)) {
                    list.add("\n");
                }
            }

            public void handleComment(final char[] data, final int pos) {
            }

            public void handleError(final String errMsg, final int pos) {
            }
        };
        try {
            new ParserDelegator().parse(reader, parserCallback, true);
        } catch (IOException e) {
            e.printStackTrace();
        }
        String result = "";
        for (String s : list) {
            result += s;
        }
        return result;
    }


    @Override
    public void exportToClipboard(JComponent comp, Clipboard clip, int action) throws IllegalStateException {
        if (action == COPY) {
            clip.setContents(this.createTransferable(comp), null);
        }
    }

    @Override
    public int getSourceActions(JComponent c) {
        return COPY;
    }

}

class MyTransferable implements Transferable {

    private static final DataFlavor[] supportedFlavors;

    static {
        try {
            supportedFlavors = new DataFlavor[]{
                    new DataFlavor("text/html;class=java.lang.String"),
                    new DataFlavor("text/plain;class=java.lang.String")
            };
        } catch (ClassNotFoundException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private final String plainData;
    private final String htmlData;

    public MyTransferable(String plainData, String htmlData) {
        this.plainData = plainData;
        this.htmlData = htmlData;
    }

    public DataFlavor[] getTransferDataFlavors() {
        return supportedFlavors;
    }

    public boolean isDataFlavorSupported(DataFlavor flavor) {
        for (DataFlavor supportedFlavor : supportedFlavors) {
            if (supportedFlavor == flavor) {
                return true;
            }
        }
        return false;
    }

    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
        if (flavor.equals(supportedFlavors[0])) {
            return htmlData;
        }
        if (flavor.equals(supportedFlavors[1])) {
            return plainData;
        }
        throw new UnsupportedFlavorException(flavor);
    }
}
Luminosity answered 16/10, 2011 at 16:47 Comment(0)
D
3

Note: this is not an answer to the question, just a comment with code to the answer by @Thorn, related to security restrictions

In webstartables with default permissions (that is, none ;-) you can ask the SecurityManager at runtime for a ClipboardService: it will popup a dialog asking the user once to allow (or disallow) the copy. With that, you can replace the default copy action in the textComponent. In SwingX demo we support pasting the code from the source area by:

/**
 * Replaces the editor's default copy action in security restricted
 * environments with one messaging the ClipboardService. Does nothing 
 * if not restricted.
 * 
 * @param editor the editor to replace 
 */
public static void replaceCopyAction(final JEditorPane editor) {
    if (!isRestricted()) return;
    Action safeCopy = new AbstractAction() {

        @Override
        public void actionPerformed(ActionEvent e) {
            try {
                ClipboardService cs = (ClipboardService)ServiceManager.lookup
                    ("javax.jnlp.ClipboardService");
                StringSelection transferable = new StringSelection(editor.getSelectedText());
                cs.setContents(transferable);
            } catch (Exception e1) {
                // do nothing
            }
        }
    };
    editor.getActionMap().put(DefaultEditorKit.copyAction, safeCopy);
}

private static boolean isRestricted() {
    SecurityManager manager = System.getSecurityManager();
    if (manager == null) return false;
    try {
        manager.checkSystemClipboardAccess();
        return false;
    } catch (SecurityException e) {
        // nothing to do - not allowed to access
    }
    return true;
}
Defamation answered 2/12, 2011 at 14:15 Comment(0)
C
1

Thank you for your code post! I am working on getting an app up and running under JNLP that allows the user to create MLA citations and then copy/paste them into a word processor. So the formatting needs to be preserved.

See http://proctinator.com/citation/

There is an easier way, but I think I will need the sort of approach you demonstrate above to get my application working with jnlp.

The code below works find for a JEditorPane running in an unrestricted environment. But copy/paste is not directly available when you app is in a sandbox (such as would be the case for an applet or JNLP file that did not request full permissions.)

JEditorPane citEditorPane;
//user fills pane with MLA citations.
citEditorPane.selectAll();
citEditorPane.copy();
citEditorPane.select(0, 0);
Castigate answered 2/12, 2011 at 13:57 Comment(1)
as to copy permission for webstartable in the sandbox, see my not-an-answer-but-comment :-)Defamation

© 2022 - 2024 — McMap. All rights reserved.