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