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