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_HINT; 21 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID; 22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT; 23 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_PREFIX; 24 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH; 25 import static com.android.ide.common.layout.LayoutConstants.ATTR_STYLE; 26 import static com.android.ide.common.layout.LayoutConstants.ATTR_TEXT; 27 import static com.android.ide.common.layout.LayoutConstants.DOT_LAYOUT_PARAMS; 28 import static com.android.ide.common.layout.LayoutConstants.ID_PREFIX; 29 import static com.android.ide.common.layout.LayoutConstants.NEW_ID_PREFIX; 30 import static com.android.ide.common.layout.LayoutConstants.VALUE_FILL_PARENT; 31 import static com.android.ide.common.layout.LayoutConstants.VALUE_MATCH_PARENT; 32 import static com.android.ide.common.layout.LayoutConstants.VALUE_WRAP_CONTENT; 33 34 import com.android.ide.common.api.DropFeedback; 35 import com.android.ide.common.api.IAttributeInfo; 36 import com.android.ide.common.api.IAttributeInfo.Format; 37 import com.android.ide.common.api.IClientRulesEngine; 38 import com.android.ide.common.api.IDragElement; 39 import com.android.ide.common.api.IGraphics; 40 import com.android.ide.common.api.IMenuCallback; 41 import com.android.ide.common.api.INode; 42 import com.android.ide.common.api.IValidator; 43 import com.android.ide.common.api.IViewMetadata; 44 import com.android.ide.common.api.IViewRule; 45 import com.android.ide.common.api.InsertType; 46 import com.android.ide.common.api.Point; 47 import com.android.ide.common.api.Rect; 48 import com.android.ide.common.api.RuleAction; 49 import com.android.ide.common.api.RuleAction.ActionProvider; 50 import com.android.ide.common.api.RuleAction.ChoiceProvider; 51 import com.android.ide.common.api.SegmentType; 52 import com.android.resources.ResourceType; 53 import com.android.util.Pair; 54 55 import java.net.URL; 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Collection; 59 import java.util.Collections; 60 import java.util.Comparator; 61 import java.util.HashMap; 62 import java.util.HashSet; 63 import java.util.LinkedList; 64 import java.util.List; 65 import java.util.Map; 66 import java.util.Map.Entry; 67 import java.util.Set; 68 69 /** 70 * Common IViewRule processing to all view and layout classes. 71 */ 72 public class BaseViewRule implements IViewRule { 73 /** List of recently edited properties */ 74 private static List<String> sRecent = new LinkedList<String>(); 75 76 /** Maximum number of recent properties to track and list */ 77 private final static int MAX_RECENT_COUNT = 12; 78 79 // Strings used as internal ids, group ids and prefixes for actions 80 private static final String FALSE_ID = "false"; //$NON-NLS-1$ 81 private static final String TRUE_ID = "true"; //$NON-NLS-1$ 82 private static final String PROP_PREFIX = "@prop@"; //$NON-NLS-1$ 83 private static final String CLEAR_ID = "clear"; //$NON-NLS-1$ 84 private static final String ZCUSTOM = "zcustom"; //$NON-NLS-1$ 85 86 protected IClientRulesEngine mRulesEngine; 87 88 // Cache of attributes. Key is FQCN of a node mixed with its view hierarchy 89 // parent. Values are a custom map as needed by getContextMenu. 90 private Map<String, Map<String, Prop>> mAttributesMap = 91 new HashMap<String, Map<String, Prop>>(); 92 93 public boolean onInitialize(String fqcn, IClientRulesEngine engine) { 94 this.mRulesEngine = engine; 95 96 // This base rule can handle any class so we don't need to filter on 97 // FQCN. Derived classes should do so if they can handle some 98 // subclasses. 99 100 // If onInitialize returns false, it means it can't handle the given 101 // FQCN and will be unloaded. 102 103 return true; 104 } 105 106 public void onDispose() { 107 // Nothing to dispose. 108 } 109 110 public String getDisplayName() { 111 // Default is to not override the selection display name. 112 return null; 113 } 114 115 /** 116 * Returns the {@link IClientRulesEngine} associated with this {@link IViewRule} 117 * 118 * @return the {@link IClientRulesEngine} associated with this {@link IViewRule} 119 */ 120 public IClientRulesEngine getRulesEngine() { 121 return mRulesEngine; 122 } 123 124 // === Context Menu === 125 126 /** 127 * Generate custom actions for the context menu: <br/> 128 * - Explicit layout_width and layout_height attributes. 129 * - List of all other simple toggle attributes. 130 */ 131 public void addContextMenuActions(List<RuleAction> actions, final INode selectedNode) { 132 String width = null; 133 String currentWidth = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH); 134 135 String fillParent = getFillParentValueName(); 136 boolean canMatchParent = supportsMatchParent(); 137 if (canMatchParent && VALUE_FILL_PARENT.equals(currentWidth)) { 138 currentWidth = VALUE_MATCH_PARENT; 139 } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(currentWidth)) { 140 currentWidth = VALUE_FILL_PARENT; 141 } else if (!VALUE_WRAP_CONTENT.equals(currentWidth) && !fillParent.equals(currentWidth)) { 142 width = currentWidth; 143 } 144 145 String height = null; 146 String currentHeight = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT); 147 148 if (canMatchParent && VALUE_FILL_PARENT.equals(currentHeight)) { 149 currentHeight = VALUE_MATCH_PARENT; 150 } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(currentHeight)) { 151 currentHeight = VALUE_FILL_PARENT; 152 } else if (!VALUE_WRAP_CONTENT.equals(currentHeight) 153 && !fillParent.equals(currentHeight)) { 154 height = currentHeight; 155 } 156 final String newWidth = width; 157 final String newHeight = height; 158 159 final IMenuCallback onChange = new IMenuCallback() { 160 public void action( 161 final RuleAction action, 162 final List<? extends INode> selectedNodes, 163 final String valueId, final Boolean newValue) { 164 String fullActionId = action.getId(); 165 boolean isProp = fullActionId.startsWith(PROP_PREFIX); 166 final String actionId = isProp ? 167 fullActionId.substring(PROP_PREFIX.length()) : fullActionId; 168 169 if (fullActionId.equals(ATTR_LAYOUT_WIDTH)) { 170 final String newAttrValue = getValue(valueId, newWidth); 171 if (newAttrValue != null) { 172 for (INode node : selectedNodes) { 173 node.editXml("Change Attribute " + ATTR_LAYOUT_WIDTH, 174 new PropertySettingNodeHandler(ANDROID_URI, 175 ATTR_LAYOUT_WIDTH, newAttrValue)); 176 } 177 editedProperty(ATTR_LAYOUT_WIDTH); 178 } 179 return; 180 } else if (fullActionId.equals(ATTR_LAYOUT_HEIGHT)) { 181 // Ask the user 182 final String newAttrValue = getValue(valueId, newHeight); 183 if (newAttrValue != null) { 184 for (INode node : selectedNodes) { 185 node.editXml("Change Attribute " + ATTR_LAYOUT_HEIGHT, 186 new PropertySettingNodeHandler(ANDROID_URI, 187 ATTR_LAYOUT_HEIGHT, newAttrValue)); 188 } 189 editedProperty(ATTR_LAYOUT_HEIGHT); 190 } 191 return; 192 } else if (fullActionId.equals(ATTR_ID)) { 193 // Ids must be set individually so open the id dialog for each 194 // selected node (though allow cancel to break the loop) 195 for (INode node : selectedNodes) { 196 // Strip off the @id prefix stuff 197 String oldId = node.getStringAttr(ANDROID_URI, ATTR_ID); 198 oldId = stripIdPrefix(ensureValidString(oldId)); 199 IValidator validator = mRulesEngine.getResourceValidator(); 200 String newId = mRulesEngine.displayInput("New Id:", oldId, validator); 201 if (newId != null && newId.trim().length() > 0) { 202 if (!newId.startsWith(NEW_ID_PREFIX)) { 203 newId = NEW_ID_PREFIX + stripIdPrefix(newId); 204 } 205 node.editXml("Change ID", new PropertySettingNodeHandler(ANDROID_URI, 206 ATTR_ID, newId)); 207 editedProperty(ATTR_ID); 208 } else if (newId == null) { 209 // Cancelled 210 break; 211 } 212 } 213 return; 214 } else if (isProp) { 215 INode firstNode = selectedNodes.get(0); 216 String key = getPropertyMapKey(selectedNode); 217 Map<String, Prop> props = mAttributesMap.get(key); 218 final Prop prop = (props != null) ? props.get(actionId) : null; 219 220 if (prop != null) { 221 editedProperty(actionId); 222 223 // For custom values (requiring an input dialog) input the 224 // value outside the undo-block. 225 // Input the value as a text, unless we know it's the "text" or 226 // "style" attributes (where we know we want to ask for specific 227 // resource types). 228 String uri = ANDROID_URI; 229 String v = null; 230 if (prop.isStringEdit()) { 231 boolean isStyle = actionId.equals(ATTR_STYLE); 232 boolean isText = actionId.equals(ATTR_TEXT); 233 boolean isHint = actionId.equals(ATTR_HINT); 234 if (isStyle || isText || isHint) { 235 String resourceTypeName = isStyle 236 ? ResourceType.STYLE.getName() 237 : ResourceType.STRING.getName(); 238 String oldValue = selectedNodes.size() == 1 239 ? firstNode.getStringAttr(null, ATTR_STYLE) 240 : ""; //$NON-NLS-1$ 241 oldValue = ensureValidString(oldValue); 242 v = mRulesEngine.displayResourceInput(resourceTypeName, oldValue); 243 if (isStyle) { 244 uri = null; 245 } 246 } else { 247 v = inputAttributeValue(firstNode, actionId); 248 } 249 } 250 final String customValue = v; 251 252 for (INode n : selectedNodes) { 253 if (prop.isToggle()) { 254 // case of toggle 255 String value = ""; //$NON-NLS-1$ 256 if (valueId.equals(TRUE_ID)) { 257 value = newValue ? "true" : ""; //$NON-NLS-1$ //$NON-NLS-2$ 258 } else if (valueId.equals(FALSE_ID)) { 259 value = newValue ? "false" : "";//$NON-NLS-1$ //$NON-NLS-2$ 260 } 261 n.setAttribute(uri, actionId, value); 262 } else if (prop.isFlag()) { 263 // case of a flag 264 String values = ""; //$NON-NLS-1$ 265 if (!valueId.equals(CLEAR_ID)) { 266 values = n.getStringAttr(ANDROID_URI, actionId); 267 Set<String> newValues = new HashSet<String>(); 268 if (values != null) { 269 newValues.addAll(Arrays.asList( 270 values.split("\\|"))); //$NON-NLS-1$ 271 } 272 if (newValue) { 273 newValues.add(valueId); 274 } else { 275 newValues.remove(valueId); 276 } 277 278 List<String> sorted = new ArrayList<String>(newValues); 279 Collections.sort(sorted); 280 values = join('|', sorted); 281 282 // Special case 283 if (valueId.equals("normal")) { //$NON-NLS-1$ 284 // For textStyle for example, if you have "bold|italic" 285 // and you select the "normal" property, this should 286 // not behave in the normal flag way and "or" itself in; 287 // it should replace the other two. 288 // This also applies to imeOptions. 289 values = valueId; 290 } 291 } 292 n.setAttribute(uri, actionId, values); 293 } else if (prop.isEnum()) { 294 // case of an enum 295 String value = ""; //$NON-NLS-1$ 296 if (!valueId.equals(CLEAR_ID)) { 297 value = newValue ? valueId : ""; //$NON-NLS-1$ 298 } 299 n.setAttribute(uri, actionId, value); 300 } else { 301 assert prop.isStringEdit(); 302 // We've already received the value outside the undo block 303 if (customValue != null) { 304 n.setAttribute(uri, actionId, customValue); 305 } 306 } 307 } 308 } 309 } 310 } 311 312 /** 313 * Input the custom value for the given attribute. This will use the Reference 314 * Chooser if it is a reference value, otherwise a plain text editor. 315 */ 316 private String inputAttributeValue(final INode node, final String attribute) { 317 String oldValue = node.getStringAttr(ANDROID_URI, attribute); 318 oldValue = ensureValidString(oldValue); 319 IAttributeInfo attributeInfo = node.getAttributeInfo(ANDROID_URI, attribute); 320 if (attributeInfo != null 321 && IAttributeInfo.Format.REFERENCE.in(attributeInfo.getFormats())) { 322 return mRulesEngine.displayReferenceInput(oldValue); 323 } else { 324 // A single resource type? If so use a resource chooser initialized 325 // to this specific type 326 /* This does not work well, because the metadata is a bit misleading: 327 * for example a Button's "text" property and a Button's "onClick" property 328 * both claim to be of type [string], but @string/ is NOT valid for 329 * onClick.. 330 if (attributeInfo != null && attributeInfo.getFormats().length == 1) { 331 // Resource chooser 332 Format format = attributeInfo.getFormats()[0]; 333 return mRulesEngine.displayResourceInput(format.name(), oldValue); 334 } 335 */ 336 337 // Fallback: just edit the raw XML string 338 String message = String.format("New %1$s Value:", attribute); 339 return mRulesEngine.displayInput(message, oldValue, null); 340 } 341 } 342 343 /** 344 * Returns the value (which will ask the user if the value is the special 345 * {@link #ZCUSTOM} marker 346 */ 347 private String getValue(String valueId, String defaultValue) { 348 if (valueId.equals(ZCUSTOM)) { 349 if (defaultValue == null) { 350 defaultValue = ""; 351 } 352 String value = mRulesEngine.displayInput( 353 "Set custom layout attribute value (example: 50dp)", 354 defaultValue, null); 355 if (value != null && value.trim().length() > 0) { 356 return value.trim(); 357 } else { 358 return null; 359 } 360 } 361 362 return valueId; 363 } 364 }; 365 366 IAttributeInfo textAttribute = selectedNode.getAttributeInfo(ANDROID_URI, ATTR_TEXT); 367 if (textAttribute != null) { 368 actions.add(RuleAction.createAction(PROP_PREFIX + ATTR_TEXT, "Edit Text...", onChange, 369 null, 10, true)); 370 } 371 372 actions.add(RuleAction.createAction(ATTR_ID, "Edit ID...", onChange, null, 20, true)); 373 374 addCommonPropertyActions(actions, selectedNode, onChange, 21); 375 376 // Create width choice submenu 377 actions.add(RuleAction.createSeparator(32)); 378 List<Pair<String, String>> widthChoices = new ArrayList<Pair<String,String>>(4); 379 widthChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content")); 380 if (canMatchParent) { 381 widthChoices.add(Pair.of(VALUE_MATCH_PARENT, "Match Parent")); 382 } else { 383 widthChoices.add(Pair.of(VALUE_FILL_PARENT, "Fill Parent")); 384 } 385 if (width != null) { 386 widthChoices.add(Pair.of(width, width)); 387 } 388 widthChoices.add(Pair.of(ZCUSTOM, "Other...")); 389 actions.add(RuleAction.createChoices( 390 ATTR_LAYOUT_WIDTH, "Layout Width", 391 onChange, 392 null /* iconUrls */, 393 currentWidth, 394 null, 35, 395 true, // supportsMultipleNodes 396 widthChoices)); 397 398 // Create height choice submenu 399 List<Pair<String, String>> heightChoices = new ArrayList<Pair<String,String>>(4); 400 heightChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content")); 401 if (canMatchParent) { 402 heightChoices.add(Pair.of(VALUE_MATCH_PARENT, "Match Parent")); 403 } else { 404 heightChoices.add(Pair.of(VALUE_FILL_PARENT, "Fill Parent")); 405 } 406 if (height != null) { 407 heightChoices.add(Pair.of(height, height)); 408 } 409 heightChoices.add(Pair.of(ZCUSTOM, "Other...")); 410 actions.add(RuleAction.createChoices( 411 ATTR_LAYOUT_HEIGHT, "Layout Height", 412 onChange, 413 null /* iconUrls */, 414 currentHeight, 415 null, 40, 416 true, 417 heightChoices)); 418 419 actions.add(RuleAction.createSeparator(45)); 420 RuleAction properties = RuleAction.createChoices("properties", "Other Properties", //$NON-NLS-1$ 421 onChange /*callback*/, null /*icon*/, 50, 422 true /*supportsMultipleNodes*/, new ActionProvider() { 423 public List<RuleAction> getNestedActions(INode node) { 424 List<RuleAction> propertyActionTypes = new ArrayList<RuleAction>(); 425 propertyActionTypes.add(RuleAction.createChoices( 426 "recent", "Recent", //$NON-NLS-1$ 427 onChange /*callback*/, null /*icon*/, 10, 428 true /*supportsMultipleNodes*/, new ActionProvider() { 429 public List<RuleAction> getNestedActions(INode n) { 430 List<RuleAction> propertyActions = new ArrayList<RuleAction>(); 431 addRecentPropertyActions(propertyActions, n, onChange); 432 return propertyActions; 433 } 434 })); 435 436 propertyActionTypes.add(RuleAction.createSeparator(20)); 437 438 addInheritedProperties(propertyActionTypes, node, onChange, 30); 439 440 propertyActionTypes.add(RuleAction.createSeparator(50)); 441 propertyActionTypes.add(RuleAction.createChoices( 442 "layoutparams", "Layout Parameters", //$NON-NLS-1$ 443 onChange /*callback*/, null /*icon*/, 60, 444 true /*supportsMultipleNodes*/, new ActionProvider() { 445 public List<RuleAction> getNestedActions(INode n) { 446 List<RuleAction> propertyActions = new ArrayList<RuleAction>(); 447 addPropertyActions(propertyActions, n, onChange, null, true); 448 return propertyActions; 449 } 450 })); 451 452 propertyActionTypes.add(RuleAction.createSeparator(70)); 453 454 propertyActionTypes.add(RuleAction.createChoices( 455 "allprops", "All By Name", //$NON-NLS-1$ 456 onChange /*callback*/, null /*icon*/, 80, 457 true /*supportsMultipleNodes*/, new ActionProvider() { 458 public List<RuleAction> getNestedActions(INode n) { 459 List<RuleAction> propertyActions = new ArrayList<RuleAction>(); 460 addPropertyActions(propertyActions, n, onChange, null, false); 461 return propertyActions; 462 } 463 })); 464 465 return propertyActionTypes; 466 } 467 }); 468 469 actions.add(properties); 470 } 471 472 private static String getPropertyMapKey(INode node) { 473 // Compute the key for mAttributesMap. This depends on the type of this 474 // node and its parent in the view hierarchy. 475 StringBuilder sb = new StringBuilder(); 476 sb.append(node.getFqcn()); 477 sb.append('_'); 478 INode parent = node.getParent(); 479 if (parent != null) { 480 sb.append(parent.getFqcn()); 481 } 482 return sb.toString(); 483 } 484 485 /** 486 * Adds menu items for the inherited attributes, one pull-right menu for each super class 487 * that defines attributes. 488 * 489 * @param propertyActionTypes the actions list to add into 490 * @param node the node to apply the attributes to 491 * @param onChange the callback to use for setting attributes 492 * @param sortPriority the initial sort attribute for the first menu item 493 */ 494 private void addInheritedProperties(List<RuleAction> propertyActionTypes, INode node, 495 final IMenuCallback onChange, int sortPriority) { 496 List<String> attributeSources = node.getAttributeSources(); 497 for (final String definedBy : attributeSources) { 498 String sourceClass = definedBy; 499 500 // Strip package prefixes when necessary 501 int index = sourceClass.length(); 502 if (sourceClass.endsWith(DOT_LAYOUT_PARAMS)) { 503 index = sourceClass.length() - DOT_LAYOUT_PARAMS.length() - 1; 504 } 505 int lastDot = sourceClass.lastIndexOf('.', index); 506 if (lastDot != -1) { 507 sourceClass = sourceClass.substring(lastDot + 1); 508 } 509 510 String label; 511 if (definedBy.equals(node.getFqcn())) { 512 label = String.format("Defined by %1$s", sourceClass); 513 } else { 514 label = String.format("Inherited from %1$s", sourceClass); 515 } 516 517 propertyActionTypes.add(RuleAction.createChoices("def_" + definedBy, 518 label, 519 onChange /*callback*/, null /*icon*/, sortPriority++, 520 true /*supportsMultipleNodes*/, new ActionProvider() { 521 public List<RuleAction> getNestedActions(INode n) { 522 List<RuleAction> propertyActions = new ArrayList<RuleAction>(); 523 addPropertyActions(propertyActions, n, onChange, definedBy, false); 524 return propertyActions; 525 } 526 })); 527 } 528 } 529 530 /** 531 * Creates a list of properties that are commonly edited for views of the 532 * selected node's type 533 */ 534 private void addCommonPropertyActions(List<RuleAction> actions, INode selectedNode, 535 IMenuCallback onChange, int sortPriority) { 536 Map<String, Prop> properties = getPropertyMetadata(selectedNode); 537 IViewMetadata metadata = mRulesEngine.getMetadata(selectedNode.getFqcn()); 538 if (metadata != null) { 539 List<String> attributes = metadata.getTopAttributes(); 540 if (attributes.size() > 0) { 541 for (String attribute : attributes) { 542 // Text and ID are handled manually in the menu construction code because 543 // we want to place them consistently and customize the action label 544 if (ATTR_TEXT.equals(attribute) || ATTR_ID.equals(attribute)) { 545 continue; 546 } 547 548 Prop property = properties.get(attribute); 549 if (property != null) { 550 String title = property.getTitle(); 551 if (title.endsWith("...")) { 552 title = String.format("Edit %1$s", property.getTitle()); 553 } 554 actions.add(createPropertyAction(property, attribute, title, 555 selectedNode, onChange, sortPriority)); 556 sortPriority++; 557 } 558 } 559 } 560 } 561 } 562 563 /** 564 * Record that the given property was just edited; adds it to the front of 565 * the recently edited property list 566 * 567 * @param property the name of the property 568 */ 569 static void editedProperty(String property) { 570 if (sRecent.contains(property)) { 571 sRecent.remove(property); 572 } else if (sRecent.size() > MAX_RECENT_COUNT) { 573 sRecent.remove(sRecent.size() - 1); 574 } 575 sRecent.add(0, property); 576 } 577 578 /** 579 * Creates a list of recently modified properties that apply to the given selected node 580 */ 581 private void addRecentPropertyActions(List<RuleAction> actions, INode selectedNode, 582 IMenuCallback onChange) { 583 int sortPriority = 10; 584 Map<String, Prop> properties = getPropertyMetadata(selectedNode); 585 for (String attribute : sRecent) { 586 Prop property = properties.get(attribute); 587 if (property != null) { 588 actions.add(createPropertyAction(property, attribute, property.getTitle(), 589 selectedNode, onChange, sortPriority)); 590 sortPriority += 10; 591 } 592 } 593 } 594 595 /** 596 * Creates a list of nested actions representing the property-setting 597 * actions for the given selected node 598 */ 599 private void addPropertyActions(List<RuleAction> actions, INode selectedNode, 600 IMenuCallback onChange, String definedBy, boolean layoutParamsOnly) { 601 602 Map<String, Prop> properties = getPropertyMetadata(selectedNode); 603 604 int sortPriority = 10; 605 for (Map.Entry<String, Prop> entry : properties.entrySet()) { 606 String id = entry.getKey(); 607 Prop property = entry.getValue(); 608 if (layoutParamsOnly) { 609 // If we have definedBy information, that is most accurate; all layout 610 // params will be defined by a class whose name ends with 611 // .LayoutParams: 612 if (definedBy != null) { 613 if (!definedBy.endsWith(DOT_LAYOUT_PARAMS)) { 614 continue; 615 } 616 } else if (!id.startsWith(ATTR_LAYOUT_PREFIX)) { 617 continue; 618 } 619 } 620 if (definedBy != null && !definedBy.equals(property.getDefinedBy())) { 621 continue; 622 } 623 actions.add(createPropertyAction(property, id, property.getTitle(), 624 selectedNode, onChange, sortPriority)); 625 sortPriority += 10; 626 } 627 628 // The properties are coming out of map key order which isn't right, so sort 629 // alphabetically instead 630 Collections.sort(actions, new Comparator<RuleAction>() { 631 public int compare(RuleAction action1, RuleAction action2) { 632 return action1.getTitle().compareTo(action2.getTitle()); 633 } 634 }); 635 } 636 637 private RuleAction createPropertyAction(Prop p, String id, String title, INode selectedNode, 638 IMenuCallback onChange, int sortPriority) { 639 if (p.isToggle()) { 640 // Toggles are handled as a multiple-choice between true, false 641 // and nothing (clear) 642 String value = selectedNode.getStringAttr(ANDROID_URI, id); 643 if (value != null) 644 value = value.toLowerCase(); 645 if ("true".equals(value)) { //$NON-NLS-1$ 646 value = TRUE_ID; 647 } else if ("false".equals(value)) { //$NON-NLS-1$ 648 value = FALSE_ID; 649 } else { 650 value = CLEAR_ID; 651 } 652 return RuleAction.createChoices(PROP_PREFIX + id, title, 653 onChange, BOOLEAN_CHOICE_PROVIDER, 654 value, 655 null, sortPriority, 656 true); 657 } else if (p.getChoices() != null) { 658 // Enum or flags. Their possible values are the multiple-choice 659 // items, with an extra "clear" option to remove everything. 660 String current = selectedNode.getStringAttr(ANDROID_URI, id); 661 if (current == null || current.length() == 0) { 662 current = CLEAR_ID; 663 } 664 return RuleAction.createChoices(PROP_PREFIX + id, title, 665 onChange, new EnumPropertyChoiceProvider(p), 666 current, 667 null, sortPriority, 668 true); 669 } else { 670 return RuleAction.createAction( 671 PROP_PREFIX + id, 672 title, 673 onChange, 674 null, sortPriority, 675 true); 676 } 677 } 678 679 private Map<String, Prop> getPropertyMetadata(final INode selectedNode) { 680 String key = getPropertyMapKey(selectedNode); 681 Map<String, Prop> props = mAttributesMap.get(key); 682 if (props == null) { 683 // Prepare the property map 684 props = new HashMap<String, Prop>(); 685 for (IAttributeInfo attrInfo : selectedNode.getDeclaredAttributes()) { 686 String id = attrInfo != null ? attrInfo.getName() : null; 687 if (id == null || id.equals(ATTR_LAYOUT_WIDTH) || id.equals(ATTR_LAYOUT_HEIGHT)) { 688 // Layout width/height are already handled at the root level 689 continue; 690 } 691 Format[] formats = attrInfo != null ? attrInfo.getFormats() : null; 692 if (formats == null) { 693 continue; 694 } 695 696 String title = getAttributeDisplayName(id); 697 698 String definedBy = attrInfo != null ? attrInfo.getDefinedBy() : null; 699 if (IAttributeInfo.Format.BOOLEAN.in(formats)) { 700 props.put(id, new Prop(title, true, definedBy)); 701 } else if (IAttributeInfo.Format.ENUM.in(formats)) { 702 // Convert each enum into a map id=>title 703 Map<String, String> values = new HashMap<String, String>(); 704 if (attrInfo != null) { 705 for (String e : attrInfo.getEnumValues()) { 706 values.put(e, getAttributeDisplayName(e)); 707 } 708 } 709 710 props.put(id, new Prop(title, false, false, values, definedBy)); 711 } else if (IAttributeInfo.Format.FLAG.in(formats)) { 712 // Convert each flag into a map id=>title 713 Map<String, String> values = new HashMap<String, String>(); 714 if (attrInfo != null) { 715 for (String e : attrInfo.getFlagValues()) { 716 values.put(e, getAttributeDisplayName(e)); 717 } 718 } 719 720 props.put(id, new Prop(title, false, true, values, definedBy)); 721 } else { 722 props.put(id, new Prop(title + "...", false, definedBy)); 723 } 724 } 725 mAttributesMap.put(key, props); 726 } 727 return props; 728 } 729 730 /** 731 * A {@link ChoiceProvder} which provides alternatives suitable for choosing 732 * values for a boolean property: true, false, or "default". 733 */ 734 private static ChoiceProvider BOOLEAN_CHOICE_PROVIDER = new ChoiceProvider() { 735 public void addChoices(List<String> titles, List<URL> iconUrls, List<String> ids) { 736 titles.add("True"); 737 ids.add(TRUE_ID); 738 739 titles.add("False"); 740 ids.add(FALSE_ID); 741 742 titles.add(RuleAction.SEPARATOR); 743 ids.add(RuleAction.SEPARATOR); 744 745 titles.add("Default"); 746 ids.add(CLEAR_ID); 747 } 748 }; 749 750 /** 751 * A {@link ChoiceProvider} which provides the various available 752 * attribute values available for a given {@link Prop} property descriptor. 753 */ 754 private static class EnumPropertyChoiceProvider implements ChoiceProvider { 755 private Prop mProperty; 756 757 public EnumPropertyChoiceProvider(Prop property) { 758 super(); 759 this.mProperty = property; 760 } 761 762 public void addChoices(List<String> titles, List<URL> iconUrls, List<String> ids) { 763 for (Entry<String, String> entry : mProperty.getChoices().entrySet()) { 764 ids.add(entry.getKey()); 765 titles.add(entry.getValue()); 766 } 767 768 titles.add(RuleAction.SEPARATOR); 769 ids.add(RuleAction.SEPARATOR); 770 771 titles.add("Default"); 772 ids.add(CLEAR_ID); 773 } 774 } 775 776 /** 777 * Returns true if the given node is "filled" (e.g. has layout width set to match 778 * parent or fill parent 779 */ 780 protected final boolean isFilled(INode node, String attribute) { 781 String value = node.getStringAttr(ANDROID_URI, attribute); 782 return VALUE_MATCH_PARENT.equals(value) || VALUE_FILL_PARENT.equals(value); 783 } 784 785 /** 786 * Returns fill_parent or match_parent, depending on whether the minimum supported 787 * platform supports match_parent or not 788 * 789 * @return match_parent or fill_parent depending on which is supported by the project 790 */ 791 protected final String getFillParentValueName() { 792 return supportsMatchParent() ? VALUE_MATCH_PARENT : VALUE_FILL_PARENT; 793 } 794 795 /** 796 * Returns true if the project supports match_parent instead of just fill_parent 797 * 798 * @return true if the project supports match_parent instead of just fill_parent 799 */ 800 protected final boolean supportsMatchParent() { 801 // fill_parent was renamed match_parent in API level 8 802 return mRulesEngine.getMinApiLevel() >= 8; 803 } 804 805 /** Join strings into a single string with the given delimiter */ 806 static String join(char delimiter, Collection<String> strings) { 807 StringBuilder sb = new StringBuilder(100); 808 for (String s : strings) { 809 if (sb.length() > 0) { 810 sb.append(delimiter); 811 } 812 sb.append(s); 813 } 814 return sb.toString(); 815 } 816 817 static Map<String, String> concatenate(Map<String, String> pre, Map<String, String> post) { 818 Map<String, String> result = new HashMap<String, String>(pre.size() + post.size()); 819 result.putAll(pre); 820 result.putAll(post); 821 return result; 822 } 823 824 // Quick utility for building up maps declaratively to minimize the diffs 825 static Map<String, String> mapify(String... values) { 826 Map<String, String> map = new HashMap<String, String>(values.length / 2); 827 for (int i = 0; i < values.length; i += 2) { 828 String key = values[i]; 829 if (key == null) { 830 continue; 831 } 832 String value = values[i + 1]; 833 map.put(key, value); 834 } 835 836 return map; 837 } 838 839 /** 840 * Produces a display name for an attribute, usually capitalizing the attribute name 841 * and splitting up underscores into new words 842 * 843 * @param name the attribute name to convert 844 * @return a display name for the attribute name 845 */ 846 public static String getAttributeDisplayName(String name) { 847 if (name != null && name.length() > 0) { 848 StringBuilder sb = new StringBuilder(); 849 boolean capitalizeNext = true; 850 for (int i = 0, n = name.length(); i < n; i++) { 851 char c = name.charAt(i); 852 if (capitalizeNext) { 853 c = Character.toUpperCase(c); 854 } 855 capitalizeNext = false; 856 if (c == '_') { 857 c = ' '; 858 capitalizeNext = true; 859 } 860 sb.append(c); 861 } 862 863 return sb.toString(); 864 } 865 866 return name; 867 } 868 869 // ==== Selection ==== 870 871 public List<String> getSelectionHint(INode parentNode, INode childNode) { 872 return null; 873 } 874 875 public void addLayoutActions(List<RuleAction> actions, INode parentNode, 876 List<? extends INode> children) { 877 } 878 879 // ==== Drag'n'drop support ==== 880 881 // By default Views do not accept drag'n'drop. 882 public DropFeedback onDropEnter(INode targetNode, Object targetView, IDragElement[] elements) { 883 return null; 884 } 885 886 public DropFeedback onDropMove(INode targetNode, IDragElement[] elements, 887 DropFeedback feedback, Point p) { 888 return null; 889 } 890 891 public void onDropLeave(INode targetNode, IDragElement[] elements, DropFeedback feedback) { 892 // ignore 893 } 894 895 public void onDropped( 896 INode targetNode, 897 IDragElement[] elements, 898 DropFeedback feedback, 899 Point p) { 900 // ignore 901 } 902 903 // ==== Paste support ==== 904 905 /** 906 * Most views can't accept children so there's nothing to paste on them. In 907 * this case, defer the call to the parent layout and use the target node as 908 * an indication of where to paste. 909 */ 910 public void onPaste(INode targetNode, Object targetView, IDragElement[] elements) { 911 // 912 INode parent = targetNode.getParent(); 913 if (parent != null) { 914 String parentFqcn = parent.getFqcn(); 915 IViewRule parentRule = mRulesEngine.loadRule(parentFqcn); 916 917 if (parentRule instanceof BaseLayoutRule) { 918 ((BaseLayoutRule) parentRule).onPasteBeforeChild(parent, targetView, targetNode, 919 elements); 920 } 921 } 922 } 923 924 /** 925 * Support class for the context menu code. Stores state about properties in 926 * the context menu. 927 */ 928 private static class Prop { 929 private final boolean mToggle; 930 private final boolean mFlag; 931 private final String mTitle; 932 private final Map<String, String> mChoices; 933 private String mDefinedBy; 934 935 public Prop(String title, boolean isToggle, boolean isFlag, Map<String, String> choices, 936 String definedBy) { 937 mTitle = title; 938 mToggle = isToggle; 939 mFlag = isFlag; 940 mChoices = choices; 941 mDefinedBy = definedBy; 942 } 943 944 public String getDefinedBy() { 945 return mDefinedBy; 946 } 947 948 public Prop(String title, boolean isToggle, String definedBy) { 949 this(title, isToggle, false, null, definedBy); 950 } 951 952 private boolean isToggle() { 953 return mToggle; 954 } 955 956 private boolean isFlag() { 957 return mFlag && mChoices != null; 958 } 959 960 private boolean isEnum() { 961 return !mFlag && mChoices != null; 962 } 963 964 private String getTitle() { 965 return mTitle; 966 } 967 968 private Map<String, String> getChoices() { 969 return mChoices; 970 } 971 972 private boolean isStringEdit() { 973 return mChoices == null && !mToggle; 974 } 975 } 976 977 /** 978 * Returns a source attribute value which points to a sample image. This is typically 979 * used to provide an initial image shown on ImageButtons, etc. There is no guarantee 980 * that the source pointed to by this method actually exists. 981 * 982 * @return a source attribute to use for sample images, never null 983 */ 984 protected final String getSampleImageSrc() { 985 // Builtin graphics available since v1: 986 return "@android:drawable/btn_star"; //$NON-NLS-1$ 987 } 988 989 public void onCreate(INode node, INode parent, InsertType insertType) { 990 } 991 992 public void onChildInserted(INode node, INode parent, InsertType insertType) { 993 } 994 995 public void onRemovingChildren(List<INode> deleted, INode parent) { 996 } 997 998 /** 999 * Strips the {@code @+id} or {@code @id} prefix off of the given id 1000 * 1001 * @param id attribute to be stripped 1002 * @return the id name without the {@code @+id} or {@code @id} prefix 1003 */ 1004 public static String stripIdPrefix(String id) { 1005 if (id == null) { 1006 return ""; //$NON-NLS-1$ 1007 } else if (id.startsWith(NEW_ID_PREFIX)) { 1008 return id.substring(NEW_ID_PREFIX.length()); 1009 } else if (id.startsWith(ID_PREFIX)) { 1010 return id.substring(ID_PREFIX.length()); 1011 } 1012 return id; 1013 } 1014 1015 private static String ensureValidString(String value) { 1016 if (value == null) { 1017 value = ""; //$NON-NLS-1$ 1018 } 1019 return value; 1020 } 1021 1022 public void paintSelectionFeedback(IGraphics graphics, INode parentNode, 1023 List<? extends INode> childNodes, Object view) { 1024 } 1025 1026 // ---- Resizing ---- 1027 1028 public DropFeedback onResizeBegin(INode child, INode parent, SegmentType horizontalEdge, 1029 SegmentType verticalEdge, Object childView, Object parentView) { 1030 return null; 1031 } 1032 1033 public void onResizeUpdate(DropFeedback feedback, INode child, INode parent, Rect newBounds, 1034 int modifierMask) { 1035 } 1036 1037 public void onResizeEnd(DropFeedback feedback, INode child, final INode parent, 1038 final Rect newBounds) { 1039 } 1040 } 1041