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