1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
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.android.ide.eclipse.adt.internal.editors.layout.gle2;
17 
18 import static com.android.SdkConstants.ANDROID_NS_NAME;
19 import static com.android.SdkConstants.NS_RESOURCES;
20 import static com.android.SdkConstants.XMLNS_URI;
21 
22 import com.android.ide.common.api.IDragElement;
23 import com.android.ide.common.api.IDragElement.IDragAttribute;
24 import com.android.ide.common.api.INode;
25 import com.android.ide.eclipse.adt.AdtPlugin;
26 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
27 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
28 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
29 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
30 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
31 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
32 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
33 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
34 
35 import org.eclipse.jface.action.Action;
36 import org.eclipse.swt.custom.StyledText;
37 import org.eclipse.swt.dnd.Clipboard;
38 import org.eclipse.swt.dnd.TextTransfer;
39 import org.eclipse.swt.dnd.Transfer;
40 import org.eclipse.swt.dnd.TransferData;
41 import org.eclipse.swt.widgets.Composite;
42 
43 import java.util.ArrayList;
44 import java.util.HashMap;
45 import java.util.List;
46 import java.util.Map;
47 
48 /**
49  * The {@link ClipboardSupport} class manages the native clipboard, providing operations
50  * to copy, cut and paste view items, and can answer whether the clipboard contains
51  * a transferable we care about.
52  */
53 public class ClipboardSupport {
54     private static final boolean DEBUG = false;
55 
56     /** SWT clipboard instance. */
57     private Clipboard mClipboard;
58     private LayoutCanvas mCanvas;
59 
60     /**
61      * Constructs a new {@link ClipboardSupport} tied to the given
62      * {@link LayoutCanvas}.
63      *
64      * @param canvas The {@link LayoutCanvas} to provide clipboard support for.
65      * @param parent The parent widget in the SWT hierarchy of the canvas.
66      */
ClipboardSupport(LayoutCanvas canvas, Composite parent)67     public ClipboardSupport(LayoutCanvas canvas, Composite parent) {
68         mCanvas = canvas;
69 
70         mClipboard = new Clipboard(parent.getDisplay());
71     }
72 
73     /**
74      * Frees up any resources held by the {@link ClipboardSupport}.
75      */
dispose()76     public void dispose() {
77         if (mClipboard != null) {
78             mClipboard.dispose();
79             mClipboard = null;
80         }
81     }
82 
83     /**
84      * Perform the "Copy" action, either from the Edit menu or from the context
85      * menu.
86      * <p/>
87      * This sanitizes the selection, so it must be a copy. It then inserts the
88      * selection both as text and as {@link SimpleElement}s in the clipboard.
89      * (If there is selected text in the error label, then the error is used
90      * as the text portion of the transferable.)
91      *
92      * @param selection A list of selection items to add to the clipboard;
93      *            <b>this should be a copy already - this method will not make a
94      *            copy</b>
95      */
copySelectionToClipboard(List<SelectionItem> selection)96     public void copySelectionToClipboard(List<SelectionItem> selection) {
97         SelectionManager.sanitize(selection);
98 
99         // The error message area shares the copy action with the canvas. Invoking the
100         // copy action when there are errors visible *AND* the user has selected text there,
101         // should include the error message as the text transferable.
102         String message = null;
103         GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor();
104         StyledText errorLabel = graphicalEditor.getErrorLabel();
105         if (errorLabel.getSelectionCount() > 0) {
106             message = errorLabel.getSelectionText();
107         }
108 
109         if (selection.isEmpty()) {
110             if (message != null) {
111                 mClipboard.setContents(
112                         new Object[] { message },
113                         new Transfer[] { TextTransfer.getInstance() }
114                 );
115             }
116             return;
117         }
118 
119         Object[] data = new Object[] {
120                 SelectionItem.getAsElements(selection),
121                 message != null ? message : SelectionItem.getAsText(mCanvas, selection)
122         };
123 
124         Transfer[] types = new Transfer[] {
125                 SimpleXmlTransfer.getInstance(),
126                 TextTransfer.getInstance()
127         };
128 
129         mClipboard.setContents(data, types);
130     }
131 
132     /**
133      * Perform the "Cut" action, either from the Edit menu or from the context
134      * menu.
135      * <p/>
136      * This sanitizes the selection, so it must be a copy. It uses the
137      * {@link #copySelectionToClipboard(List)} method to copy the selection to
138      * the clipboard. Finally it uses {@link #deleteSelection(String, List)} to
139      * delete the selection with a "Cut" verb for the title.
140      *
141      * @param selection A list of selection items to add to the clipboard;
142      *            <b>this should be a copy already - this method will not make a
143      *            copy</b>
144      */
cutSelectionToClipboard(List<SelectionItem> selection)145     public void cutSelectionToClipboard(List<SelectionItem> selection) {
146         copySelectionToClipboard(selection);
147         deleteSelection(mCanvas.getCutLabel(), selection);
148     }
149 
150     /**
151      * Deletes the given selection.
152      *
153      * @param verb A translated verb for the action. Will be used for the
154      *            undo/redo title. Typically this should be
155      *            {@link Action#getText()} for either the cut or the delete
156      *            actions in the canvas.
157      * @param selection The selection. Must not be null. Can be empty, in which
158      *            case nothing happens. The selection list will be sanitized so
159      *            the caller should pass in a copy.
160      */
deleteSelection(String verb, final List<SelectionItem> selection)161     public void deleteSelection(String verb, final List<SelectionItem> selection) {
162         SelectionManager.sanitize(selection);
163 
164         if (selection.isEmpty()) {
165             return;
166         }
167 
168         // If all selected items have the same *kind* of parent, display that in the undo title.
169         String title = null;
170         for (SelectionItem cs : selection) {
171             CanvasViewInfo vi = cs.getViewInfo();
172             if (vi != null && vi.getParent() != null) {
173                 CanvasViewInfo parent = vi.getParent();
174                 assert parent != null;
175                 if (title == null) {
176                     title = parent.getName();
177                 } else if (!title.equals(parent.getName())) {
178                     // More than one kind of parent selected.
179                     title = null;
180                     break;
181                 }
182             }
183         }
184 
185         if (title != null) {
186             // Typically the name is an FQCN. Just get the last segment.
187             int pos = title.lastIndexOf('.');
188             if (pos > 0 && pos < title.length() - 1) {
189                 title = title.substring(pos + 1);
190             }
191         }
192         boolean multiple = mCanvas.getSelectionManager().hasMultiSelection();
193         if (title == null) {
194             title = String.format(
195                         multiple ? "%1$s elements" : "%1$s element",
196                         verb);
197         } else {
198             title = String.format(
199                         multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s",
200                         verb, title);
201         }
202 
203         // Implementation note: we don't clear the internal selection after removing
204         // the elements. An update XML model event should happen when the model gets released
205         // which will trigger a recompute of the layout, thus reloading the model thus
206         // resetting the selection.
207         mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(title, new Runnable() {
208             @Override
209             public void run() {
210                 // Segment the deleted nodes into clusters of siblings
211                 Map<NodeProxy, List<INode>> clusters =
212                         new HashMap<NodeProxy, List<INode>>();
213                 for (SelectionItem cs : selection) {
214                     NodeProxy node = cs.getNode();
215                     if (node == null) {
216                         continue;
217                     }
218                     INode parent = node.getParent();
219                     if (parent != null) {
220                         List<INode> children = clusters.get(parent);
221                         if (children == null) {
222                             children = new ArrayList<INode>();
223                             clusters.put((NodeProxy) parent, children);
224                         }
225                         children.add(node);
226                     }
227                 }
228 
229                 // Notify parent views about children getting deleted
230                 RulesEngine rulesEngine = mCanvas.getRulesEngine();
231                 for (Map.Entry<NodeProxy, List<INode>> entry : clusters.entrySet()) {
232                     NodeProxy parent = entry.getKey();
233                     List<INode> children = entry.getValue();
234                     assert children != null && children.size() > 0;
235                     rulesEngine.callOnRemovingChildren(parent, children);
236                     parent.applyPendingChanges();
237                 }
238 
239                 for (SelectionItem cs : selection) {
240                     CanvasViewInfo vi = cs.getViewInfo();
241                     // You can't delete the root element
242                     if (vi != null && !vi.isRoot()) {
243                         UiViewElementNode ui = vi.getUiViewNode();
244                         if (ui != null) {
245                             ui.deleteXmlNode();
246                         }
247                     }
248                 }
249             }
250         });
251     }
252 
253     /**
254      * Perform the "Paste" action, either from the Edit menu or from the context
255      * menu.
256      *
257      * @param selection A list of selection items to add to the clipboard;
258      *            <b>this should be a copy already - this method will not make a
259      *            copy</b>
260      */
pasteSelection(List<SelectionItem> selection)261     public void pasteSelection(List<SelectionItem> selection) {
262 
263         SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
264         final SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt);
265 
266         if (pasted == null || pasted.length == 0) {
267             return;
268         }
269 
270         CanvasViewInfo lastRoot = mCanvas.getViewHierarchy().getRoot();
271         if (lastRoot == null) {
272             // Pasting in an empty document. Only paste the first element.
273             pasteInEmptyDocument(pasted[0]);
274             return;
275         }
276 
277         // Otherwise use the current selection, if any, as a guide where to paste
278         // using the first selected element only. If there's no selection use
279         // the root as the insertion point.
280         SelectionManager.sanitize(selection);
281         final CanvasViewInfo target;
282         if (selection.size() > 0) {
283             SelectionItem cs = selection.get(0);
284             target = cs.getViewInfo();
285         } else {
286             target = lastRoot;
287         }
288 
289         final NodeProxy targetNode = mCanvas.getNodeFactory().create(target);
290         mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Paste", new Runnable() {
291             @Override
292             public void run() {
293                 RulesEngine engine = mCanvas.getRulesEngine();
294                 NodeProxy node = engine.callOnPaste(targetNode, target.getViewObject(), pasted);
295                 node.applyPendingChanges();
296             }
297         });
298     }
299 
300     /**
301      * Paste a new root into an empty XML layout.
302      * <p/>
303      * In case of error (unknown FQCN, document not empty), silently do nothing.
304      * In case of success, the new element will have some default attributes set (xmlns:android,
305      * layout_width and height). The edit is wrapped in a proper undo.
306      * <p/>
307      * Implementation is similar to {@link #createDocumentRoot} except we also
308      * copy all the attributes and inner elements recursively.
309      */
pasteInEmptyDocument(final IDragElement pastedElement)310     private void pasteInEmptyDocument(final IDragElement pastedElement) {
311         String rootFqcn = pastedElement.getFqcn();
312 
313         // Need a valid empty document to create the new root
314         final LayoutEditorDelegate delegate = mCanvas.getEditorDelegate();
315         final UiDocumentNode uiDoc = delegate.getUiRootNode();
316         if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
317             debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
318             return;
319         }
320 
321         // Find the view descriptor matching our FQCN
322         final ViewElementDescriptor viewDesc = delegate.getFqcnViewDescriptor(rootFqcn);
323         if (viewDesc == null) {
324             // TODO this could happen if pasting a custom view not known in this project
325             debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn);
326             return;
327         }
328 
329         // Get the last segment of the FQCN for the undo title
330         String title = rootFqcn;
331         int pos = title.lastIndexOf('.');
332         if (pos > 0 && pos < title.length() - 1) {
333             title = title.substring(pos + 1);
334         }
335         title = String.format("Paste root %1$s in document", title);
336 
337         delegate.getEditor().wrapUndoEditXmlModel(title, new Runnable() {
338             @Override
339             public void run() {
340                 UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
341 
342                 // A root node requires the Android XMLNS
343                 uiNew.setAttributeValue(ANDROID_NS_NAME, XMLNS_URI, NS_RESOURCES,
344                         true /*override*/);
345 
346                 // Copy all the attributes from the pasted element
347                 for (IDragAttribute attr : pastedElement.getAttributes()) {
348                     uiNew.setAttributeValue(
349                             attr.getName(),
350                             attr.getUri(),
351                             attr.getValue(),
352                             true /*override*/);
353                 }
354 
355                 // Adjust the attributes, adding the default layout_width/height
356                 // only if they are not present (the original element should have
357                 // them though.)
358                 DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
359 
360                 uiNew.createXmlNode();
361 
362                 // Now process all children
363                 for (IDragElement childElement : pastedElement.getInnerElements()) {
364                     addChild(uiNew, childElement);
365                 }
366             }
367 
368             private void addChild(UiElementNode uiParent, IDragElement childElement) {
369                 String childFqcn = childElement.getFqcn();
370                 final ViewElementDescriptor childDesc =
371                     delegate.getFqcnViewDescriptor(childFqcn);
372                 if (childDesc == null) {
373                     // TODO this could happen if pasting a custom view
374                     debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
375                     return;
376                 }
377 
378                 UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);
379 
380                 // Copy all the attributes from the pasted element
381                 for (IDragAttribute attr : childElement.getAttributes()) {
382                     uiChild.setAttributeValue(
383                             attr.getName(),
384                             attr.getUri(),
385                             attr.getValue(),
386                             true /*override*/);
387                 }
388 
389                 // Adjust the attributes, adding the default layout_width/height
390                 // only if they are not present (the original element should have
391                 // them though.)
392                 DescriptorsUtils.setDefaultLayoutAttributes(
393                         uiChild, false /*updateLayout*/);
394 
395                 uiChild.createXmlNode();
396 
397                 // Now process all grand children
398                 for (IDragElement grandChildElement : childElement.getInnerElements()) {
399                     addChild(uiChild, grandChildElement);
400                 }
401             }
402         });
403     }
404 
405     /**
406      * Returns true if we have a a simple xml transfer data object on the
407      * clipboard.
408      *
409      * @return True if and only if the clipboard contains one of XML element
410      *         objects.
411      */
hasSxtOnClipboard()412     public boolean hasSxtOnClipboard() {
413         // The paste operation is only available if we can paste our custom type.
414         // We do not currently support pasting random text (e.g. XML). Maybe later.
415         SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
416         for (TransferData td : mClipboard.getAvailableTypes()) {
417             if (sxt.isSupportedType(td)) {
418                 return true;
419             }
420         }
421 
422         return false;
423     }
424 
debugPrintf(String message, Object... params)425     private void debugPrintf(String message, Object... params) {
426         if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params));
427     }
428 
429 }
430