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.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