aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/ClipboardSupport.java
diff options
context:
space:
mode:
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.java429
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));
+ }
+
+}