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 */
16package com.android.ide.eclipse.adt.internal.editors.layout.gle2;
17
18import static com.android.SdkConstants.ANDROID_NS_NAME;
19import static com.android.SdkConstants.NS_RESOURCES;
20import static com.android.SdkConstants.XMLNS_URI;
21
22import com.android.ide.common.api.IDragElement;
23import com.android.ide.common.api.IDragElement.IDragAttribute;
24import com.android.ide.common.api.INode;
25import com.android.ide.eclipse.adt.AdtPlugin;
26import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
27import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate;
28import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
29import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy;
30import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine;
31import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode;
32import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode;
33import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
34
35import org.eclipse.jface.action.Action;
36import org.eclipse.swt.custom.StyledText;
37import org.eclipse.swt.dnd.Clipboard;
38import org.eclipse.swt.dnd.TextTransfer;
39import org.eclipse.swt.dnd.Transfer;
40import org.eclipse.swt.dnd.TransferData;
41import org.eclipse.swt.widgets.Composite;
42
43import java.util.ArrayList;
44import java.util.HashMap;
45import java.util.List;
46import 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 */
53public 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     */
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     */
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     */
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     */
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     */
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     */
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(String)} except we also
308     * copy all the attributes and inner elements recursively.
309     */
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     */
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
425    private void debugPrintf(String message, Object... params) {
426        if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params));
427    }
428
429}
430