Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Making a JEditorPane with html put correctly formatted text in clipboard

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

like image 347
Steve McLeod Avatar asked Oct 12 '11 18:10

Steve McLeod


2 Answers

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);
    }
}
like image 196
Steve McLeod Avatar answered Oct 13 '22 14:10

Steve McLeod


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;
}
like image 27
kleopatra Avatar answered Oct 13 '22 14:10

kleopatra