chrixm
chrixm

Reputation: 1024

Copy JTextArea as "text/html" DataFlavor

I have a JTextArea and I am using a Highlighter to apply some syntax highlighting to some of my text as per my SSCCE below:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;

public class SSCCE extends JFrame {
  public SSCCE() {
    final JTextArea aMain = new JTextArea();
    aMain.setFont(new Font("Consolas", Font.PLAIN, 11));
    aMain.setMargin(new Insets(5, 5, 5, 5));
    aMain.setEditable(false);
    add(aMain);

    aMain.setText("The quick brown fox jumped over the lazy dog.");
    Highlighter h = aMain.getHighlighter();
    try {
      h.addHighlight(10, 15, new DefaultHighlighter.DefaultHighlightPainter(new Color(0xFFC800)));
    }
    catch (BadLocationException e) { 
      e.printStackTrace(); 
    }

    aMain.getActionMap().put("Copy", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        aMain.copy();
      }
    });
    aMain.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "Copy");

    setTitle("SSCCE");
    setSize(350, 150);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setLocationRelativeTo(null);
    setVisible(true);
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new SSCCE();
      }
    });
  }
}

When the user selects a portion of text and presses CTRL+C then I am calling the copy() method of the JTextArea class. This copies the text onto the system clipboard as Plain Text and I lose any highlighting I have applied to my text. I am looking for the ability to copy the style information which includes the highlights as either "text/html" or "text/rtf". I believe I need to use the Transferable and DataFlavor classes, but I am struggling putting something together - I don't know how to get the data from the JTextArea in the correct format to place onto the clipboard.

I am basically trying to copy the highlighting and then paste it into Microsoft Word or similar application with the highlighting intact. Is the style data available in the correct format or do I manually have to construct a HTML markup by enumerating all the highlights?

Upvotes: 6

Views: 2387

Answers (2)

chrixm
chrixm

Reputation: 1024

Based on the answer by MadProgrammer, which from a concept basis is spot on and has enabled me to come up with the following. The only difference between the two answers is my support of multiple DataFlavor's so a text/plain is copied as well as a text/html flavor. I have also provided an improved HTML markup routine which includes text which is outside a highlight but within the selection.

UPDATE 1: My original answer didn't handle situations where you have nested highlights - the solution can now be used to export any highlighted text within a JTextArea to any application which supports text/html or text/plain.

UPDATE 2: Now added support for all the different classes of DataFlavor's as per MadProgrammer's suggestion. I have also fixed an issue if we have overlapping highlights which start at the same location.

import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import javax.swing.*;
import javax.swing.text.*;

@SuppressWarnings("serial")

public class SSCCE extends JFrame {
  public SSCCE() {
    final JTextArea aMain = new JTextArea();
    aMain.setFont(new Font("Consolas", Font.PLAIN, 11));
    aMain.setMargin(new Insets(5, 5, 5, 5));
    aMain.setEditable(false);
    add(aMain);

    aMain.setText("The quick brown fox jumped over the lazy dog.");
    Highlighter h = aMain.getHighlighter();
    try {
      h.addHighlight(10, 15, new DefaultHighlighter.DefaultHighlightPainter(new Color(0xFFC800)));
    }
    catch (BadLocationException e) { 
      e.printStackTrace(); 
    }

    aMain.getActionMap().put("Copy", new AbstractAction() {
      public void actionPerformed(ActionEvent e) {
        Highlighter h = aMain.getHighlighter();
        Highlighter.Highlight[] hls = h.getHighlights();
        int start = aMain.getSelectionStart();
        int end = aMain.getSelectionEnd();
        Document d = aMain.getDocument();

        Arrays.sort(hls, new Comparator<Highlighter.Highlight>() {
          @Override
          public int compare(Highlighter.Highlight a, Highlighter.Highlight b) {
            int r = a.getStartOffset() - b.getStartOffset();
            if (r == 0) {
              r = (b.getEndOffset() - b.getStartOffset()) - (a.getEndOffset() - a.getStartOffset());
            }
            return r;
          }
        });

        try {
          StringBuilder sb = new StringBuilder();
          sb.append("<html><body>");
          sb.append("<pre style='font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace; font-size: 10pt'>");

          String s = d.getText(start, end - start);
          String as[] = s.replaceAll("\r?\n", "\n").split("(?!^)");
          Color ac[] = new Color[as.length];

          for (Highlighter.Highlight hl : hls) { 
            int hs = hl.getStartOffset();
            int he = hl.getEndOffset();

            if ((he > start) && (hs < end)) {
              Color c = ((DefaultHighlighter.DefaultHighlightPainter)hl.getPainter()).getColor();
              if ((c != null) && (aMain.getSelectionColor() != c)) {
                hs = (hs < start) ? start : hs;
                he = (he > end) ? end : he;

                for (int i = (hs - start); i < (he - start); i++) {
                  ac[i] = c;
                }
              }
            }
            else if (hs > end) {
              break;
            }
          }

          Color pc = null;
          for (int i = 0; i < as.length; i++) {
            if (ac[i] != pc) {
              if (pc != null) {
                sb.append("</span>");
              }
              if (ac[i] != null) {
                sb.append("<span style='background-color: " + String.format("#%02x%02x%02x", ac[i].getRed(), ac[i].getGreen(), ac[i].getBlue()) + "'>");
              }
              pc = ac[i];
            }
            sb.append(as[i].replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\n", "<br>"));
          }
          if (pc != null) {
            sb.append("</span>");
          }

          sb.append("</pre>");
          sb.append("</body></html>");

          Clipboard c = Toolkit.getDefaultToolkit().getSystemClipboard();
          c.setContents(new MyTransferable(s, sb.toString()), null);
        }
        catch (BadLocationException ex) {
          ex.printStackTrace();
          aMain.copy();
        }
      }
    });
    aMain.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "Copy");

    setTitle("SSCCE");
    setSize(350, 150);
    setDefaultCloseOperation(EXIT_ON_CLOSE);
    setLocationRelativeTo(null);
    setVisible(true);
  }

