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