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 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.LayoutEditor;
     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      */
     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      */
     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      */
     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.getLayoutEditor().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      */
    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      */
    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.getLayoutEditor().wrapUndoEditXmlModel(title, new Runnable() {
    206             public void run() {
    207                 // Segment the deleted nodes into clusters of siblings
    208                 Map<NodeProxy, List<INode>> clusters =
    209                         new HashMap<NodeProxy, List<INode>>();
    210                 for (SelectionItem cs : selection) {
    211                     NodeProxy node = cs.getNode();
    212                     INode parent = node.getParent();
    213                     if (parent != null) {
    214                         List<INode> children = clusters.get(parent);
    215                         if (children == null) {
    216                             children = new ArrayList<INode>();
    217                             clusters.put((NodeProxy) parent, children);
    218                         }
    219                         children.add(node);
    220                     }
    221                 }
    222 
    223                 // Notify parent views about children getting deleted
    224                 RulesEngine rulesEngine = mCanvas.getRulesEngine();
    225                 LayoutEditor editor = mCanvas.getLayoutEditor();
    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(editor, 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      */
    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.getLayoutEditor().wrapUndoEditXmlModel("Paste", new Runnable() {
    286             public void run() {
    287                 mCanvas.getRulesEngine().callOnPaste(targetNode, target.getViewObject(), pasted);
    288                 targetNode.applyPendingChanges();
    289             }
    290         });
    291     }
    292 
    293     /**
    294      * Paste a new root into an empty XML layout.
    295      * <p/>
    296      * In case of error (unknown FQCN, document not empty), silently do nothing.
    297      * In case of success, the new element will have some default attributes set (xmlns:android,
    298      * layout_width and height). The edit is wrapped in a proper undo.
    299      * <p/>
    300      * Implementation is similar to {@link #createDocumentRoot(String)} except we also
    301      * copy all the attributes and inner elements recursively.
    302      */
    303     private void pasteInEmptyDocument(final IDragElement pastedElement) {
    304         String rootFqcn = pastedElement.getFqcn();
    305 
    306         // Need a valid empty document to create the new root
    307         final LayoutEditor layoutEditor = mCanvas.getLayoutEditor();
    308         final UiDocumentNode uiDoc = layoutEditor.getUiRootNode();
    309         if (uiDoc == null || uiDoc.getUiChildren().size() > 0) {
    310             debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn);
    311             return;
    312         }
    313 
    314         // Find the view descriptor matching our FQCN
    315         final ViewElementDescriptor viewDesc = layoutEditor.getFqcnViewDescriptor(rootFqcn);
    316         if (viewDesc == null) {
    317             // TODO this could happen if pasting a custom view not known in this project
    318             debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn);
    319             return;
    320         }
    321 
    322         // Get the last segment of the FQCN for the undo title
    323         String title = rootFqcn;
    324         int pos = title.lastIndexOf('.');
    325         if (pos > 0 && pos < title.length() - 1) {
    326             title = title.substring(pos + 1);
    327         }
    328         title = String.format("Paste root %1$s in document", title);
    329 
    330         layoutEditor.wrapUndoEditXmlModel(title, new Runnable() {
    331             public void run() {
    332                 UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc);
    333 
    334                 // A root node requires the Android XMLNS
    335                 uiNew.setAttributeValue(
    336                         "android", //$NON-NLS-1$
    337                         XmlnsAttributeDescriptor.XMLNS_URI,
    338                         SdkConstants.NS_RESOURCES,
    339                         true /*override*/);
    340 
    341                 // Copy all the attributes from the pasted element
    342                 for (IDragAttribute attr : pastedElement.getAttributes()) {
    343                     uiNew.setAttributeValue(
    344                             attr.getName(),
    345                             attr.getUri(),
    346                             attr.getValue(),
    347                             true /*override*/);
    348                 }
    349 
    350                 // Adjust the attributes, adding the default layout_width/height
    351                 // only if they are not present (the original element should have
    352                 // them though.)
    353                 DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/);
    354 
    355                 uiNew.createXmlNode();
    356 
    357                 // Now process all children
    358                 for (IDragElement childElement : pastedElement.getInnerElements()) {
    359                     addChild(uiNew, childElement);
    360                 }
    361             }
    362 
    363             private void addChild(UiElementNode uiParent, IDragElement childElement) {
    364                 String childFqcn = childElement.getFqcn();
    365                 final ViewElementDescriptor childDesc =
    366                     layoutEditor.getFqcnViewDescriptor(childFqcn);
    367                 if (childDesc == null) {
    368                     // TODO this could happen if pasting a custom view
    369                     debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn);
    370                     return;
    371                 }
    372 
    373                 UiElementNode uiChild = uiParent.appendNewUiChild(childDesc);
    374 
    375                 // Copy all the attributes from the pasted element
    376                 for (IDragAttribute attr : childElement.getAttributes()) {
    377                     uiChild.setAttributeValue(
    378                             attr.getName(),
    379                             attr.getUri(),
    380                             attr.getValue(),
    381                             true /*override*/);
    382                 }
    383 
    384                 // Adjust the attributes, adding the default layout_width/height
    385                 // only if they are not present (the original element should have
    386                 // them though.)
    387                 DescriptorsUtils.setDefaultLayoutAttributes(
    388                         uiChild, false /*updateLayout*/);
    389 
    390                 uiChild.createXmlNode();
    391 
    392                 // Now process all grand children
    393                 for (IDragElement grandChildElement : childElement.getInnerElements()) {
    394                     addChild(uiChild, grandChildElement);
    395                 }
    396             }
    397         });
    398     }
    399 
    400     /**
    401      * Returns true if we have a a simple xml transfer data object on the
    402      * clipboard.
    403      *
    404      * @return True if and only if the clipboard contains one of XML element
    405      *         objects.
    406      */
    407     public boolean hasSxtOnClipboard() {
    408         // The paste operation is only available if we can paste our custom type.
    409         // We do not currently support pasting random text (e.g. XML). Maybe later.
    410         SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance();
    411         for (TransferData td : mClipboard.getAvailableTypes()) {
    412             if (sxt.isSupportedType(td)) {
    413                 return true;
    414             }
    415         }
    416 
    417         return false;
    418     }
    419 
    420     private void debugPrintf(String message, Object... params) {
    421         if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params));
    422     }
    423 
    424 }
    425