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