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