001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.datatransfer;
003
004import java.awt.GraphicsEnvironment;
005import java.awt.HeadlessException;
006import java.awt.Toolkit;
007import java.awt.datatransfer.Clipboard;
008import java.awt.datatransfer.ClipboardOwner;
009import java.awt.datatransfer.DataFlavor;
010import java.awt.datatransfer.StringSelection;
011import java.awt.datatransfer.Transferable;
012import java.awt.datatransfer.UnsupportedFlavorException;
013import java.io.IOException;
014
015import org.openstreetmap.josm.gui.util.GuiHelper;
016import org.openstreetmap.josm.tools.Logging;
017import org.openstreetmap.josm.tools.Utils;
018
019/**
020 * This is a utility class that provides methods useful for general data transfer support.
021 *
022 * @author Michael Zangl
023 * @since 10604
024 */
025public final class ClipboardUtils {
026    private static final class DoNothingClipboardOwner implements ClipboardOwner {
027        @Override
028        public void lostOwnership(Clipboard clpbrd, Transferable t) {
029            // Do nothing
030        }
031    }
032
033    private static Clipboard clipboard;
034
035    private ClipboardUtils() {
036        // Hide default constructor for utility classes
037    }
038
039    /**
040     * This method should be used from all of JOSM to access the clipboard.
041     * <p>
042     * It will default to the system clipboard except for cases where that clipboard is not accessible.
043     * @return A clipboard.
044     * @see #getClipboardContent()
045     */
046    public static synchronized Clipboard getClipboard() {
047        // Might be unsupported in some more cases, we need a fake clipboard then.
048        if (clipboard == null) {
049            try {
050                clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
051            } catch (HeadlessException | SecurityException e) {
052                Logging.warn("Using fake clipboard.", e);
053                clipboard = new Clipboard("fake");
054            }
055        }
056        return clipboard;
057    }
058
059    /**
060     * Gets the singleton instance of the system selection as a <code>Clipboard</code> object.
061     * This allows an application to read and modify the current, system-wide selection.
062     * @return the system selection as a <code>Clipboard</code>, or <code>null</code> if the native platform does not
063     *         support a system selection <code>Clipboard</code> or if GraphicsEnvironment.isHeadless() returns true
064     * @see Toolkit#getSystemSelection
065     */
066    public static Clipboard getSystemSelection() {
067        if (GraphicsEnvironment.isHeadless()) {
068            return null;
069        } else {
070            return Toolkit.getDefaultToolkit().getSystemSelection();
071        }
072    }
073
074    /**
075     * Gets the clipboard content as string.
076     * @return the content if available, <code>null</code> otherwise.
077     */
078    public static String getClipboardStringContent() {
079        try {
080            Transferable t = getClipboardContent();
081            if (t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
082                return (String) t.getTransferData(DataFlavor.stringFlavor);
083            }
084        } catch (UnsupportedFlavorException | IOException ex) {
085            Logging.error(ex);
086        }
087        return null;
088    }
089
090    /**
091     * Extracts clipboard content as {@code Transferable} object. Using this method avoids some problems on some platforms.
092     * @return The content or <code>null</code> if it is not available
093     */
094    public static synchronized Transferable getClipboardContent() {
095        return getClipboardContent(getClipboard());
096    }
097
098    /**
099     * Extracts clipboard content as {@code Transferable} object. Using this method avoids some problems on some platforms.
100     * @param clipboard clipboard from which contents are retrieved
101     * @return clipboard contents if available, {@code null} otherwise.
102     */
103    public static Transferable getClipboardContent(Clipboard clipboard) {
104        Transferable t = null;
105        for (int tries = 0; t == null && tries < 10; tries++) {
106            try {
107                t = clipboard.getContents(null);
108            } catch (IllegalStateException e) {
109                // Clipboard currently unavailable.
110                // On some platforms, the system clipboard is unavailable while it is accessed by another application.
111                Logging.trace("Clipboard unavailable.", e);
112                try {
113                    Thread.sleep(1);
114                } catch (InterruptedException ex) {
115                    Logging.log(Logging.LEVEL_WARN, "InterruptedException in " + Utils.class.getSimpleName()
116                            + " while getting clipboard content", ex);
117                    Thread.currentThread().interrupt();
118                }
119            } catch (NullPointerException e) { // NOPMD
120                // JDK-6322854: On Linux/X11, NPE can happen for unknown reasons, on all versions of Java
121                Logging.error(e);
122            }
123        }
124        return t;
125    }
126
127    /**
128     * Copy the given string to the clipboard.
129     * @param s The string to copy.
130     * @return True if the  copy was successful
131     */
132    public static boolean copyString(String s) {
133        return copy(new StringSelection(s));
134    }
135
136    /**
137     * Copies the given transferable to the clipboard. Handles state problems that occur on some platforms.
138     * @param transferable The transferable to copy.
139     * @return True if the copy was successful
140     */
141    public static boolean copy(final Transferable transferable) {
142        return GuiHelper.runInEDTAndWaitAndReturn(() -> {
143            try {
144                getClipboard().setContents(transferable, new DoNothingClipboardOwner());
145                return Boolean.TRUE;
146            } catch (IllegalStateException ex) {
147                Logging.error(ex);
148                return Boolean.FALSE;
149            }
150        });
151    }
152
153    /**
154     * Returns a new {@link DataFlavor} for the given class and human-readable name.
155     * @param c class
156     * @param humanPresentableName the human-readable string used to identify this flavor
157     * @return a new {@link DataFlavor} for the given class and human-readable name
158     * @since 10801
159     */
160    public static DataFlavor newDataFlavor(Class<?> c, String humanPresentableName) {
161        try {
162            return new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType + ";class=" + c.getName(),
163                    humanPresentableName, c.getClassLoader());
164        } catch (ClassNotFoundException e) {
165            throw new IllegalArgumentException(e);
166        }
167    }
168
169    /**
170     * Clears the system clipboard.
171     * @return True if the clear was successful
172     * @since 14500
173     */
174    public static boolean clear() {
175        // Cannot simply set clipboard contents to null, see https://stackoverflow.com/a/18254949/2257172
176        return copy(new StringSelection(""));
177    }
178}