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