  private static class MyTransferable implements Transferable {
    private static ArrayList<DataFlavor> MyFlavors = new ArrayList<DataFlavor>();
    private String plain = null;
    private String html = null;

    static {
      try {
        for (String m : new String[]{"text/plain", "text/html"}) {
          MyFlavors.add(new DataFlavor(m + ";class=java.lang.String"));
          MyFlavors.add(new DataFlavor(m + ";class=java.io.Reader"));
          MyFlavors.add(new DataFlavor(m + ";class=java.io.InputStream;charset=utf-8"));
        }
      }
      catch (ClassNotFoundException e) {
        e.printStackTrace();
      }
    }

    public MyTransferable(String plain, String html) {
      this.plain = plain;
      this.html = html;
    }

    public DataFlavor[] getTransferDataFlavors() {
      return MyFlavors.toArray(new DataFlavor[MyFlavors.size()]);
    }

    public boolean isDataFlavorSupported(DataFlavor flavor) {
      return MyFlavors.contains(flavor);
    }

    public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
      String s = null;
      if (flavor.getMimeType().contains("text/plain")) {
        s = plain;
      }
      else if (flavor.getMimeType().contains("text/html")) {
        s = html;
      }
      if (s != null) {
        if (String.class.equals(flavor.getRepresentationClass())) {
          return s;
        }
        else if (Reader.class.equals(flavor.getRepresentationClass())) {
          return new StringReader(s);
        }
        else if (InputStream.class.equals(flavor.getRepresentationClass())) {
          return new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8));
        }
      }
      throw new UnsupportedFlavorException(flavor);
    }
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new SSCCE();
      }
    });
  }
}

Upvotes: 1

MadProgrammer
MadProgrammer

Reputation: 347314

Okay, so basically, because the highlighter is just "painted" over the JTextArea and doesn't actually adjust the style of the text, you kind of need to do it all yourself

Basically you need to:

  • Get a list of all the current highlights
  • Extract the text from the document
  • Wrap it html
  • Create a suitable HTML based transferable
  • Copy the html markup to the clipboard

Easy...

import java.awt.Color;
import java.awt.Font;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringBufferInputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Highlighter;

public class SSCCE extends JFrame {

    public SSCCE() {
        final JTextArea aMain = new JTextArea();
        aMain.setFont(new Font("Consolas", Font.PLAIN, 11));
        aMain.setMargin(new Insets(5, 5, 5, 5));
        aMain.setEditable(false);
        add(aMain);

        aMain.setText("The quick brown fox jumped over the lazy dog.");
        Highlighter h = aMain.getHighlighter();
        try {
            h.addHighlight(10, 15, new DefaultHighlighter.DefaultHighlightPainter(new Color(0xFFC800)));
        } catch (BadLocationException e) {
            e.printStackTrace();
        }

        aMain.getActionMap().put("Copy", new AbstractAction() {
            public void actionPerformed(ActionEvent e) {
                Highlighter h = aMain.getHighlighter();
                Highlighter.Highlight[] highlights = h.getHighlights();
                StringBuilder sb = new StringBuilder(64);
                sb.append("<html><body>");
                boolean markedUp = false;
                for (Highlighter.Highlight highlight : highlights) {
                    int start = highlight.getStartOffset();
                    int end = highlight.getEndOffset();

                    try {
                        String text = aMain.getDocument().getText(start, end - start);

                        sb.append("<span style = 'background-color: #FFC800'>");
                        sb.append(text);
                        sb.append("</span>");
                        markedUp = true;
                    } catch (BadLocationException ex) {
                        ex.printStackTrace();
                    }
                }
                sb.append("</body></html>");
                if (markedUp) {
                    Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
                    clipboard.setContents(new HtmlSelection(sb.toString()), null);
                }
            }
        });
        aMain.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_C, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()), "Copy");

        setTitle("SSCCE");
        setSize(350, 150);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
        setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new SSCCE();
            }
        });
    }

    private static class HtmlSelection implements Transferable {

        private static List<DataFlavor> htmlFlavors = new ArrayList<>(3);

        static {

            try {
                htmlFlavors.add(new DataFlavor("text/html;class=java.lang.String"));
                htmlFlavors.add(new DataFlavor("text/html;class=java.io.Reader"));
                htmlFlavors.add(new DataFlavor("text/html;charset=unicode;class=java.io.InputStream"));
            } catch (ClassNotFoundException ex) {
                ex.printStackTrace();
            }

        }

        private String html;

        public HtmlSelection(String html) {
            this.html = html;
        }

        public DataFlavor[] getTransferDataFlavors() {
            return (DataFlavor[]) htmlFlavors.toArray(new DataFlavor[htmlFlavors.size()]);
        }

        public boolean isDataFlavorSupported(DataFlavor flavor) {
            return htmlFlavors.contains(flavor);
        }

        public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException {
            if (String.class.equals(flavor.getRepresentationClass())) {
                return html;
            } else if (Reader.class.equals(flavor.getRepresentationClass())) {
                return new StringReader(html);
            } else if (InputStream.class.equals(flavor.getRepresentationClass())) {
                return new StringBufferInputStream(html);
            }
            throw new UnsupportedFlavorException(flavor);
        }
    }
}

I've pasted this to a text editor and opened in a browser and pasted into to Word

The above is based on Copy jTable row with its grid lines into excel/word documents

Upvotes: 7

Related Questions