blob: 102000eefb91496f1fdd851abf5993c50c65bdd8 [file] [log] [blame]
/*
* 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.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.ide.CutElementMarker;
import com.intellij.openapi.ide.KillRingTransferable;
import com.intellij.openapi.util.Comparing;
import com.intellij.util.EventDispatcher;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.datatransfer.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class CopyPasteManagerEx extends CopyPasteManager implements ClipboardOwner {
private final List<Transferable> myData = new ArrayList<Transferable>();
private final EventDispatcher<ContentChangedListener> myDispatcher = EventDispatcher.create(ContentChangedListener.class);
private final ClipboardSynchronizer myClipboardSynchronizer;
private boolean myOwnContent = false;
public static CopyPasteManagerEx getInstanceEx() {
return (CopyPasteManagerEx)getInstance();
}
public CopyPasteManagerEx(ClipboardSynchronizer clipboardSynchronizer) {
myClipboardSynchronizer = clipboardSynchronizer;
}
@Override
public void lostOwnership(Clipboard clipboard, Transferable contents) {
myOwnContent = false;
myClipboardSynchronizer.resetContent();
fireContentChanged(contents, null);
}
private void fireContentChanged(@Nullable Transferable oldContent, @Nullable Transferable newContent) {
myDispatcher.getMulticaster().contentChanged(oldContent, newContent);
}
@Override
public void addContentChangedListener(@NotNull ContentChangedListener listener) {
myDispatcher.addListener(listener);
}
@Override
public void addContentChangedListener(@NotNull final ContentChangedListener listener, @NotNull Disposable parentDisposable) {
myDispatcher.addListener(listener, parentDisposable);
}
@Override
public void removeContentChangedListener(@NotNull ContentChangedListener listener) {
myDispatcher.removeListener(listener);
}
@Override
public boolean areDataFlavorsAvailable(@NotNull DataFlavor... flavors) {
return flavors.length > 0 && myClipboardSynchronizer.areDataFlavorsAvailable(flavors);
}
@Override
public void setContents(@NotNull Transferable content) {
Transferable oldContent = myOwnContent && !myData.isEmpty() ? myData.get(0) : null;
Transferable contentToUse = addNewContentToStack(content);
setSystemClipboardContent(contentToUse);
fireContentChanged(oldContent, contentToUse);
}
@Override
public boolean isCutElement(@Nullable final Object element) {
for (CutElementMarker marker : Extensions.getExtensions(CutElementMarker.EP_NAME)) {
if (marker.isCutElement(element)) return true;
}
return false;
}
@Override
public void stopKillRings() {
for (Transferable data : myData) {
if (data instanceof KillRingTransferable) {
((KillRingTransferable)data).setReadyToCombine(false);
}
}
}
private void setSystemClipboardContent(Transferable content) {
myClipboardSynchronizer.setContent(content, this);
myOwnContent = true;
}
/**
* Stores given content within the current manager. It is merged with already stored ones
* if necessary (see {@link KillRingTransferable}).
*
* @param content content to store
* @return content that is either the given one or the one that was assembled from it and already stored one
*/
@NotNull
private Transferable addNewContentToStack(@NotNull Transferable content) {
try {
String clipString = getStringContent(content);
if (clipString == null) {
return content;
}
if (content instanceof KillRingTransferable) {
KillRingTransferable killRingContent = (KillRingTransferable)content;
if (killRingContent.isReadyToCombine() && !myData.isEmpty()) {
Transferable prev = myData.get(0);
if (prev instanceof KillRingTransferable) {
Transferable merged = merge(killRingContent, (KillRingTransferable)prev);
if (merged != null) {
myData.set(0, merged);
return merged;
}
}
}
if (killRingContent.isReadyToCombine()) {
addToTheTopOfTheStack(killRingContent);
return killRingContent;
}
}
Transferable same = null;
for (Transferable old : myData) {
if (clipString.equals(getStringContent(old))) {
same = old;
break;
}
}
if (same == null) {
addToTheTopOfTheStack(content);
}
else {
moveContentToStackTop(same, false); // notification is done in setContents() method
}
}
catch (UnsupportedFlavorException ignore) { }
catch (IOException ignore) { }
return content;
}
private void addToTheTopOfTheStack(@NotNull Transferable content) {
myData.add(0, content);
deleteAfterAllowedMaximum();
}
/**
* Merges given new data with the given old one and returns merge result in case of success.
*
* @param newData new data to merge
* @param oldData old data to merge
* @return merge result of the given data if possible; <code>null</code> otherwise
* @throws IOException as defined by {@link Transferable#getTransferData(DataFlavor)}
* @throws UnsupportedFlavorException as defined by {@link Transferable#getTransferData(DataFlavor)}
*/
@Nullable
private static Transferable merge(@NotNull KillRingTransferable newData, @NotNull KillRingTransferable oldData)
throws IOException, UnsupportedFlavorException {
if (!oldData.isReadyToCombine() || !newData.isReadyToCombine()) {
return null;
}
Document document = newData.getDocument();
if (document == null || document != oldData.getDocument()) {
return null;
}
Object newDataText = newData.getTransferData(DataFlavor.stringFlavor);
Object oldDataText = oldData.getTransferData(DataFlavor.stringFlavor);
if (newDataText == null || oldDataText == null) {
return null;
}
if (oldData.isCut()) {
if (newData.getStartOffset() == oldData.getStartOffset()) {
return new KillRingTransferable(
oldDataText.toString() + newDataText, document, oldData.getStartOffset(), newData.getEndOffset(), newData.isCut()
);
}
}
if (newData.getStartOffset() == oldData.getEndOffset()) {
return new KillRingTransferable(
oldDataText.toString() + newDataText, document, oldData.getStartOffset(), newData.getEndOffset(), false
);
}
if (newData.getEndOffset() == oldData.getStartOffset()) {
return new KillRingTransferable(
newDataText.toString() + oldDataText, document, newData.getStartOffset(), oldData.getEndOffset(), false
);
}
return null;
}
private static String getStringContent(Transferable content) {
try {
return (String)content.getTransferData(DataFlavor.stringFlavor);
}
catch (UnsupportedFlavorException ignore) { }
catch (IOException ignore) { }
return null;
}
private void deleteAfterAllowedMaximum() {
int max = UISettings.getInstance().MAX_CLIPBOARD_CONTENTS;
for (int i = myData.size() - 1; i >= max; i--) {
myData.remove(i);
}
}
@Override
public Transferable getContents() {
return myClipboardSynchronizer.getContents();
}
@Nullable
@Override
public <T> T getContents(@NotNull DataFlavor flavor) {
if (areDataFlavorsAvailable(flavor)) {
try {
Transferable contents = getContents();
if (contents != null) {
@SuppressWarnings("unchecked") T data = (T)contents.getTransferData(flavor);
return data;
}
}
catch (UnsupportedFlavorException ignore) { }
catch (IOException ignore) { }
}
return null;
}
@NotNull
@Override
public Transferable[] getAllContents() {
String clipString = getContents(DataFlavor.stringFlavor);
if (clipString != null && (myData.isEmpty() || !Comparing.equal(clipString, getStringContent(myData.get(0))))) {
addToTheTopOfTheStack(new StringSelection(clipString));
}
return myData.toArray(new Transferable[myData.size()]);
}
public void removeContent(Transferable t) {
Transferable current = myData.isEmpty() ? null : myData.get(0);
myData.remove(t);
if (Comparing.equal(t, current)) {
Transferable newContent = !myData.isEmpty() ? myData.get(0) : new StringSelection("");
setSystemClipboardContent(newContent);
fireContentChanged(current, newContent);
}
}
/** @deprecated use {@link #moveContentToStackTop(Transferable)} (to remove in IDEA 14) */
@SuppressWarnings("UnusedDeclaration")
public void moveContentTopStackTop(Transferable t) {
moveContentToStackTop(t);
}
public void moveContentToStackTop(Transferable t) {
moveContentToStackTop(t, true);
}
private void moveContentToStackTop(Transferable t, boolean notifyOthers) {
Transferable current = myData.isEmpty() ? null : myData.get(0);
if (!Comparing.equal(t, current)) {
myData.remove(t);
myData.add(0, t);
if (notifyOthers) {
setSystemClipboardContent(t);
fireContentChanged(current, t);
}
}
}
}