1 /* 2 * Copyright (C) 2010 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 package com.android.ide.eclipse.adt.internal.editors.layout.gle2; 17 18 import static com.android.SdkConstants.ANDROID_NS_NAME; 19 import static com.android.SdkConstants.NS_RESOURCES; 20 import static com.android.SdkConstants.XMLNS_URI; 21 22 import com.android.ide.common.api.IDragElement; 23 import com.android.ide.common.api.IDragElement.IDragAttribute; 24 import com.android.ide.common.api.INode; 25 import com.android.ide.eclipse.adt.AdtPlugin; 26 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; 27 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 28 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor; 29 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; 30 import com.android.ide.eclipse.adt.internal.editors.layout.gre.RulesEngine; 31 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 32 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; 33 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; 34 35 import org.eclipse.jface.action.Action; 36 import org.eclipse.swt.custom.StyledText; 37 import org.eclipse.swt.dnd.Clipboard; 38 import org.eclipse.swt.dnd.TextTransfer; 39 import org.eclipse.swt.dnd.Transfer; 40 import org.eclipse.swt.dnd.TransferData; 41 import org.eclipse.swt.widgets.Composite; 42 43 import java.util.ArrayList; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 48 /** 49 * The {@link ClipboardSupport} class manages the native clipboard, providing operations 50 * to copy, cut and paste view items, and can answer whether the clipboard contains 51 * a transferable we care about. 52 */ 53 public class ClipboardSupport { 54 private static final boolean DEBUG = false; 55 56 /** SWT clipboard instance. */ 57 private Clipboard mClipboard; 58 private LayoutCanvas mCanvas; 59 60 /** 61 * Constructs a new {@link ClipboardSupport} tied to the given 62 * {@link LayoutCanvas}. 63 * 64 * @param canvas The {@link LayoutCanvas} to provide clipboard support for. 65 * @param parent The parent widget in the SWT hierarchy of the canvas. 66 */ 67 public ClipboardSupport(LayoutCanvas canvas, Composite parent) { 68 mCanvas = canvas; 69 70 mClipboard = new Clipboard(parent.getDisplay()); 71 } 72 73 /** 74 * Frees up any resources held by the {@link ClipboardSupport}. 75 */ 76 public void dispose() { 77 if (mClipboard != null) { 78 mClipboard.dispose(); 79 mClipboard = null; 80 } 81 } 82 83 /** 84 * Perform the "Copy" action, either from the Edit menu or from the context 85 * menu. 86 * <p/> 87 * This sanitizes the selection, so it must be a copy. It then inserts the 88 * selection both as text and as {@link SimpleElement}s in the clipboard. 89 * (If there is selected text in the error label, then the error is used 90 * as the text portion of the transferable.) 91 * 92 * @param selection A list of selection items to add to the clipboard; 93 * <b>this should be a copy already - this method will not make a 94 * copy</b> 95 */ 96 public void copySelectionToClipboard(List<SelectionItem> selection) { 97 SelectionManager.sanitize(selection); 98 99 // The error message area shares the copy action with the canvas. Invoking the 100 // copy action when there are errors visible *AND* the user has selected text there, 101 // should include the error message as the text transferable. 102 String message = null; 103 GraphicalEditorPart graphicalEditor = mCanvas.getEditorDelegate().getGraphicalEditor(); 104 StyledText errorLabel = graphicalEditor.getErrorLabel(); 105 if (errorLabel.getSelectionCount() > 0) { 106 message = errorLabel.getSelectionText(); 107 } 108 109 if (selection.isEmpty()) { 110 if (message != null) { 111 mClipboard.setContents( 112 new Object[] { message }, 113 new Transfer[] { TextTransfer.getInstance() } 114 ); 115 } 116 return; 117 } 118 119 Object[] data = new Object[] { 120 SelectionItem.getAsElements(selection), 121 message != null ? message : SelectionItem.getAsText(mCanvas, selection) 122 }; 123 124 Transfer[] types = new Transfer[] { 125 SimpleXmlTransfer.getInstance(), 126 TextTransfer.getInstance() 127 }; 128 129 mClipboard.setContents(data, types); 130 } 131 132 /** 133 * Perform the "Cut" action, either from the Edit menu or from the context 134 * menu. 135 * <p/> 136 * This sanitizes the selection, so it must be a copy. It uses the 137 * {@link #copySelectionToClipboard(List)} method to copy the selection to 138 * the clipboard. Finally it uses {@link #deleteSelection(String, List)} to 139 * delete the selection with a "Cut" verb for the title. 140 * 141 * @param selection A list of selection items to add to the clipboard; 142 * <b>this should be a copy already - this method will not make a 143 * copy</b> 144 */ 145 public void cutSelectionToClipboard(List<SelectionItem> selection) { 146 copySelectionToClipboard(selection); 147 deleteSelection(mCanvas.getCutLabel(), selection); 148 } 149 150 /** 151 * Deletes the given selection. 152 * 153 * @param verb A translated verb for the action. Will be used for the 154 * undo/redo title. Typically this should be 155 * {@link Action#getText()} for either the cut or the delete 156 * actions in the canvas. 157 * @param selection The selection. Must not be null. Can be empty, in which 158 * case nothing happens. The selection list will be sanitized so 159 * the caller should pass in a copy. 160 */ 161 public void deleteSelection(String verb, final List<SelectionItem> selection) { 162 SelectionManager.sanitize(selection); 163 164 if (selection.isEmpty()) { 165 return; 166 } 167 168 // If all selected items have the same *kind* of parent, display that in the undo title. 169 String title = null; 170 for (SelectionItem cs : selection) { 171 CanvasViewInfo vi = cs.getViewInfo(); 172 if (vi != null && vi.getParent() != null) { 173 CanvasViewInfo parent = vi.getParent(); 174 assert parent != null; 175 if (title == null) { 176 title = parent.getName(); 177 } else if (!title.equals(parent.getName())) { 178 // More than one kind of parent selected. 179 title = null; 180 break; 181 } 182 } 183 } 184 185 if (title != null) { 186 // Typically the name is an FQCN. Just get the last segment. 187 int pos = title.lastIndexOf('.'); 188 if (pos > 0 && pos < title.length() - 1) { 189 title = title.substring(pos + 1); 190 } 191 } 192 boolean multiple = mCanvas.getSelectionManager().hasMultiSelection(); 193 if (title == null) { 194 title = String.format( 195 multiple ? "%1$s elements" : "%1$s element", 196 verb); 197 } else { 198 title = String.format( 199 multiple ? "%1$s elements from %2$s" : "%1$s element from %2$s", 200 verb, title); 201 } 202 203 // Implementation note: we don't clear the internal selection after removing 204 // the elements. An update XML model event should happen when the model gets released 205 // which will trigger a recompute of the layout, thus reloading the model thus 206 // resetting the selection. 207 mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel(title, new Runnable() { 208 @Override 209 public void run() { 210 // Segment the deleted nodes into clusters of siblings 211 Map<NodeProxy, List<INode>> clusters = 212 new HashMap<NodeProxy, List<INode>>(); 213 for (SelectionItem cs : selection) { 214 NodeProxy node = cs.getNode(); 215 if (node == null) { 216 continue; 217 } 218 INode parent = node.getParent(); 219 if (parent != null) { 220 List<INode> children = clusters.get(parent); 221 if (children == null) { 222 children = new ArrayList<INode>(); 223 clusters.put((NodeProxy) parent, children); 224 } 225 children.add(node); 226 } 227 } 228 229 // Notify parent views about children getting deleted 230 RulesEngine rulesEngine = mCanvas.getRulesEngine(); 231 for (Map.Entry<NodeProxy, List<INode>> entry : clusters.entrySet()) { 232 NodeProxy parent = entry.getKey(); 233 List<INode> children = entry.getValue(); 234 assert children != null && children.size() > 0; 235 rulesEngine.callOnRemovingChildren(parent, children); 236 parent.applyPendingChanges(); 237 } 238 239 for (SelectionItem cs : selection) { 240 CanvasViewInfo vi = cs.getViewInfo(); 241 // You can't delete the root element 242 if (vi != null && !vi.isRoot()) { 243 UiViewElementNode ui = vi.getUiViewNode(); 244 if (ui != null) { 245 ui.deleteXmlNode(); 246 } 247 } 248 } 249 } 250 }); 251 } 252 253 /** 254 * Perform the "Paste" action, either from the Edit menu or from the context 255 * menu. 256 * 257 * @param selection A list of selection items to add to the clipboard; 258 * <b>this should be a copy already - this method will not make a 259 * copy</b> 260 */ 261 public void pasteSelection(List<SelectionItem> selection) { 262 263 SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); 264 final SimpleElement[] pasted = (SimpleElement[]) mClipboard.getContents(sxt); 265 266 if (pasted == null || pasted.length == 0) { 267 return; 268 } 269 270 CanvasViewInfo lastRoot = mCanvas.getViewHierarchy().getRoot(); 271 if (lastRoot == null) { 272 // Pasting in an empty document. Only paste the first element. 273 pasteInEmptyDocument(pasted[0]); 274 return; 275 } 276 277 // Otherwise use the current selection, if any, as a guide where to paste 278 // using the first selected element only. If there's no selection use 279 // the root as the insertion point. 280 SelectionManager.sanitize(selection); 281 final CanvasViewInfo target; 282 if (selection.size() > 0) { 283 SelectionItem cs = selection.get(0); 284 target = cs.getViewInfo(); 285 } else { 286 target = lastRoot; 287 } 288 289 final NodeProxy targetNode = mCanvas.getNodeFactory().create(target); 290 mCanvas.getEditorDelegate().getEditor().wrapUndoEditXmlModel("Paste", new Runnable() { 291 @Override 292 public void run() { 293 RulesEngine engine = mCanvas.getRulesEngine(); 294 NodeProxy node = engine.callOnPaste(targetNode, target.getViewObject(), pasted); 295 node.applyPendingChanges(); 296 } 297 }); 298 } 299 300 /** 301 * Paste a new root into an empty XML layout. 302 * <p/> 303 * In case of error (unknown FQCN, document not empty), silently do nothing. 304 * In case of success, the new element will have some default attributes set (xmlns:android, 305 * layout_width and height). The edit is wrapped in a proper undo. 306 * <p/> 307 * Implementation is similar to {@link #createDocumentRoot} except we also 308 * copy all the attributes and inner elements recursively. 309 */ 310 private void pasteInEmptyDocument(final IDragElement pastedElement) { 311 String rootFqcn = pastedElement.getFqcn(); 312 313 // Need a valid empty document to create the new root 314 final LayoutEditorDelegate delegate = mCanvas.getEditorDelegate(); 315 final UiDocumentNode uiDoc = delegate.getUiRootNode(); 316 if (uiDoc == null || uiDoc.getUiChildren().size() > 0) { 317 debugPrintf("Failed to paste document root for %1$s: document is not empty", rootFqcn); 318 return; 319 } 320 321 // Find the view descriptor matching our FQCN 322 final ViewElementDescriptor viewDesc = delegate.getFqcnViewDescriptor(rootFqcn); 323 if (viewDesc == null) { 324 // TODO this could happen if pasting a custom view not known in this project 325 debugPrintf("Failed to paste document root, unknown FQCN %1$s", rootFqcn); 326 return; 327 } 328 329 // Get the last segment of the FQCN for the undo title 330 String title = rootFqcn; 331 int pos = title.lastIndexOf('.'); 332 if (pos > 0 && pos < title.length() - 1) { 333 title = title.substring(pos + 1); 334 } 335 title = String.format("Paste root %1$s in document", title); 336 337 delegate.getEditor().wrapUndoEditXmlModel(title, new Runnable() { 338 @Override 339 public void run() { 340 UiElementNode uiNew = uiDoc.appendNewUiChild(viewDesc); 341 342 // A root node requires the Android XMLNS 343 uiNew.setAttributeValue(ANDROID_NS_NAME, XMLNS_URI, NS_RESOURCES, 344 true /*override*/); 345 346 // Copy all the attributes from the pasted element 347 for (IDragAttribute attr : pastedElement.getAttributes()) { 348 uiNew.setAttributeValue( 349 attr.getName(), 350 attr.getUri(), 351 attr.getValue(), 352 true /*override*/); 353 } 354 355 // Adjust the attributes, adding the default layout_width/height 356 // only if they are not present (the original element should have 357 // them though.) 358 DescriptorsUtils.setDefaultLayoutAttributes(uiNew, false /*updateLayout*/); 359 360 uiNew.createXmlNode(); 361 362 // Now process all children 363 for (IDragElement childElement : pastedElement.getInnerElements()) { 364 addChild(uiNew, childElement); 365 } 366 } 367 368 private void addChild(UiElementNode uiParent, IDragElement childElement) { 369 String childFqcn = childElement.getFqcn(); 370 final ViewElementDescriptor childDesc = 371 delegate.getFqcnViewDescriptor(childFqcn); 372 if (childDesc == null) { 373 // TODO this could happen if pasting a custom view 374 debugPrintf("Failed to paste element, unknown FQCN %1$s", childFqcn); 375 return; 376 } 377 378 UiElementNode uiChild = uiParent.appendNewUiChild(childDesc); 379 380 // Copy all the attributes from the pasted element 381 for (IDragAttribute attr : childElement.getAttributes()) { 382 uiChild.setAttributeValue( 383 attr.getName(), 384 attr.getUri(), 385 attr.getValue(), 386 true /*override*/); 387 } 388 389 // Adjust the attributes, adding the default layout_width/height 390 // only if they are not present (the original element should have 391 // them though.) 392 DescriptorsUtils.setDefaultLayoutAttributes( 393 uiChild, false /*updateLayout*/); 394 395 uiChild.createXmlNode(); 396 397 // Now process all grand children 398 for (IDragElement grandChildElement : childElement.getInnerElements()) { 399 addChild(uiChild, grandChildElement); 400 } 401 } 402 }); 403 } 404 405 /** 406 * Returns true if we have a a simple xml transfer data object on the 407 * clipboard. 408 * 409 * @return True if and only if the clipboard contains one of XML element 410 * objects. 411 */ 412 public boolean hasSxtOnClipboard() { 413 // The paste operation is only available if we can paste our custom type. 414 // We do not currently support pasting random text (e.g. XML). Maybe later. 415 SimpleXmlTransfer sxt = SimpleXmlTransfer.getInstance(); 416 for (TransferData td : mClipboard.getAvailableTypes()) { 417 if (sxt.isSupportedType(td)) { 418 return true; 419 } 420 } 421 422 return false; 423 } 424 425 private void debugPrintf(String message, Object... params) { 426 if (DEBUG) AdtPlugin.printToConsole("Clipboard", String.format(message, params)); 427 } 428 429 } 430