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 17 package com.android.ide.common.layout; 18 19 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI; 20 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ABOVE; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BASELINE; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_BOTTOM; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_LEFT; 25 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM; 26 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; 27 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; 28 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; 29 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_RIGHT; 30 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_TOP; 31 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING; 32 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_BELOW; 33 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; 34 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_IN_PARENT; 35 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_CENTER_VERTICAL; 36 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN; 37 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_COLUMN_SPAN; 38 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_GRAVITY; 39 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 40 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN; 41 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_BOTTOM; 42 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_LEFT; 43 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_RIGHT; 44 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_MARGIN_TOP; 45 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW; 46 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_ROW_SPAN; 47 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_LEFT_OF; 48 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_TO_RIGHT_OF; 49 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 50 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_X; 51 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_Y; 52 import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; 53 import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; 54 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; 55 56 import com.android.ide.common.api.DrawingStyle; 57 import com.android.ide.common.api.DropFeedback; 58 import com.android.ide.common.api.IAttributeInfo; 59 import com.android.ide.common.api.IClientRulesEngine; 60 import com.android.ide.common.api.IDragElement; 61 import com.android.ide.common.api.IDragElement.IDragAttribute; 62 import com.android.ide.common.api.IFeedbackPainter; 63 import com.android.ide.common.api.IGraphics; 64 import com.android.ide.common.api.IMenuCallback; 65 import com.android.ide.common.api.INode; 66 import com.android.ide.common.api.INodeHandler; 67 import com.android.ide.common.api.IViewRule; 68 import com.android.ide.common.api.MarginType; 69 import com.android.ide.common.api.Point; 70 import com.android.ide.common.api.Rect; 71 import com.android.ide.common.api.RuleAction; 72 import com.android.ide.common.api.RuleAction.ChoiceProvider; 73 import com.android.ide.common.api.Segment; 74 import com.android.ide.common.api.SegmentType; 75 import com.android.sdklib.SdkConstants; 76 import com.android.util.Pair; 77 78 import java.net.URL; 79 import java.util.Arrays; 80 import java.util.Collections; 81 import java.util.HashMap; 82 import java.util.HashSet; 83 import java.util.List; 84 import java.util.Map; 85 import java.util.Set; 86 87 /** 88 * A {@link IViewRule} for all layouts. 89 */ 90 public class BaseLayoutRule extends BaseViewRule { 91 private static final String ACTION_FILL_WIDTH = "_fillW"; //$NON-NLS-1$ 92 private static final String ACTION_FILL_HEIGHT = "_fillH"; //$NON-NLS-1$ 93 private static final String ACTION_MARGIN = "_margin"; //$NON-NLS-1$ 94 private static final URL ICON_MARGINS = 95 BaseLayoutRule.class.getResource("margins.png"); //$NON-NLS-1$ 96 private static final URL ICON_GRAVITY = 97 BaseLayoutRule.class.getResource("gravity.png"); //$NON-NLS-1$ 98 private static final URL ICON_FILL_WIDTH = 99 BaseLayoutRule.class.getResource("fillwidth.png"); //$NON-NLS-1$ 100 private static final URL ICON_FILL_HEIGHT = 101 BaseLayoutRule.class.getResource("fillheight.png"); //$NON-NLS-1$ 102 103 // ==== Layout Actions support ==== 104 105 // The Margin layout parameters are available for LinearLayout, FrameLayout, RelativeLayout, 106 // and their subclasses. 107 protected final RuleAction createMarginAction(final INode parentNode, 108 final List<? extends INode> children) { 109 110 final List<? extends INode> targets = children == null || children.size() == 0 ? 111 Collections.singletonList(parentNode) 112 : children; 113 final INode first = targets.get(0); 114 115 IMenuCallback actionCallback = new IMenuCallback() { 116 @Override 117 public void action(RuleAction action, List<? extends INode> selectedNodes, 118 final String valueId, final Boolean newValue) { 119 parentNode.editXml("Change Margins", new INodeHandler() { 120 @Override 121 public void handle(INode n) { 122 String uri = ANDROID_URI; 123 String all = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN); 124 String left = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_LEFT); 125 String right = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_RIGHT); 126 String top = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_TOP); 127 String bottom = first.getStringAttr(uri, ATTR_LAYOUT_MARGIN_BOTTOM); 128 String[] margins = mRulesEngine.displayMarginInput(all, left, 129 right, top, bottom); 130 if (margins != null) { 131 assert margins.length == 5; 132 for (INode child : targets) { 133 child.setAttribute(uri, ATTR_LAYOUT_MARGIN, margins[0]); 134 child.setAttribute(uri, ATTR_LAYOUT_MARGIN_LEFT, margins[1]); 135 child.setAttribute(uri, ATTR_LAYOUT_MARGIN_RIGHT, margins[2]); 136 child.setAttribute(uri, ATTR_LAYOUT_MARGIN_TOP, margins[3]); 137 child.setAttribute(uri, ATTR_LAYOUT_MARGIN_BOTTOM, margins[4]); 138 } 139 } 140 } 141 }); 142 } 143 }; 144 145 return RuleAction.createAction(ACTION_MARGIN, "Change Margins...", actionCallback, 146 ICON_MARGINS, 40, false); 147 } 148 149 // Both LinearLayout and RelativeLayout have a gravity (but RelativeLayout applies it 150 // to the parent whereas for LinearLayout it's on the children) 151 protected final RuleAction createGravityAction(final List<? extends INode> targets, final 152 String attributeName) { 153 if (targets != null && targets.size() > 0) { 154 final INode first = targets.get(0); 155 ChoiceProvider provider = new ChoiceProvider() { 156 @Override 157 public void addChoices(List<String> titles, List<URL> iconUrls, 158 List<String> ids) { 159 IAttributeInfo info = first.getAttributeInfo(ANDROID_URI, attributeName); 160 if (info != null) { 161 // Generate list of possible gravity value constants 162 assert info.getFormats().contains(IAttributeInfo.Format.FLAG); 163 for (String name : info.getFlagValues()) { 164 titles.add(getAttributeDisplayName(name)); 165 ids.add(name); 166 } 167 } 168 } 169 }; 170 171 return RuleAction.createChoices("_gravity", "Change Gravity", //$NON-NLS-1$ 172 new PropertyCallback(targets, "Change Gravity", ANDROID_URI, 173 attributeName), 174 provider, 175 first.getStringAttr(ANDROID_URI, attributeName), ICON_GRAVITY, 176 43, false); 177 } 178 179 return null; 180 } 181 182 @Override 183 public void addLayoutActions(List<RuleAction> actions, final INode parentNode, 184 final List<? extends INode> children) { 185 super.addLayoutActions(actions, parentNode, children); 186 187 final List<? extends INode> targets = children == null || children.size() == 0 ? 188 Collections.singletonList(parentNode) 189 : children; 190 final INode first = targets.get(0); 191 192 // Shared action callback 193 IMenuCallback actionCallback = new IMenuCallback() { 194 @Override 195 public void action(RuleAction action, List<? extends INode> selectedNodes, 196 final String valueId, final Boolean newValue) { 197 final String actionId = action.getId(); 198 final String undoLabel; 199 if (actionId.equals(ACTION_FILL_WIDTH)) { 200 undoLabel = "Change Width Fill"; 201 } else if (actionId.equals(ACTION_FILL_HEIGHT)) { 202 undoLabel = "Change Height Fill"; 203 } else { 204 return; 205 } 206 parentNode.editXml(undoLabel, new INodeHandler() { 207 @Override 208 public void handle(INode n) { 209 String attribute = actionId.equals(ACTION_FILL_WIDTH) 210 ? ATTR_LAYOUT_WIDTH : ATTR_LAYOUT_HEIGHT; 211 String value; 212 if (newValue) { 213 if (supportsMatchParent()) { 214 value = VALUE_MATCH_PARENT; 215 } else { 216 value = VALUE_FILL_PARENT; 217 } 218 } else { 219 value = VALUE_WRAP_CONTENT; 220 } 221 for (INode child : targets) { 222 child.setAttribute(ANDROID_URI, attribute, value); 223 } 224 } 225 }); 226 } 227 }; 228 229 actions.add(RuleAction.createToggle(ACTION_FILL_WIDTH, "Toggle Fill Width", 230 isFilled(first, ATTR_LAYOUT_WIDTH), actionCallback, ICON_FILL_WIDTH, 10, false)); 231 actions.add(RuleAction.createToggle(ACTION_FILL_HEIGHT, "Toggle Fill Height", 232 isFilled(first, ATTR_LAYOUT_HEIGHT), actionCallback, ICON_FILL_HEIGHT, 20, false)); 233 } 234 235 // ==== Paste support ==== 236 237 /** 238 * The default behavior for pasting in a layout is to simulate a drop in the 239 * top-left corner of the view. 240 * <p/> 241 * Note that we explicitly do not call super() here -- the BaseViewRule.onPaste handler 242 * will call onPasteBeforeChild() instead. 243 * <p/> 244 * Derived layouts should override this behavior if not appropriate. 245 */ 246 @Override 247 public void onPaste(INode targetNode, Object targetView, IDragElement[] elements) { 248 DropFeedback feedback = onDropEnter(targetNode, targetView, elements); 249 if (feedback != null) { 250 Point p = targetNode.getBounds().getTopLeft(); 251 feedback = onDropMove(targetNode, elements, feedback, p); 252 if (feedback != null) { 253 onDropLeave(targetNode, elements, feedback); 254 onDropped(targetNode, elements, feedback, p); 255 } 256 } 257 } 258 259 /** 260 * The default behavior for pasting in a layout with a specific child target 261 * is to simulate a drop right above the top left of the given child target. 262 * <p/> 263 * This method is invoked by BaseView when onPaste() is called -- 264 * views don't generally accept children and instead use the target node as 265 * a hint to paste "before" it. 266 * 267 * @param parentNode the parent node we're pasting into 268 * @param parentView the view object for the parent layout, or null 269 * @param targetNode the first selected node 270 * @param elements the elements being pasted 271 */ 272 public void onPasteBeforeChild(INode parentNode, Object parentView, INode targetNode, 273 IDragElement[] elements) { 274 DropFeedback feedback = onDropEnter(parentNode, parentView, elements); 275 if (feedback != null) { 276 Point parentP = parentNode.getBounds().getTopLeft(); 277 Point targetP = targetNode.getBounds().getTopLeft(); 278 if (parentP.y < targetP.y) { 279 targetP.y -= 1; 280 } 281 282 feedback = onDropMove(parentNode, elements, feedback, targetP); 283 if (feedback != null) { 284 onDropLeave(parentNode, elements, feedback); 285 onDropped(parentNode, elements, feedback, targetP); 286 } 287 } 288 } 289 290 // ==== Utility methods used by derived layouts ==== 291 292 /** 293 * Draws the bounds of the given elements and all its children elements in the canvas 294 * with the specified offset. 295 * 296 * @param gc the graphics context 297 * @param element the element to be drawn 298 * @param offsetX a horizontal delta to add to the current bounds of the element when 299 * drawing it 300 * @param offsetY a vertical delta to add to the current bounds of the element when 301 * drawing it 302 */ 303 public void drawElement(IGraphics gc, IDragElement element, int offsetX, int offsetY) { 304 Rect b = element.getBounds(); 305 if (b.isValid()) { 306 gc.drawRect(b.x + offsetX, b.y + offsetY, b.x + offsetX + b.w, b.y + offsetY + b.h); 307 } 308 309 for (IDragElement inner : element.getInnerElements()) { 310 drawElement(gc, inner, offsetX, offsetY); 311 } 312 } 313 314 /** 315 * Collect all the "android:id" IDs from the dropped elements. When moving 316 * objects within the same canvas, that's all there is to do. However if the 317 * objects are moved to a different canvas or are copied then set 318 * createNewIds to true to find the existing IDs under targetNode and create 319 * a map with new non-conflicting unique IDs as needed. Returns a map String 320 * old-id => tuple (String new-id, String fqcn) where fqcn is the FQCN of 321 * the element. 322 */ 323 protected static Map<String, Pair<String, String>> getDropIdMap(INode targetNode, 324 IDragElement[] elements, boolean createNewIds) { 325 Map<String, Pair<String, String>> idMap = new HashMap<String, Pair<String, String>>(); 326 327 if (createNewIds) { 328 collectIds(idMap, elements); 329 // Need to remap ids if necessary 330 idMap = remapIds(targetNode, idMap); 331 } 332 333 return idMap; 334 } 335 336 /** 337 * Fills idMap with a map String id => tuple (String id, String fqcn) where 338 * fqcn is the FQCN of the element (in case we want to generate new IDs 339 * based on the element type.) 340 * 341 * @see #getDropIdMap 342 */ 343 protected static Map<String, Pair<String, String>> collectIds( 344 Map<String, Pair<String, String>> idMap, 345 IDragElement[] elements) { 346 for (IDragElement element : elements) { 347 IDragAttribute attr = element.getAttribute(ANDROID_URI, ATTR_ID); 348 if (attr != null) { 349 String id = attr.getValue(); 350 if (id != null && id.length() > 0) { 351 idMap.put(id, Pair.of(id, element.getFqcn())); 352 } 353 } 354 355 collectIds(idMap, element.getInnerElements()); 356 } 357 358 return idMap; 359 } 360 361 /** 362 * Used by #getDropIdMap to find new IDs in case of conflict. 363 */ 364 protected static Map<String, Pair<String, String>> remapIds(INode node, 365 Map<String, Pair<String, String>> idMap) { 366 // Visit the document to get a list of existing ids 367 Set<String> existingIdSet = new HashSet<String>(); 368 collectExistingIds(node.getRoot(), existingIdSet); 369 370 Map<String, Pair<String, String>> new_map = new HashMap<String, Pair<String, String>>(); 371 for (Map.Entry<String, Pair<String, String>> entry : idMap.entrySet()) { 372 String key = entry.getKey(); 373 Pair<String, String> value = entry.getValue(); 374 375 String id = normalizeId(key); 376 377 if (!existingIdSet.contains(id)) { 378 // Not a conflict. Use as-is. 379 new_map.put(key, value); 380 if (!key.equals(id)) { 381 new_map.put(id, value); 382 } 383 } else { 384 // There is a conflict. Get a new id. 385 String new_id = findNewId(value.getSecond(), existingIdSet); 386 value = Pair.of(new_id, value.getSecond()); 387 new_map.put(id, value); 388 new_map.put(id.replaceFirst("@\\+", "@"), value); //$NON-NLS-1$ //$NON-NLS-2$ 389 } 390 } 391 392 return new_map; 393 } 394 395 /** 396 * Used by #remapIds to find a new ID for a conflicting element. 397 */ 398 protected static String findNewId(String fqcn, Set<String> existingIdSet) { 399 // Get the last component of the FQCN (e.g. "android.view.Button" => 400 // "Button") 401 String name = fqcn.substring(fqcn.lastIndexOf('.') + 1); 402 403 for (int i = 1; i < 1000000; i++) { 404 String id = String.format("@+id/%s%02d", name, i); //$NON-NLS-1$ 405 if (!existingIdSet.contains(id)) { 406 existingIdSet.add(id); 407 return id; 408 } 409 } 410 411 // We'll never reach here. 412 return null; 413 } 414 415 /** 416 * Used by #getDropIdMap to find existing IDs recursively. 417 */ 418 protected static void collectExistingIds(INode root, Set<String> existingIdSet) { 419 if (root == null) { 420 return; 421 } 422 423 String id = root.getStringAttr(ANDROID_URI, ATTR_ID); 424 if (id != null) { 425 id = normalizeId(id); 426 427 if (!existingIdSet.contains(id)) { 428 existingIdSet.add(id); 429 } 430 } 431 432 for (INode child : root.getChildren()) { 433 collectExistingIds(child, existingIdSet); 434 } 435 } 436 437 /** 438 * Transforms @id/name into @+id/name to treat both forms the same way. 439 */ 440 protected static String normalizeId(String id) { 441 if (id.indexOf("@+") == -1) { //$NON-NLS-1$ 442 id = id.replaceFirst("@", "@+"); //$NON-NLS-1$ //$NON-NLS-2$ 443 } 444 return id; 445 } 446 447 /** 448 * For use by {@link BaseLayoutRule#addAttributes} A filter should return a 449 * valid replacement string. 450 */ 451 protected static interface AttributeFilter { 452 String replace(String attributeUri, String attributeName, String attributeValue); 453 } 454 455 private static final String[] EXCLUDED_ATTRIBUTES = new String[] { 456 // Common 457 ATTR_LAYOUT_GRAVITY, 458 459 // from AbsoluteLayout 460 ATTR_LAYOUT_X, 461 ATTR_LAYOUT_Y, 462 463 // from RelativeLayout 464 ATTR_LAYOUT_ABOVE, 465 ATTR_LAYOUT_BELOW, 466 ATTR_LAYOUT_TO_LEFT_OF, 467 ATTR_LAYOUT_TO_RIGHT_OF, 468 ATTR_LAYOUT_ALIGN_BASELINE, 469 ATTR_LAYOUT_ALIGN_TOP, 470 ATTR_LAYOUT_ALIGN_BOTTOM, 471 ATTR_LAYOUT_ALIGN_LEFT, 472 ATTR_LAYOUT_ALIGN_RIGHT, 473 ATTR_LAYOUT_ALIGN_PARENT_TOP, 474 ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, 475 ATTR_LAYOUT_ALIGN_PARENT_LEFT, 476 ATTR_LAYOUT_ALIGN_PARENT_RIGHT, 477 ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, 478 ATTR_LAYOUT_CENTER_HORIZONTAL, 479 ATTR_LAYOUT_CENTER_IN_PARENT, 480 ATTR_LAYOUT_CENTER_VERTICAL, 481 482 // From GridLayout 483 ATTR_LAYOUT_ROW, 484 ATTR_LAYOUT_ROW_SPAN, 485 ATTR_LAYOUT_COLUMN, 486 ATTR_LAYOUT_COLUMN_SPAN 487 }; 488 489 /** 490 * Default attribute filter used by the various layouts to filter out some properties 491 * we don't want to offer. 492 */ 493 public static final AttributeFilter DEFAULT_ATTR_FILTER = new AttributeFilter() { 494 Set<String> mExcludes; 495 496 @Override 497 public String replace(String uri, String name, String value) { 498 if (!ANDROID_URI.equals(uri)) { 499 return value; 500 } 501 502 if (mExcludes == null) { 503 mExcludes = new HashSet<String>(EXCLUDED_ATTRIBUTES.length); 504 mExcludes.addAll(Arrays.asList(EXCLUDED_ATTRIBUTES)); 505 } 506 507 return mExcludes.contains(name) ? null : value; 508 } 509 }; 510 511 /** 512 * Copies all the attributes from oldElement to newNode. Uses the idMap to 513 * transform the value of all attributes of Format.REFERENCE. If filter is 514 * non-null, it's a filter that can rewrite the attribute string. 515 */ 516 protected static void addAttributes(INode newNode, IDragElement oldElement, 517 Map<String, Pair<String, String>> idMap, AttributeFilter filter) { 518 519 for (IDragAttribute attr : oldElement.getAttributes()) { 520 String uri = attr.getUri(); 521 String name = attr.getName(); 522 String value = attr.getValue(); 523 524 IAttributeInfo attrInfo = newNode.getAttributeInfo(uri, name); 525 if (attrInfo != null) { 526 if (attrInfo.getFormats().contains(IAttributeInfo.Format.REFERENCE)) { 527 if (idMap.containsKey(value)) { 528 value = idMap.get(value).getFirst(); 529 } 530 } 531 } 532 533 if (filter != null) { 534 value = filter.replace(uri, name, value); 535 } 536 if (value != null && value.length() > 0) { 537 newNode.setAttribute(uri, name, value); 538 } 539 } 540 } 541 542 /** 543 * Adds all the children elements of oldElement to newNode, recursively. 544 * Attributes are adjusted by calling addAttributes with idMap as necessary, 545 * with no closure filter. 546 */ 547 protected static void addInnerElements(INode newNode, IDragElement oldElement, 548 Map<String, Pair<String, String>> idMap) { 549 550 for (IDragElement element : oldElement.getInnerElements()) { 551 String fqcn = element.getFqcn(); 552 INode childNode = newNode.appendChild(fqcn); 553 554 addAttributes(childNode, element, idMap, null /* filter */); 555 addInnerElements(childNode, element, idMap); 556 } 557 } 558 559 /** 560 * Insert the given elements into the given node at the given position 561 * 562 * @param targetNode the node to insert into 563 * @param elements the elements to insert 564 * @param createNewIds if true, generate new ids when there is a conflict 565 * @param initialInsertPos index among targetnode's children which to insert the 566 * children 567 */ 568 public static void insertAt(final INode targetNode, final IDragElement[] elements, 569 final boolean createNewIds, final int initialInsertPos) { 570 571 // Collect IDs from dropped elements and remap them to new IDs 572 // if this is a copy or from a different canvas. 573 final Map<String, Pair<String, String>> idMap = getDropIdMap(targetNode, elements, 574 createNewIds); 575 576 targetNode.editXml("Insert Elements", new INodeHandler() { 577 578 @Override 579 public void handle(INode node) { 580 // Now write the new elements. 581 int insertPos = initialInsertPos; 582 for (IDragElement element : elements) { 583 String fqcn = element.getFqcn(); 584 585 INode newChild = targetNode.insertChildAt(fqcn, insertPos); 586 587 // insertPos==-1 means to insert at the end. Otherwise 588 // increment the insertion position. 589 if (insertPos >= 0) { 590 insertPos++; 591 } 592 593 // Copy all the attributes, modifying them as needed. 594 addAttributes(newChild, element, idMap, DEFAULT_ATTR_FILTER); 595 addInnerElements(newChild, element, idMap); 596 } 597 } 598 }); 599 } 600 601 // ---- Resizing ---- 602 603 /** Creates a new {@link ResizeState} object to track resize state */ 604 protected ResizeState createResizeState(INode layout, Object layoutView, INode node) { 605 return new ResizeState(this, layout, layoutView, node); 606 } 607 608 @Override 609 public DropFeedback onResizeBegin(INode child, INode parent, 610 SegmentType horizontalEdge, SegmentType verticalEdge, 611 Object childView, Object parentView) { 612 ResizeState state = createResizeState(parent, parentView, child); 613 state.horizontalEdgeType = horizontalEdge; 614 state.verticalEdgeType = verticalEdge; 615 616 // Compute preferred (wrap_content) size such that we can offer guidelines to 617 // snap to the preferred size 618 Map<INode, Rect> sizes = mRulesEngine.measureChildren(parent, 619 new IClientRulesEngine.AttributeFilter() { 620 @Override 621 public String getAttribute(INode node, String namespace, String localName) { 622 // Change attributes to wrap_content 623 if (ATTR_LAYOUT_WIDTH.equals(localName) 624 && SdkConstants.NS_RESOURCES.equals(namespace)) { 625 return VALUE_WRAP_CONTENT; 626 } 627 if (ATTR_LAYOUT_HEIGHT.equals(localName) 628 && SdkConstants.NS_RESOURCES.equals(namespace)) { 629 return VALUE_WRAP_CONTENT; 630 } 631 632 return null; 633 } 634 }); 635 if (sizes != null) { 636 state.wrapBounds = sizes.get(child); 637 } 638 639 return new DropFeedback(state, new IFeedbackPainter() { 640 @Override 641 public void paint(IGraphics gc, INode node, DropFeedback feedback) { 642 ResizeState resizeState = (ResizeState) feedback.userData; 643 if (resizeState != null && resizeState.bounds != null) { 644 paintResizeFeedback(gc, node, resizeState); 645 } 646 } 647 }); 648 } 649 650 protected void paintResizeFeedback(IGraphics gc, INode node, ResizeState resizeState) { 651 gc.useStyle(DrawingStyle.RESIZE_PREVIEW); 652 Rect b = resizeState.bounds; 653 gc.drawRect(b); 654 655 if (resizeState.horizontalFillSegment != null) { 656 gc.useStyle(DrawingStyle.GUIDELINE); 657 Segment s = resizeState.horizontalFillSegment; 658 gc.drawLine(s.from, s.at, s.to, s.at); 659 } 660 if (resizeState.verticalFillSegment != null) { 661 gc.useStyle(DrawingStyle.GUIDELINE); 662 Segment s = resizeState.verticalFillSegment; 663 gc.drawLine(s.at, s.from, s.at, s.to); 664 } 665 666 if (resizeState.wrapBounds != null) { 667 gc.useStyle(DrawingStyle.GUIDELINE); 668 int wrapWidth = resizeState.wrapBounds.w; 669 int wrapHeight = resizeState.wrapBounds.h; 670 671 // Show the "wrap_content" guideline. 672 // If we are showing both the wrap_width and wrap_height lines 673 // then we show at most the rectangle formed by the two lines; 674 // otherwise we show the entire width of the line 675 if (resizeState.horizontalEdgeType != null) { 676 int y = -1; 677 switch (resizeState.horizontalEdgeType) { 678 case TOP: 679 y = b.y + b.h - wrapHeight; 680 break; 681 case BOTTOM: 682 y = b.y + wrapHeight; 683 break; 684 default: assert false : resizeState.horizontalEdgeType; 685 } 686 if (resizeState.verticalEdgeType != null) { 687 switch (resizeState.verticalEdgeType) { 688 case LEFT: 689 gc.drawLine(b.x + b.w - wrapWidth, y, b.x + b.w, y); 690 break; 691 case RIGHT: 692 gc.drawLine(b.x, y, b.x + wrapWidth, y); 693 break; 694 default: assert false : resizeState.verticalEdgeType; 695 } 696 } else { 697 gc.drawLine(b.x, y, b.x + b.w, y); 698 } 699 } 700 if (resizeState.verticalEdgeType != null) { 701 int x = -1; 702 switch (resizeState.verticalEdgeType) { 703 case LEFT: 704 x = b.x + b.w - wrapWidth; 705 break; 706 case RIGHT: 707 x = b.x + wrapWidth; 708 break; 709 default: assert false : resizeState.verticalEdgeType; 710 } 711 if (resizeState.horizontalEdgeType != null) { 712 switch (resizeState.horizontalEdgeType) { 713 case TOP: 714 gc.drawLine(x, b.y + b.h - wrapHeight, x, b.y + b.h); 715 break; 716 case BOTTOM: 717 gc.drawLine(x, b.y, x, b.y + wrapHeight); 718 break; 719 default: assert false : resizeState.horizontalEdgeType; 720 } 721 } else { 722 gc.drawLine(x, b.y, x, b.y + b.h); 723 } 724 } 725 } 726 } 727 728 /** 729 * Returns the maximum number of pixels will be considered a "match" when snapping 730 * resize or move positions to edges or other constraints 731 * 732 * @return the maximum number of pixels to consider for snapping 733 */ 734 public static final int getMaxMatchDistance() { 735 // TODO - make constant once we're happy with the feel 736 return 20; 737 } 738 739 @Override 740 public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, 741 Rect newBounds, int modifierMask) { 742 ResizeState state = (ResizeState) feedback.userData; 743 state.bounds = newBounds; 744 state.modifierMask = modifierMask; 745 746 // Match on wrap bounds 747 state.wrapWidth = state.wrapHeight = false; 748 if (state.wrapBounds != null) { 749 Rect b = state.wrapBounds; 750 int maxMatchDistance = getMaxMatchDistance(); 751 if (state.horizontalEdgeType != null) { 752 if (Math.abs(newBounds.h - b.h) < maxMatchDistance) { 753 state.wrapHeight = true; 754 if (state.horizontalEdgeType == SegmentType.TOP) { 755 newBounds.y += newBounds.h - b.h; 756 } 757 newBounds.h = b.h; 758 } 759 } 760 if (state.verticalEdgeType != null) { 761 if (Math.abs(newBounds.w - b.w) < maxMatchDistance) { 762 state.wrapWidth = true; 763 if (state.verticalEdgeType == SegmentType.LEFT) { 764 newBounds.x += newBounds.w - b.w; 765 } 766 newBounds.w = b.w; 767 } 768 } 769 } 770 771 // Match on fill bounds 772 state.horizontalFillSegment = null; 773 state.fillHeight = false; 774 if (state.horizontalEdgeType == SegmentType.BOTTOM && !state.wrapHeight) { 775 Rect parentBounds = parent.getBounds(); 776 state.horizontalFillSegment = new Segment(parentBounds.y2(), newBounds.x, 777 newBounds.x2(), 778 null /*node*/, null /*id*/, SegmentType.BOTTOM, MarginType.NO_MARGIN); 779 if (Math.abs(newBounds.y2() - parentBounds.y2()) < getMaxMatchDistance()) { 780 state.fillHeight = true; 781 newBounds.h = parentBounds.y2() - newBounds.y; 782 } 783 } 784 state.verticalFillSegment = null; 785 state.fillWidth = false; 786 if (state.verticalEdgeType == SegmentType.RIGHT && !state.wrapWidth) { 787 Rect parentBounds = parent.getBounds(); 788 state.verticalFillSegment = new Segment(parentBounds.x2(), newBounds.y, 789 newBounds.y2(), 790 null /*node*/, null /*id*/, SegmentType.RIGHT, MarginType.NO_MARGIN); 791 if (Math.abs(newBounds.x2() - parentBounds.x2()) < getMaxMatchDistance()) { 792 state.fillWidth = true; 793 newBounds.w = parentBounds.x2() - newBounds.x; 794 } 795 } 796 797 feedback.tooltip = getResizeUpdateMessage(state, child, parent, 798 newBounds, state.horizontalEdgeType, state.verticalEdgeType); 799 } 800 801 @Override 802 public void onResizeEnd(DropFeedback feedback, INode child, final INode parent, 803 final Rect newBounds) { 804 final Rect oldBounds = child.getBounds(); 805 if (oldBounds.w != newBounds.w || oldBounds.h != newBounds.h) { 806 final ResizeState state = (ResizeState) feedback.userData; 807 child.editXml("Resize", new INodeHandler() { 808 @Override 809 public void handle(INode n) { 810 setNewSizeBounds(state, n, parent, oldBounds, newBounds, 811 state.horizontalEdgeType, state.verticalEdgeType); 812 } 813 }); 814 } 815 } 816 817 /** 818 * Returns the message to display to the user during the resize operation 819 * 820 * @param resizeState the current resize state 821 * @param child the child node being resized 822 * @param parent the parent of the resized node 823 * @param newBounds the new bounds to resize the child to, in pixels 824 * @param horizontalEdge the horizontal edge being resized 825 * @param verticalEdge the vertical edge being resized 826 * @return the message to display for the current resize bounds 827 */ 828 protected String getResizeUpdateMessage(ResizeState resizeState, INode child, INode parent, 829 Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { 830 String width = resizeState.getWidthAttribute(); 831 String height = resizeState.getHeightAttribute(); 832 833 if (horizontalEdge == null) { 834 return width; 835 } else if (verticalEdge == null) { 836 return height; 837 } else { 838 // U+00D7: Unicode for multiplication sign 839 return String.format("%s \u00D7 %s", width, height); 840 } 841 } 842 843 /** 844 * Performs the edit on the node to complete a resizing operation. The actual edit 845 * part is pulled out such that subclasses can change/add to the edits and be part of 846 * the same undo event 847 * 848 * @param resizeState the current resize state 849 * @param node the child node being resized 850 * @param layout the parent of the resized node 851 * @param newBounds the new bounds to resize the child to, in pixels 852 * @param horizontalEdge the horizontal edge being resized 853 * @param verticalEdge the vertical edge being resized 854 */ 855 protected void setNewSizeBounds(ResizeState resizeState, INode node, INode layout, 856 Rect oldBounds, Rect newBounds, SegmentType horizontalEdge, SegmentType verticalEdge) { 857 if (verticalEdge != null 858 && (newBounds.w != oldBounds.w || resizeState.wrapWidth || resizeState.fillWidth)) { 859 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_WIDTH, resizeState.getWidthAttribute()); 860 } 861 if (horizontalEdge != null 862 && (newBounds.h != oldBounds.h || resizeState.wrapHeight || resizeState.fillHeight)) { 863 node.setAttribute(ANDROID_URI, ATTR_LAYOUT_HEIGHT, resizeState.getHeightAttribute()); 864 } 865 } 866 } 867