| /* |
| * Copyright 2000-2014 JetBrains s.r.o. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.intellij.ide; |
| |
| import com.intellij.Patches; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.components.ApplicationComponent; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.SystemInfo; |
| import com.intellij.openapi.util.registry.Registry; |
| import com.intellij.ui.mac.foundation.Foundation; |
| import com.intellij.ui.mac.foundation.ID; |
| import com.intellij.util.concurrency.FutureResult; |
| import com.sun.jna.IntegerType; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| import sun.awt.datatransfer.DataTransferer; |
| |
| import java.awt.*; |
| import java.awt.datatransfer.*; |
| import java.io.IOException; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Set; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * <p>This class is used to workaround the problem with getting clipboard contents (http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4818143). |
| * Although this bug is marked as fixed actually Sun just set 10 seconds timeout for {@link java.awt.datatransfer.Clipboard#getContents(Object)} |
| * method which may cause unacceptably long UI freezes. So we worked around this as follows: |
| * <ul> |
| * <li>for Macs we perform synchronization with system clipboard on a separate thread and schedule it when IDEA frame is activated |
| * or Copy/Cut action in Swing component is invoked, and use native method calls to access system clipboard lock-free (?);</li> |
| * <li>for X Window we temporary set short timeout and check for available formats (which should be fast if a clipboard owner is alive).</li> |
| * </ul> |
| * </p> |
| * |
| * @author nik |
| */ |
| public class ClipboardSynchronizer implements ApplicationComponent { |
| private static final Logger LOG = Logger.getInstance("#com.intellij.ide.ClipboardSynchronizer"); |
| |
| private final ClipboardHandler myClipboardHandler; |
| |
| public static ClipboardSynchronizer getInstance() { |
| return ApplicationManager.getApplication().getComponent(ClipboardSynchronizer.class); |
| } |
| |
| public ClipboardSynchronizer() { |
| if (ApplicationManager.getApplication().isHeadlessEnvironment() && ApplicationManager.getApplication().isUnitTestMode()) { |
| myClipboardHandler = new HeadlessClipboardHandler(); |
| } |
| else if (Patches.SLOW_GETTING_CLIPBOARD_CONTENTS && SystemInfo.isMac) { |
| myClipboardHandler = new MacClipboardHandler(); |
| } |
| else if (Patches.SLOW_GETTING_CLIPBOARD_CONTENTS && SystemInfo.isXWindow) { |
| myClipboardHandler = new XWinClipboardHandler(); |
| } |
| else { |
| myClipboardHandler = new ClipboardHandler(); |
| } |
| } |
| |
| @Override |
| public void initComponent() { |
| myClipboardHandler.init(); |
| } |
| |
| @Override |
| public void disposeComponent() { |
| myClipboardHandler.dispose(); |
| } |
| |
| @NotNull |
| @Override |
| public String getComponentName() { |
| return "ClipboardSynchronizer"; |
| } |
| |
| public boolean areDataFlavorsAvailable(@NotNull DataFlavor... flavors) { |
| try { |
| return myClipboardHandler.areDataFlavorsAvailable(flavors); |
| } |
| catch (IllegalStateException e) { |
| LOG.info(e); |
| return false; |
| } |
| } |
| |
| @Nullable |
| public Transferable getContents() { |
| try { |
| return myClipboardHandler.getContents(); |
| } |
| catch (IllegalStateException e) { |
| LOG.info(e); |
| return null; |
| } |
| } |
| |
| public void setContent(@NotNull final Transferable content, @NotNull final ClipboardOwner owner) { |
| myClipboardHandler.setContent(content, owner); |
| } |
| |
| public void resetContent() { |
| myClipboardHandler.resetContent(); |
| } |
| |
| |
| private static class ClipboardHandler { |
| public void init() { } |
| |
| public void dispose() { } |
| |
| public boolean areDataFlavorsAvailable(@NotNull DataFlavor... flavors) { |
| Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); |
| for (DataFlavor flavor : flavors) { |
| if (clipboard.isDataFlavorAvailable(flavor)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Nullable |
| public Transferable getContents() throws IllegalStateException { |
| IllegalStateException last = null; |
| for (int i = 0; i < 3; i++) { |
| try { |
| return Toolkit.getDefaultToolkit().getSystemClipboard().getContents(this); |
| } |
| catch (IllegalStateException e) { |
| try { |
| //noinspection BusyWait |
| Thread.sleep(50); |
| } |
| catch (InterruptedException ignored) { |
| } |
| last = e; |
| } |
| } |
| throw last; |
| } |
| |
| public void setContent(@NotNull final Transferable content, @NotNull final ClipboardOwner owner) { |
| for (int i = 0; i < 3; i++) { |
| try { |
| Toolkit.getDefaultToolkit().getSystemClipboard().setContents(content, owner); |
| } |
| catch (IllegalStateException e) { |
| try { |
| //noinspection BusyWait |
| Thread.sleep(50); |
| } |
| catch (InterruptedException ignored) { |
| } |
| continue; |
| } |
| break; |
| } |
| } |
| |
| public void resetContent() { |
| } |
| } |
| |
| |
| private static class MacClipboardHandler extends ClipboardHandler { |
| private Pair<String,Transferable> myFullTransferable; |
| |
| @Nullable |
| private Transferable doGetContents() throws IllegalStateException { |
| if (Registry.is("ide.mac.useNativeClipboard")) { |
| final Transferable safe = getContentsSafe(); |
| if (safe != null) { |
| return safe; |
| } |
| } |
| |
| return super.getContents(); |
| } |
| |
| @Override |
| public boolean areDataFlavorsAvailable(@NotNull DataFlavor... flavors) { |
| Transferable contents = getContents(); |
| return contents != null && ClipboardSynchronizer.areDataFlavorsAvailable(contents, flavors); |
| } |
| |
| @Override |
| public Transferable getContents() { |
| Transferable transferable = doGetContents(); |
| if (transferable != null && myFullTransferable != null && transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) { |
| try { |
| String stringData = (String) transferable.getTransferData(DataFlavor.stringFlavor); |
| if (stringData != null && stringData.equals(myFullTransferable.getFirst())) { |
| return myFullTransferable.getSecond(); |
| } |
| } |
| catch (UnsupportedFlavorException e) { |
| LOG.info(e); |
| } |
| catch (IOException e) { |
| LOG.info(e); |
| } |
| } |
| |
| myFullTransferable = null; |
| return transferable; |
| } |
| |
| @Override |
| public void resetContent() { |
| //myFullTransferable = null; |
| super.resetContent(); |
| } |
| |
| @Override |
| public void setContent(@NotNull final Transferable content, @NotNull final ClipboardOwner owner) { |
| if (Registry.is("ide.mac.useNativeClipboard") && content.isDataFlavorSupported(DataFlavor.stringFlavor)) { |
| try { |
| String stringData = (String) content.getTransferData(DataFlavor.stringFlavor); |
| myFullTransferable = Pair.create(stringData, content); |
| super.setContent(new StringSelection(stringData), owner); |
| } |
| catch (UnsupportedFlavorException e) { |
| LOG.info(e); |
| } |
| catch (IOException e) { |
| LOG.info(e); |
| } |
| } else { |
| myFullTransferable = null; |
| super.setContent(content, owner); |
| } |
| } |
| |
| @Nullable |
| private static Transferable getContentsSafe() { |
| final FutureResult<Transferable> result = new FutureResult<Transferable>(); |
| |
| Foundation.executeOnMainThread(new Runnable() { |
| @Override |
| public void run() { |
| Transferable transferable = getClipboardContentNatively(); |
| if (transferable != null) { |
| result.set(transferable); |
| } |
| } |
| }, true, false); |
| |
| try { |
| return result.get(10, TimeUnit.MILLISECONDS); |
| } |
| catch (Exception ignored) { |
| return null; |
| } |
| } |
| |
| @Nullable |
| private static Transferable getClipboardContentNatively() { |
| String plainText = "public.utf8-plain-text"; |
| |
| ID pasteboard = Foundation.invoke("NSPasteboard", "generalPasteboard"); |
| ID types = Foundation.invoke(pasteboard, "types"); |
| IntegerType count = Foundation.invoke(types, "count"); |
| |
| ID plainTextType = null; |
| |
| for (int i = 0; i < count.intValue(); i++) { |
| ID each = Foundation.invoke(types, "objectAtIndex:", i); |
| String eachType = Foundation.toStringViaUTF8(each); |
| if (plainText.equals(eachType)) { |
| plainTextType = each; |
| break; |
| } |
| } |
| |
| // will put string value even if we doesn't found java object. this is needed because java caches clipboard value internally and |
| // will reset it ONLY IF we'll put jvm-object into clipboard (see our setContent optimizations which avoids putting jvm-objects |
| // into clipboard) |
| |
| Transferable result = null; |
| if (plainTextType != null) { |
| ID text = Foundation.invoke(pasteboard, "stringForType:", plainTextType); |
| String value = Foundation.toStringViaUTF8(text); |
| if (value == null) { |
| LOG.info(String.format("[Clipboard] Strange string value (null?) for type: %s", plainTextType)); |
| } |
| else { |
| result = new StringSelection(value); |
| } |
| } |
| |
| return result; |
| } |
| } |
| |
| |
| private static class XWinClipboardHandler extends ClipboardHandler { |
| private static final String DATA_TRANSFER_TIMEOUT_PROPERTY = "sun.awt.datatransfer.timeout"; |
| private static final String LONG_TIMEOUT = "2000"; |
| private static final String SHORT_TIMEOUT = "100"; |
| private static final FlavorTable FLAVOR_MAP = (FlavorTable)SystemFlavorMap.getDefaultFlavorMap(); |
| |
| private volatile Transferable myCurrentContent = null; |
| |
| @Override |
| public void init() { |
| if (System.getProperty(DATA_TRANSFER_TIMEOUT_PROPERTY) == null) { |
| System.setProperty(DATA_TRANSFER_TIMEOUT_PROPERTY, LONG_TIMEOUT); |
| } |
| } |
| |
| @Override |
| public void dispose() { |
| resetContent(); |
| } |
| |
| @Override |
| public boolean areDataFlavorsAvailable(@NotNull DataFlavor... flavors) { |
| Transferable currentContent = myCurrentContent; |
| if (currentContent != null) { |
| return ClipboardSynchronizer.areDataFlavorsAvailable(currentContent, flavors); |
| } |
| |
| try { |
| Collection<DataFlavor> contents = checkContentsQuick(); |
| if (contents != null) { |
| return ClipboardSynchronizer.areDataFlavorsAvailable(contents, flavors); |
| } |
| |
| return super.areDataFlavorsAvailable(flavors); |
| } |
| catch (NullPointerException e) { |
| LOG.warn("Java bug #6322854", e); |
| return false; |
| } |
| catch (IllegalArgumentException e) { |
| LOG.warn("Java bug #7173464", e); |
| return false; |
| } |
| } |
| |
| @Override |
| public Transferable getContents() throws IllegalStateException { |
| final Transferable currentContent = myCurrentContent; |
| if (currentContent != null) { |
| return currentContent; |
| } |
| |
| try { |
| final Collection<DataFlavor> contents = checkContentsQuick(); |
| if (contents != null && contents.isEmpty()) { |
| return null; |
| } |
| |
| return super.getContents(); |
| } |
| catch (NullPointerException e) { |
| LOG.warn("Java bug #6322854", e); |
| return null; |
| } |
| catch (IllegalArgumentException e) { |
| LOG.warn("Java bug #7173464", e); |
| return null; |
| } |
| } |
| |
| @Override |
| public void setContent(@NotNull final Transferable content, @NotNull final ClipboardOwner owner) { |
| myCurrentContent = content; |
| super.setContent(content, owner); |
| } |
| |
| @Override |
| public void resetContent() { |
| myCurrentContent = null; |
| } |
| |
| /** |
| * Quickly checks availability of data in X11 clipboard selection. |
| * |
| * @return null if is unable to check; empty list if clipboard owner doesn't respond timely; |
| * collection of available data flavors otherwise. |
| */ |
| @Nullable |
| private static Collection<DataFlavor> checkContentsQuick() { |
| final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); |
| final Class<? extends Clipboard> aClass = clipboard.getClass(); |
| if (!"sun.awt.X11.XClipboard".equals(aClass.getName())) return null; |
| |
| final Method getClipboardFormats; |
| try { |
| getClipboardFormats = aClass.getDeclaredMethod("getClipboardFormats"); |
| getClipboardFormats.setAccessible(true); |
| } |
| catch (Exception ignore) { |
| return null; |
| } |
| |
| final String timeout = System.getProperty(DATA_TRANSFER_TIMEOUT_PROPERTY); |
| System.setProperty(DATA_TRANSFER_TIMEOUT_PROPERTY, SHORT_TIMEOUT); |
| |
| try { |
| final long[] formats = (long[])getClipboardFormats.invoke(clipboard); |
| if (formats == null || formats.length == 0) { |
| return Collections.emptySet(); |
| } |
| @SuppressWarnings({"unchecked"}) final Set<DataFlavor> set = DataTransferer.getInstance().getFlavorsForFormats(formats, FLAVOR_MAP).keySet(); |
| return set; |
| } |
| catch (IllegalAccessException ignore) { } |
| catch (IllegalArgumentException ignore) { } |
| catch (InvocationTargetException e) { |
| final Throwable cause = e.getCause(); |
| if (cause instanceof IllegalStateException) { |
| throw (IllegalStateException)cause; |
| } |
| } |
| finally { |
| System.setProperty(DATA_TRANSFER_TIMEOUT_PROPERTY, timeout); |
| } |
| |
| return null; |
| } |
| } |
| |
| |
| private static class HeadlessClipboardHandler extends ClipboardHandler { |
| private volatile Transferable myContent = null; |
| |
| @Override |
| public boolean areDataFlavorsAvailable(@NotNull DataFlavor... flavors) { |
| Transferable content = myContent; |
| return content != null && ClipboardSynchronizer.areDataFlavorsAvailable(content, flavors); |
| } |
| |
| @Override |
| public Transferable getContents() throws IllegalStateException { |
| return myContent; |
| } |
| |
| @Override |
| public void setContent(@NotNull Transferable content, @NotNull ClipboardOwner owner) { |
| myContent = content; |
| } |
| |
| @Override |
| public void resetContent() { |
| myContent = null; |
| } |
| } |
| |
| |
| private static boolean areDataFlavorsAvailable(Transferable contents, DataFlavor... flavors) { |
| for (DataFlavor flavor : flavors) { |
| if (contents.isDataFlavorSupported(flavor)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean areDataFlavorsAvailable(Collection<DataFlavor> contents, DataFlavor... flavors) { |
| for (DataFlavor flavor : flavors) { |
| if (contents.contains(flavor)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |