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