Home | History | Annotate | Download | only in gle2
      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      */
     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} 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