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