• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
onInitialize(String fqcn, IClientRulesEngine engine)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      */
getRulesEngine()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
addContextMenuActions(List<RuleAction> actions, final INode selectedNode)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 
getPropertyMapKey(INode node)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      */
addInheritedProperties(List<RuleAction> propertyActionTypes, INode node, final IMenuCallback onChange, int sortPriority)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      */
addCommonPropertyActions(List<RuleAction> actions, INode selectedNode, IMenuCallback onChange, int sortPriority)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      */
editedProperty(String property)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      */
addRecentPropertyActions(List<RuleAction> actions, INode selectedNode, IMenuCallback onChange)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      */
addPropertyActions(List<RuleAction> actions, INode selectedNode, IMenuCallback onChange, String definedBy, boolean layoutParamsOnly)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 
createPropertyAction(Prop p, String id, String title, INode selectedNode, IMenuCallback onChange, int sortPriority)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 
getPropertyMetadata(final INode selectedNode)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 
EnumPropertyChoiceProvider(Prop property)767         public EnumPropertyChoiceProvider(Prop property) {
768             super();
769             this.mProperty = property;
770         }
771 
772         @Override
addChoices(List<String> titles, List<URL> iconUrls, List<String> ids)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      */
isFilled(INode node, String attribute)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      */
getFillParentValueName()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      */
supportsMatchParent()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 */
join(char delimiter, Collection<String> strings)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 
concatenate(Map<String, String> pre, Map<String, String> post)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
mapify(String... values)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      */
getAttributeDisplayName(String name)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
onPaste(INode targetNode, Object targetView, IDragElement[] elements)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 
Prop(String title, boolean isToggle, boolean isFlag, Map<String, String> choices, String definedBy)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 
getDefinedBy()923         public String getDefinedBy() {
924             return mDefinedBy;
925         }
926 
Prop(String title, boolean isToggle, String definedBy)927         public Prop(String title, boolean isToggle, String definedBy) {
928             this(title, isToggle, false, null, definedBy);
929         }
930 
isToggle()931         private boolean isToggle() {
932             return mToggle;
933         }
934 
isFlag()935         private boolean isFlag() {
936             return mFlag && mChoices != null;
937         }
938 
isEnum()939         private boolean isEnum() {
940             return !mFlag && mChoices != null;
941         }
942 
getTitle()943         private String getTitle() {
944             return mTitle;
945         }
946 
getChoices()947         private Map<String, String> getChoices() {
948             return mChoices;
949         }
950 
isStringEdit()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      */
getSampleImageSrc()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      */
stripIdPrefix(String id)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 
ensureValidString(String value)985     private static String ensureValidString(String value) {
986         if (value == null) {
987             value = ""; //$NON-NLS-1$
988         }
989         return value;
990     }
991  }
992