replaced <code></code> with more concise {@code}
[idea/community.git] / platform / platform-impl / src / com / intellij / ide / CopyPasteManagerEx.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.intellij.ide;
17
18 import com.intellij.ide.ui.UISettings;
19 import com.intellij.openapi.Disposable;
20 import com.intellij.openapi.editor.CaretStateTransferableData;
21 import com.intellij.openapi.editor.Document;
22 import com.intellij.openapi.extensions.Extensions;
23 import com.intellij.openapi.ide.CopyPasteManager;
24 import com.intellij.openapi.ide.CutElementMarker;
25 import com.intellij.openapi.ide.KillRingTransferable;
26 import com.intellij.openapi.util.Comparing;
27 import com.intellij.util.EventDispatcher;
28 import org.jetbrains.annotations.NotNull;
29 import org.jetbrains.annotations.Nullable;
30
31 import java.awt.datatransfer.*;
32 import java.io.IOException;
33 import java.util.ArrayList;
34 import java.util.List;
35
36 public class CopyPasteManagerEx extends CopyPasteManager implements ClipboardOwner {
37   private final List<Transferable> myData = new ArrayList<>();
38   private final EventDispatcher<ContentChangedListener> myDispatcher = EventDispatcher.create(ContentChangedListener.class);
39   private final ClipboardSynchronizer myClipboardSynchronizer;
40   private boolean myOwnContent = false;
41
42   public static CopyPasteManagerEx getInstanceEx() {
43     return (CopyPasteManagerEx)getInstance();
44   }
45
46   public CopyPasteManagerEx(ClipboardSynchronizer clipboardSynchronizer) {
47     myClipboardSynchronizer = clipboardSynchronizer;
48   }
49
50   @Override
51   public void lostOwnership(Clipboard clipboard, Transferable contents) {
52     myOwnContent = false;
53     myClipboardSynchronizer.resetContent();
54     fireContentChanged(contents, null);
55   }
56
57   private void fireContentChanged(@Nullable Transferable oldContent, @Nullable Transferable newContent) {
58     myDispatcher.getMulticaster().contentChanged(oldContent, newContent);
59   }
60
61   @Override
62   public void addContentChangedListener(@NotNull ContentChangedListener listener) {
63     myDispatcher.addListener(listener);
64   }
65
66   @Override
67   public void addContentChangedListener(@NotNull final ContentChangedListener listener, @NotNull Disposable parentDisposable) {
68     myDispatcher.addListener(listener, parentDisposable);
69   }
70
71   @Override
72   public void removeContentChangedListener(@NotNull ContentChangedListener listener) {
73     myDispatcher.removeListener(listener);
74   }
75
76   @Override
77   public boolean areDataFlavorsAvailable(@NotNull DataFlavor... flavors) {
78     return flavors.length > 0 &&  myClipboardSynchronizer.areDataFlavorsAvailable(flavors);
79   }
80
81   @Override
82   public void setContents(@NotNull Transferable content) {
83     Transferable oldContent = myOwnContent && !myData.isEmpty() ? myData.get(0) : null;
84
85     Transferable contentToUse = addNewContentToStack(content);
86     setSystemClipboardContent(contentToUse);
87
88     fireContentChanged(oldContent, contentToUse);
89   }
90
91   @Override
92   public boolean isCutElement(@Nullable final Object element) {
93     for (CutElementMarker marker : Extensions.getExtensions(CutElementMarker.EP_NAME)) {
94       if (marker.isCutElement(element)) return true;
95     }
96     return false;
97   }
98
99   @Override
100   public void stopKillRings() {
101     for (Transferable data : myData) {
102       if (data instanceof KillRingTransferable) {
103         ((KillRingTransferable)data).setReadyToCombine(false);
104       }
105     }
106   }
107
108   private void setSystemClipboardContent(Transferable content) {
109     myClipboardSynchronizer.setContent(content, this);
110     myOwnContent = true;
111   }
112
113   /**
114    * Stores given content within the current manager. It is merged with already stored ones
115    * if necessary (see {@link KillRingTransferable}).
116    *
117    * @param content content to store
118    * @return content that is either the given one or the one that was assembled from it and already stored one
119    */
120   @NotNull
121   private Transferable addNewContentToStack(@NotNull Transferable content) {
122     try {
123       String clipString = getStringContent(content);
124       if (clipString == null) {
125         return content;
126       }
127
128       if (content instanceof KillRingTransferable) {
129         KillRingTransferable killRingContent = (KillRingTransferable)content;
130         if (killRingContent.isReadyToCombine() && !myData.isEmpty()) {
131           Transferable prev = myData.get(0);
132           if (prev instanceof KillRingTransferable) {
133             Transferable merged = merge(killRingContent, (KillRingTransferable)prev);
134             if (merged != null) {
135               myData.set(0, merged);
136               return merged;
137             }
138           }
139         }
140         if (killRingContent.isReadyToCombine()) {
141           addToTheTopOfTheStack(killRingContent);
142           return killRingContent;
143         }
144       }
145
146       CaretStateTransferableData caretData = CaretStateTransferableData.getFrom(content);
147       for (int i = 0; i < myData.size(); i++) {
148         Transferable old = myData.get(i);
149         if (clipString.equals(getStringContent(old)) &&
150             CaretStateTransferableData.areEquivalent(caretData, CaretStateTransferableData.getFrom(old))) {
151           myData.remove(i);
152           myData.add(0, content);
153           return content;
154         }
155       }
156
157       addToTheTopOfTheStack(content);
158     }
159     catch (UnsupportedFlavorException | IOException ignore) { }
160     return content;
161   }
162
163   private void addToTheTopOfTheStack(@NotNull Transferable content) {
164     myData.add(0, content);
165     deleteAfterAllowedMaximum();
166   }
167
168   /**
169    * Merges given new data with the given old one and returns merge result in case of success.
170    *
171    * @param newData new data to merge
172    * @param oldData old data to merge
173    * @return merge result of the given data if possible; {@code null} otherwise
174    * @throws IOException                as defined by {@link Transferable#getTransferData(DataFlavor)}
175    * @throws UnsupportedFlavorException as defined by {@link Transferable#getTransferData(DataFlavor)}
176    */
177   @Nullable
178   private static Transferable merge(@NotNull KillRingTransferable newData, @NotNull KillRingTransferable oldData)
179     throws IOException, UnsupportedFlavorException {
180     if (!oldData.isReadyToCombine() || !newData.isReadyToCombine()) {
181       return null;
182     }
183
184     Document document = newData.getDocument();
185     if (document == null || document != oldData.getDocument()) {
186       return null;
187     }
188
189     Object newDataText = newData.getTransferData(DataFlavor.stringFlavor);
190     Object oldDataText = oldData.getTransferData(DataFlavor.stringFlavor);
191     if (newDataText == null || oldDataText == null) {
192       return null;
193     }
194
195     if (oldData.isCut()) {
196       if (newData.getStartOffset() == oldData.getStartOffset()) {
197         return new KillRingTransferable(
198           oldDataText.toString() + newDataText, document, oldData.getStartOffset(), newData.getEndOffset(), newData.isCut()
199         );
200       }
201     }
202
203     if (newData.getStartOffset() == oldData.getEndOffset()) {
204       return new KillRingTransferable(
205         oldDataText.toString() + newDataText, document, oldData.getStartOffset(), newData.getEndOffset(), false
206       );
207     }
208
209     if (newData.getEndOffset() == oldData.getStartOffset()) {
210       return new KillRingTransferable(
211         newDataText.toString() + oldDataText, document, newData.getStartOffset(), oldData.getEndOffset(), false
212       );
213     }
214
215     return null;
216   }
217
218   private static String getStringContent(Transferable content) {
219     try {
220       return (String)content.getTransferData(DataFlavor.stringFlavor);
221     }
222     catch (UnsupportedFlavorException | IOException ignore) { }
223     return null;
224   }
225
226   private void deleteAfterAllowedMaximum() {
227     int max = UISettings.getInstance().getMaxClipboardContents();
228     for (int i = myData.size() - 1; i >= max; i--) {
229       myData.remove(i);
230     }
231   }
232
233   @Override
234   public Transferable getContents() {
235     return myClipboardSynchronizer.getContents();
236   }
237
238   @Nullable
239   @Override
240   public <T> T getContents(@NotNull DataFlavor flavor) {
241     if (areDataFlavorsAvailable(flavor)) {
242       //noinspection unchecked
243       return (T)myClipboardSynchronizer.getData(flavor);
244     }
245     return null;
246   }
247
248   @NotNull
249   @Override
250   public Transferable[] getAllContents() {
251     String clipString = getContents(DataFlavor.stringFlavor);
252     if (clipString != null && (myData.isEmpty() || !Comparing.equal(clipString, getStringContent(myData.get(0))))) {
253       addToTheTopOfTheStack(new StringSelection(clipString));
254     }
255     return myData.toArray(new Transferable[myData.size()]);
256   }
257
258   public void removeContent(Transferable t) {
259     Transferable current = myData.isEmpty() ? null : myData.get(0);
260     myData.remove(t);
261     if (Comparing.equal(t, current)) {
262       Transferable newContent = !myData.isEmpty() ? myData.get(0) : new StringSelection("");
263       setSystemClipboardContent(newContent);
264       fireContentChanged(current, newContent);
265     }
266   }
267
268   public void moveContentToStackTop(Transferable t) {
269     Transferable current = myData.isEmpty() ? null : myData.get(0);
270     if (!Comparing.equal(t, current)) {
271       myData.remove(t);
272       myData.add(0, t);
273       setSystemClipboardContent(t);
274       fireContentChanged(current, t);
275     }
276     else {
277       setSystemClipboardContent(t);
278     }
279   }
280 }