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