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