| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Eclipse Public License, Version 1.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/org/documents/epl-v10.php |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.ide.eclipse.adt.internal.editors.layout.gle2; |
| |
| import static com.android.SdkConstants.ANDROID_NS_NAME; |
| import static com.android.SdkConstants.NS_RESOURCES; |
| import static com.android.SdkConstants.XMLNS_URI; |
| |
| import com.android.ide.common.api.IDragElement; |
| import com.android.ide.common.api.IDragElement.IDragAttribute; |
| import com.android.ide.common.api.INode; |
| import com.android.ide.eclipse.adt.AdtPlugin; |
| import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; |
| import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; |
| import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; |
| import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; |
| import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; |
| import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; |
| |
| import org.eclipse.jface.action.Action; |
| import org.eclipse.swt.custom.StyledText; |
| import org.eclipse.swt.dnd.Clipboard; |
| import org.eclipse.swt.dnd.TextTransfer; |
| import org.eclipse.swt.dnd.Transfer; |
| import org.eclipse.swt.dnd.TransferData; |
| import org.eclipse.swt.widgets.Composite; |
| |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * The {@link ClipboardSupport} class manages the native clipboard, providing operations |
| * to copy, cut and paste view items, and can answer whether the clipboard contains |
| * a transferable we care about. |
| */ |
| public class ClipboardSupport { |
| private static final boolean DEBUG = false; |
| |
| /** SWT clipboard instance. */ |
| private Clipboard mClipboard; |
| private LayoutCanvas mCanvas; |
| |
| /** |
| * Constructs a new {@link ClipboardSupport} tied to the given |
| * {@link LayoutCanvas}. |
| * |
| * @param canvas The {@link LayoutCanvas} to provide clipboard support for. |
| * @param parent The parent widget in the SWT hierarchy of the canvas. |
| */ |
| public ClipboardSupport(LayoutCanvas canvas, Composite parent) { |
| mCanvas = canvas; |
| |
| mClipboard = new Clipboard(parent.getDisplay()); |
| } |
| |
| /** |
| * Frees up any resources held by the {@link ClipboardSupport}. |
| */ |
| public void dispose() { |
| if (mClipboard != null) { |
| mClipboard.dispose(); |
| mClipboard = null; |
| } |
| } |
| |
| /** |
| * Perform the "Copy" action, either from the Edit menu or from the context |
| * menu. |
| * <p/> |
| * This sanitizes the selection, so it must be a copy. It then inserts the |
| * selection both as text and as {@link SimpleElement}s in the clipboard. |
| * (If there is selected text in the error label, then the error is used |
| * as the text portion of the transferable.) |
| * |
| * @param selection A list of selection items to add to the clipboard; |
| * <b>this should be a copy already - this method will not make a |
| * copy</b> |
| */ |
| public void copySelectionToClipboard(List<SelectionItem> selection) { |
| SelectionManager.sanitize(selection); |
| |
| // The error message area shares the copy action with the canvas. Invoking the |
| // copy action when there are errors visible *AND* the user has selected text there, |
| // should include the error message as the text transferable. |
| String message = null; |
| GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor(); |
| StyledText errorLabel = graphicalEditor.getErrorLabel(); |
| if (errorLabel.getSelectionCount() > 0) { |
| message = errorLabel.getSelectionText(); |
| } |
| |
| if (selection.isEmpty()) { |
| if (message != null) { |
| mClipboard.setContents( |
| new Object[] { message }, |
| new Transfer[] { TextTransfer.getInstance() } |
| ); |
| } |
| return; |
| } |
| |
| Object[] data = new Object[] { |
| SelectionItem.getAsElements(selection), |
| message != null ? message : SelectionItem.getAsText(mCanvas, selection) |
| }; |
| |
| Transfer[] types = new Transfer[] { |
| SimpleXmlTransfer.getInstance(), |
| TextTransfer.getInstance() |
| }; |
| |
| mClipboard.setContents(data, types); |
| } |
| |
| /** |
| * Perform the "Cut" action, either from the Edit menu or from the context |
| * menu. |
| * <p/> |
| * This sanitizes the selection, so it must be a copy. It uses the |
| * {@link #copySelectionToClipboard(List)} method to copy the selection to |
| * the clipboard. Finally it uses {@link #deleteSelection(String, List)} to |
| * delete the selection with a "Cut" verb for the title. |
| * |
| * @param selection A list of selection items to add to the clipboard; |
| * <b>this should be a copy already - this method will not make a |
| * copy</b> |
| */ |
| public void cutSelectionToClipboard(List<SelectionItem> selection) { |
| copySelectionToClipboard(selection); |
| deleteSelection(mCanvas.getCutLabel(), selection); |
| } |
| |
| /** |
| * Deletes the given selection. |
| * |
| * @param verb A translated verb for the action. Will be used for the |
| * undo/redo title. Typically this should be |
| * {@link Action#getText()} for either the cut or the delete |
| * actions in the canvas. |
| * @param selection The selection. Must not be null. Can be empty, in which |
| * case nothing happens. The selection list will be sanitized so |
| * the caller should pass in a copy. |
| */ |
| public void deleteSelection(String verb, final List<SelectionItem> selection) { |
| SelectionManager.sanitize(selection); |
| |
| if (selection.isEmpty()) { |
| return; |
| } |
| |
| // If all selected items have the same *kind* of parent, display that in the undo title. |
| String title = null; |
| for (SelectionItem cs : selection) { |
| CanvasViewInfo vi = cs.getViewInfo(); |
| if (vi != null && vi.getParent() != null) { |
| CanvasViewInfo parent = vi.getParent(); |
| assert parent != null; |
| if (title == null) { |
| title = parent.getName(); |
| } else if (!title.equals(parent.getName())) { |
| // More than one kind of parent selected. |
| title = null; |
| break; |
| } |
| } |
| } |
| |
| if (title != null) { |
| // Typically the name is an FQCN. Just get the last segment. |
| int pos = title.lastIndexOf('.'); |
| if (pos > 0 && pos < title.length() - 1) { |
| title = title.substring(pos + 1); |
| } |
| } |
| boolean multiple = mCanvas.getSelectionManager().hasMultiSelection(); |
| if (title == null) { |
| title = String.format( |
| multiple ? "%1$s elements" : "%1$s element", |
| verb); |
| } else { |
| title = String.format( |
| multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s", |
| verb, title); |
| } |
| |
| // Implementation note: we don't clear the internal selection after removing |
| // the elements. An update XML model event should happen when the model gets released |
| // which will trigger a recompute of the layout, thus reloading the model thus |
| // resetting the selection. |
| mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(title, new Runnable() { |
| @Override |
| public void run() { |
| // Segment the deleted nodes into clusters of siblings |
| Map<NodeProxy, List<INode>> clusters = |
| new HashMap<NodeProxy, List<INode>>(); |
| for (SelectionItem cs : selection) { |
| NodeProxy node = cs.getNode(); |
| if (node == null) { |
| continue; |
| } |
| INode parent = node.getParent(); |
| if (parent != null) { |
| List<INode> children = clusters.get(parent); |
| if (children == null) { |
| children = new ArrayList<INode>(); |
| clusters.put((NodeProxy) parent, children); |
| } |
| children.add(node); |
| } |
| } |
| |
| // Notify parent views about children getting deleted |
| RulesEngine rulesEngine = mCanvas.getRulesEngine(); |
| for (Map.Entry<NodeProxy, List<INode>> entry : clusters.entrySet()) { |
| NodeProxy parent = entry.getKey(); |
| List<INode> children = entry.getValue(); |
| assert children != null && children.size() > 0; |
| rulesEngine.callOnRemovingChildren(parent, children); |
| parent.applyPendingChanges(); |
| } |
| |
| for (SelectionItem cs : selection) { |
| CanvasViewInfo vi = cs.getViewInfo(); |
| // You can't delete the root element |
| if (vi != null && !vi.isRoot()) { |
| UiViewElementNode ui = vi.getUiViewNode(); |
| if (ui != null) { |
| ui.deleteXmlNode(); |
| } |
| } |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Perform the "Paste" action, either from the Edit menu or from the context |
| * menu. |
| * |
| * @param selection A list of selection items to add to the clipboard; |
| * <b>this should be a copy already - this method will not make a |
| * copy</b> |
| */ |
| public void pasteSelection(List<SelectionItem> selection) { |
| |
| SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); |
| final SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt); |
| |
| if (pasted == null || pasted.length == 0) { |
| return; |
| } |
| |
| CanvasViewInfo lastRoot = mCanvas.getViewHierarchy().getRoot(); |
| if (lastRoot == null) { |
| // Pasting in an empty document. Only paste the first element. |
| pasteInEmptyDocument(pasted[0]); |
| return; |
| } |
| |
| // Otherwise use the current selection, if any, as a guide where to paste |
| // using the first selected element only. If there's no selection use |
| // the root as the insertion point. |
| SelectionManager.sanitize(selection); |
| final CanvasViewInfo target; |
| if (selection.size() > 0) { |
| SelectionItem cs = selection.get(0); |
| target = cs.getViewInfo(); |
| } else { |
| target = lastRoot; |
| } |
| |
| final NodeProxy targetNode = mCanvas.getNodeFactory().create(target); |
| mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Paste", new Runnable() { |
| @Override |
| public void run() { |
| RulesEngine engine = mCanvas.getRulesEngine(); |
| NodeProxy node = engine.callOnPaste(targetNode, target.getViewObject(), pasted); |
| node.applyPendingChanges(); |
| } |
| }); |
| } |
| |
| /** |
| * Paste a new root into an empty XML layout. |
| * <p/> |
| * In case of error (unknown FQCN, document not empty), silently do nothing. |
| * In case of success, the new element will have some default attributes set (xmlns:android, |
| * layout_width and height). The edit is wrapped in a proper undo. |
| * <p/> |
| * Implementation is similar to {@link #createDocumentRoot} except we also |
| * copy all the attributes and inner elements recursively. |
| */ |
| private void pasteInEmptyDocument(final IDragElement pastedElement) { |
| String rootFqcn = pastedElement.getFqcn(); |
| |
| // Need a valid empty document to create the new root |
| final LayoutEditorDelegate delegate = mCanvas.getEditorDelegate(); |
| final UiDocumentNode uiDoc = delegate.getUiRootNode(); |
| if (uiDoc == null || uiDoc.getUiChildren().size() > 0) { |
| debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn); |
| return; |
| } |
| |
| // Find the view descriptor matching our FQCN |
| final ViewElementDescriptor viewDesc = delegate.getFqcnViewDescriptor(rootFqcn); |
| if (viewDesc == null) { |
| // TODO this could happen if pasting a custom view not known in this project |
| debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn); |
| return; |
| } |
| |
| // Get the last segment of the FQCN for the undo title |
| String title = rootFqcn; |
| int pos = title.lastIndexOf('.'); |
| if (pos > 0 && pos < title.length() - 1) { |
| title = title.substring(pos + 1); |
| } |
| title = String.format("Paste root %1$s in document", title); |
| |
| delegate.getEditor().wrapUndoEditXmlModel(title, new Runnable() { |
| @Override |
| public void run() { |
| UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc); |
| |
| // A root node requires the Android XMLNS |
| uiNew.setAttributeValue(ANDROID_NS_NAME, XMLNS_URI, NS_RESOURCES, |
| true /*override*/); |
| |
| // Copy all the attributes from the pasted element |
| for (IDragAttribute attr : pastedElement.getAttributes()) { |
| uiNew.setAttributeValue( |
| attr.getName(), |
| attr.getUri(), |
| attr.getValue(), |
| true /*override*/); |
| } |
| |
| // Adjust the attributes, adding the default layout_width/height |
| // only if they are not present (the original element should have |
| // them though.) |
| DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); |
| |
| uiNew.createXmlNode(); |
| |
| // Now process all children |
| for (IDragElement childElement : pastedElement.getInnerElements()) { |
| addChild(uiNew, childElement); |
| } |
| } |
| |
| private void addChild(UiElementNode uiParent, IDragElement childElement) { |
| String childFqcn = childElement.getFqcn(); |
| final ViewElementDescriptor childDesc = |
| delegate.getFqcnViewDescriptor(childFqcn); |
| if (childDesc == null) { |
| // TODO this could happen if pasting a custom view |
| debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn); |
| return; |
| } |
| |
| UiElementNode uiChild = uiParent.appendNewUiChild(childDesc); |
| |
| // Copy all the attributes from the pasted element |
| for (IDragAttribute attr : childElement.getAttributes()) { |
| uiChild.setAttributeValue( |
| attr.getName(), |
| attr.getUri(), |
| attr.getValue(), |
| true /*override*/); |
| } |
| |
| // Adjust the attributes, adding the default layout_width/height |
| // only if they are not present (the original element should have |
| // them though.) |
| DescriptorsUtils.setDefaultLayoutAttributes( |
| uiChild, false /*updateLayout*/); |
| |
| uiChild.createXmlNode(); |
| |
| // Now process all grand children |
| for (IDragElement grandChildElement : childElement.getInnerElements()) { |
| addChild(uiChild, grandChildElement); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Returns true if we have a a simple xml transfer data object on the |
| * clipboard. |
| * |
| * @return True if and only if the clipboard contains one of XML element |
| * objects. |
| */ |
| public boolean hasSxtOnClipboard() { |
| // The paste operation is only available if we can paste our custom type. |
| // We do not currently support pasting random text (e.g. XML). Maybe later. |
| SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); |
| for (TransferData td : mClipboard.getAvailableTypes()) { |
| if (sxt.isSupportedType(td)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private void debugPrintf(String message, Object... params) { |
| if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params)); |
| } |
| |
| } |