diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java | 429 |
1 files changed, 429 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java new file mode 100644 index 000000000..263456984 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java @@ -0,0 +1,429 @@ +/* + * 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)); + } + +} |