Hello @[email protected] <[email protected]>,

Me and a couple of Swing/AWT experts on StackOverflow have been stumped by
the following behaviour. Here is the StackOverflow post --
https://stackoverflow.com/questions/79934904/

Long story short, I am trying to modify the generated content when
copy-pasting from a JTable, but the resulting copied content appears
malformed on my end, but works fine on the other StackOverflow commentors
machines.

Consider the following code.

// Source - https://stackoverflow.com/q/79934904
// Posted by davidalayachew
// Retrieved 2026-05-06, License - CC BY-SA 4.0

import module java.base;
import module java.desktop;

void main()
{

    SwingUtilities
        .invokeLater
        (
            () ->
            {

                JOptionPane
                    .showMessageDialog
                    (
                        null,
                        new JTable
                        (
                            new Object[][]
                            {
                                new Object[]{1, 2, 3},
                                new Object[]{4, 5, 6},
                                new Object[]{7777, 88888, 99999}
                            },
                            new Object[]{"abc", "xyz", "tuv"}

                        )
                    )
                    ;

            }
        )
        ;

}

It's a simple JTable, and copy-pasting from it will simply generate a
rendered HTML Table that will show up properly on the appropriate editor,
whether Gmail, Google Docs, etc. Of course, it will show up as
tab-separated text if pasted anywhere else.

Now, consider the following code instead.

// Source - https://stackoverflow.com/a/79935559
// Posted by VGR
// Retrieved 2026-05-06, License - CC BY-SA 4.0

import java.io.Serial;

import java.util.Formatter;
import java.util.Arrays;
import java.util.Objects;
import java.util.stream.IntStream;

import java.awt.EventQueue;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;

import javax.swing.TransferHandler;
import javax.swing.JComponent;
import javax.swing.JTable;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.table.TableModel;

public class CustomHTMLTransferHandler
extends TransferHandler {
    @Serial
    private static final long serialVersionUID = 1;

    @Override
    public boolean canImport(TransferSupport support) {
        return false;
    }

    @Override
    public void exportToClipboard(JComponent c,
                                  Clipboard clipboard,
                                  int action) {

        if (action != COPY && action != COPY_OR_MOVE) {
            return;
        }

        clipboard.setContents(createTransferable(c), null);
    }

    @Override
    public Transferable createTransferable(JComponent c) {
        if (!(c instanceof JTable)) {
            throw new IllegalArgumentException(
                "This class can only handle export of data from a JTable.");
        }

        return new Transferable() {
            @Override
            public boolean isDataFlavorSupported(DataFlavor flavor) {
                return flavor.equals(DataFlavor.stringFlavor) ||
                       flavor.equals(DataFlavor.fragmentHtmlFlavor) ||
                       flavor.equals(DataFlavor.allHtmlFlavor);
            }

            @Override
            public DataFlavor[] getTransferDataFlavors() {
                return new DataFlavor[] {
                    DataFlavor.stringFlavor,
                    DataFlavor.fragmentHtmlFlavor,
                    DataFlavor.allHtmlFlavor
                };
            }

            @Override
            public Object getTransferData(DataFlavor flavor)
            throws UnsupportedFlavorException {
                JTable table = (JTable) c;
                TableModel model = table.getModel();
                int columns = model.getColumnCount();
                IntStream rows;
                int[] selectedRows = table.getSelectedRows();
                if (selectedRows.length > 0) {
                    Arrays.sort(selectedRows);
                    rows = Arrays.stream(selectedRows)
                        .map(table::convertRowIndexToModel);
                } else {
                    rows = IntStream.range(0, model.getRowCount());
                }

                if (flavor.equals(DataFlavor.stringFlavor)) {
                    StringBuilder tsv = new StringBuilder();
                    rows.forEach(r -> {
                        for (int c = 0; c < columns; c++) {
                            if (c > 0) {
                                tsv.append('\t');
                            }
                            Object value = model.getValueAt(r, c);
                            String text = Objects.toString(value, "");
                            tsv.append(text.replace('\t', ' '));
                        }
                        tsv.append(System.lineSeparator());
                    });
                    return tsv.toString();
                } else if (flavor.equals(DataFlavor.fragmentHtmlFlavor) ||
                           flavor.equals(DataFlavor.allHtmlFlavor)) {

                    Formatter html = new Formatter();
                    html.format("<table frame='border' rules='all'>\n");
                    rows.forEach(r -> {
                        html.format("<tr>");
                        for (int c = 0; c < columns; c++) {
                            html.format("<td>");
                            Object value = model.getValueAt(r, c);
                            String text = Objects.toString(value, "");
                            text.codePoints().forEach(p -> {
                                html.format(
                                    p >= 32 && p < 127 && p != '<' && p !=
'&'
                                    ? "%c" : "&#%d;", p);
                            });
                        }
                        html.format("\n");
                    });
                    html.format("</table>");
                    return html.toString();
                } else {
                    throw new UnsupportedFlavorException(flavor);
                }
            }
        };
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            JTable table = new JTable(
                new Object[][] {
                    {1, 2, 3},
                    {4, 5, 6},
                    {7777, 88888, 99999}
                },
                new Object[] {"abc", "xyz", "tuv"}
            );
            table.setTransferHandler(new CustomHTMLTransferHandler());
            JOptionPane.showMessageDialog(null, new JScrollPane(table));
        });
    }
}

This code should allow us to modify the copied contents of the JTable to be
what we want. And it seems to do exactly that for everyone else except me.
And again, I downloaded a fresh JDK 26.0.1, ran the code, and still it
doesn't work. I am on Windows 11 and my browser is Firefox 150.0.1.

When I paste the above code into something like Google Docs or anything
else that should be able to receive a  rendered HTML table, it instead
demotes to a basic tab-separated list of rows. Basically, demoting from
text/html to text/plain.

For the life of me, we can't figure out why. And this seems to be only on
my machine and no one else's. I am doing nothing weird here, so I am lost
on what exactly might be wrong here on my side. I am starting to wonder if
this is some portability issue? We are running the exact same Java code but
getting different results.

Thank you for your time and consideration.
David Alayachew

Reply via email to