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