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