1 /* 2 * Copyright (C) 2008 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 17 package com.android.ide.eclipse.adt.internal.editors.ui.tree; 18 19 import com.android.ide.eclipse.adt.AdtPlugin; 20 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; 21 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 22 23 import org.apache.xml.serialize.Method; 24 import org.apache.xml.serialize.OutputFormat; 25 import org.apache.xml.serialize.XMLSerializer; 26 import org.eclipse.jface.action.Action; 27 import org.eclipse.jface.text.BadLocationException; 28 import org.eclipse.swt.dnd.Clipboard; 29 import org.eclipse.swt.dnd.TextTransfer; 30 import org.eclipse.swt.dnd.Transfer; 31 import org.eclipse.ui.ISharedImages; 32 import org.eclipse.ui.PlatformUI; 33 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 34 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 35 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 36 import org.eclipse.wst.xml.core.internal.document.NodeContainer; 37 import org.w3c.dom.Element; 38 import org.w3c.dom.Node; 39 40 import java.io.IOException; 41 import java.io.StringWriter; 42 import java.util.ArrayList; 43 import java.util.List; 44 45 46 /** 47 * Provides Cut and Copy actions for the tree nodes. 48 */ 49 @SuppressWarnings({"restriction", "deprecation"}) 50 public class CopyCutAction extends Action { 51 private List<UiElementNode> mUiNodes; 52 private boolean mPerformCut; 53 private final AndroidXmlEditor mEditor; 54 private final Clipboard mClipboard; 55 private final ICommitXml mXmlCommit; 56 57 /** 58 * Creates a new Copy or Cut action. 59 * 60 * @param selected The UI node to cut or copy. It *must* have a non-null XML node. 61 * @param performCut True if the operation is cut, false if it is copy. 62 */ CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit, UiElementNode selected, boolean performCut)63 public CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit, 64 UiElementNode selected, boolean performCut) { 65 this(editor, clipboard, xmlCommit, toList(selected), performCut); 66 } 67 68 /** 69 * Creates a new Copy or Cut action. 70 * 71 * @param selected The UI nodes to cut or copy. They *must* have a non-null XML node. 72 * The list becomes owned by the {@link CopyCutAction}. 73 * @param performCut True if the operation is cut, false if it is copy. 74 */ CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit, List<UiElementNode> selected, boolean performCut)75 public CopyCutAction(AndroidXmlEditor editor, Clipboard clipboard, ICommitXml xmlCommit, 76 List<UiElementNode> selected, boolean performCut) { 77 super(performCut ? "Cut" : "Copy"); 78 mEditor = editor; 79 mClipboard = clipboard; 80 mXmlCommit = xmlCommit; 81 82 ISharedImages images = PlatformUI.getWorkbench().getSharedImages(); 83 if (performCut) { 84 setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT)); 85 setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_HOVER)); 86 setDisabledImageDescriptor( 87 images.getImageDescriptor(ISharedImages.IMG_TOOL_CUT_DISABLED)); 88 } else { 89 setImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY)); 90 setHoverImageDescriptor(images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_HOVER)); 91 setDisabledImageDescriptor( 92 images.getImageDescriptor(ISharedImages.IMG_TOOL_COPY_DISABLED)); 93 } 94 95 mUiNodes = selected; 96 mPerformCut = performCut; 97 } 98 99 /** 100 * Performs the cut or copy action. 101 * First an XML serializer is used to turn the existing XML node into a valid 102 * XML fragment, which is added as text to the clipboard. 103 */ 104 @Override run()105 public void run() { 106 super.run(); 107 if (mUiNodes == null || mUiNodes.size() < 1) { 108 return; 109 } 110 111 // Commit the current pages first, to make sure the XML is in sync. 112 // Committing may change the XML structure. 113 if (mXmlCommit != null) { 114 mXmlCommit.commitPendingXmlChanges(); 115 } 116 117 StringBuilder allText = new StringBuilder(); 118 ArrayList<UiElementNode> nodesToCut = mPerformCut ? new ArrayList<UiElementNode>() : null; 119 120 for (UiElementNode uiNode : mUiNodes) { 121 try { 122 Node xml_node = uiNode.getXmlNode(); 123 if (xml_node == null) { 124 return; 125 } 126 127 String data = getXmlTextFromEditor(xml_node); 128 129 // In the unlikely event that IStructuredDocument failed to extract the text 130 // directly from the editor, try to fall back on a direct XML serialization 131 // of the XML node. This uses the generic Node interface with no SSE tricks. 132 if (data == null) { 133 data = getXmlTextFromSerialization(xml_node); 134 } 135 136 if (data != null) { 137 allText.append(data); 138 if (mPerformCut) { 139 // only remove notes to cut if we actually got some XML text from them 140 nodesToCut.add(uiNode); 141 } 142 } 143 144 } catch (Exception e) { 145 AdtPlugin.log(e, "CopyCutAction failed for UI node %1$s", //$NON-NLS-1$ 146 uiNode.getBreadcrumbTrailDescription(true)); 147 } 148 } // for uiNode 149 150 if (allText != null && allText.length() > 0) { 151 mClipboard.setContents( 152 new Object[] { allText.toString() }, 153 new Transfer[] { TextTransfer.getInstance() }); 154 if (mPerformCut) { 155 for (UiElementNode uiNode : nodesToCut) { 156 uiNode.deleteXmlNode(); 157 } 158 } 159 } 160 } 161 162 /** Get the data directly from the editor. */ getXmlTextFromEditor(Node xml_node)163 private String getXmlTextFromEditor(Node xml_node) { 164 String data = null; 165 IStructuredModel model = mEditor.getModelForRead(); 166 try { 167 IStructuredDocument sse_doc = mEditor.getStructuredDocument(); 168 if (xml_node instanceof NodeContainer) { 169 // The easy way to get the source of an SSE XML node. 170 data = ((NodeContainer) xml_node).getSource(); 171 } else if (xml_node instanceof IndexedRegion && sse_doc != null) { 172 // Try harder. 173 IndexedRegion region = (IndexedRegion) xml_node; 174 int start = region.getStartOffset(); 175 int end = region.getEndOffset(); 176 177 if (end > start) { 178 data = sse_doc.get(start, end - start); 179 } 180 } 181 } catch (BadLocationException e) { 182 // the region offset was invalid. ignore. 183 } finally { 184 model.releaseFromRead(); 185 } 186 return data; 187 } 188 189 /** 190 * Direct XML serialization of the XML node. 191 * <p/> 192 * This uses the generic Node interface with no SSE tricks. It's however slower 193 * and doesn't respect formatting (since serialization is involved instead of reading 194 * the actual text buffer.) 195 */ getXmlTextFromSerialization(Node xml_node)196 private String getXmlTextFromSerialization(Node xml_node) throws IOException { 197 String data; 198 StringWriter sw = new StringWriter(); 199 XMLSerializer serializer = new XMLSerializer(sw, 200 new OutputFormat(Method.XML, 201 OutputFormat.Defaults.Encoding /* utf-8 */, 202 true /* indent */)); 203 // Serialize will throw an IOException if it fails. 204 serializer.serialize((Element) xml_node); 205 data = sw.toString(); 206 return data; 207 } 208 209 /** 210 * Static helper class to wrap on node into a list for the constructors. 211 */ toList(UiElementNode selected)212 private static ArrayList<UiElementNode> toList(UiElementNode selected) { 213 ArrayList<UiElementNode> list = null; 214 if (selected != null) { 215 list = new ArrayList<UiElementNode>(1); 216 list.add(selected); 217 } 218 return list; 219 } 220 } 221 222