aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java996
1 files changed, 996 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java
new file mode 100644
index 000000000..83ce9ef8f
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/common/layout/BaseViewRule.java
@@ -0,0 +1,996 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Eclipse Public License, Version 1.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.eclipse.org/org/documents/epl-v10.php
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.ide.common.layout;
+
+import static com.android.SdkConstants.ANDROID_URI;
+import static com.android.SdkConstants.ATTR_CLASS;
+import static com.android.SdkConstants.ATTR_HINT;
+import static com.android.SdkConstants.ATTR_ID;
+import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT;
+import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX;
+import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH;
+import static com.android.SdkConstants.ATTR_STYLE;
+import static com.android.SdkConstants.ATTR_TEXT;
+import static com.android.SdkConstants.DOT_LAYOUT_PARAMS;
+import static com.android.SdkConstants.ID_PREFIX;
+import static com.android.SdkConstants.NEW_ID_PREFIX;
+import static com.android.SdkConstants.VALUE_FALSE;
+import static com.android.SdkConstants.VALUE_FILL_PARENT;
+import static com.android.SdkConstants.VALUE_MATCH_PARENT;
+import static com.android.SdkConstants.VALUE_TRUE;
+import static com.android.SdkConstants.VALUE_WRAP_CONTENT;
+import static com.android.SdkConstants.VIEW_FRAGMENT;
+
+import com.android.annotations.NonNull;
+import com.android.annotations.Nullable;
+import com.android.ide.common.api.AbstractViewRule;
+import com.android.ide.common.api.IAttributeInfo;
+import com.android.ide.common.api.IAttributeInfo.Format;
+import com.android.ide.common.api.IClientRulesEngine;
+import com.android.ide.common.api.IDragElement;
+import com.android.ide.common.api.IMenuCallback;
+import com.android.ide.common.api.INode;
+import com.android.ide.common.api.IViewMetadata;
+import com.android.ide.common.api.IViewRule;
+import com.android.ide.common.api.RuleAction;
+import com.android.ide.common.api.RuleAction.ActionProvider;
+import com.android.ide.common.api.RuleAction.ChoiceProvider;
+import com.android.resources.ResourceType;
+import com.android.utils.Pair;
+
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+/**
+ * Common IViewRule processing to all view and layout classes.
+ */
+public class BaseViewRule extends AbstractViewRule {
+ /** List of recently edited properties */
+ private static List<String> sRecent = new LinkedList<String>();
+
+ /** Maximum number of recent properties to track and list */
+ private final static int MAX_RECENT_COUNT = 12;
+
+ // Strings used as internal ids, group ids and prefixes for actions
+ private static final String FALSE_ID = "false"; //$NON-NLS-1$
+ private static final String TRUE_ID = "true"; //$NON-NLS-1$
+ private static final String PROP_PREFIX = "@prop@"; //$NON-NLS-1$
+ private static final String CLEAR_ID = "clear"; //$NON-NLS-1$
+ private static final String ZCUSTOM = "zcustom"; //$NON-NLS-1$
+
+ protected IClientRulesEngine mRulesEngine;
+
+ // Cache of attributes. Key is FQCN of a node mixed with its view hierarchy
+ // parent. Values are a custom map as needed by getContextMenu.
+ private Map<String, Map<String, Prop>> mAttributesMap =
+ new HashMap<String, Map<String, Prop>>();
+
+ @Override
+ public boolean onInitialize(@NonNull String fqcn, @NonNull IClientRulesEngine engine) {
+ mRulesEngine = engine;
+
+ // This base rule can handle any class so we don't need to filter on
+ // FQCN. Derived classes should do so if they can handle some
+ // subclasses.
+
+ // If onInitialize returns false, it means it can't handle the given
+ // FQCN and will be unloaded.
+
+ return true;
+ }
+
+ /**
+ * Returns the {@link IClientRulesEngine} associated with this {@link IViewRule}
+ *
+ * @return the {@link IClientRulesEngine} associated with this {@link IViewRule}
+ */
+ public IClientRulesEngine getRulesEngine() {
+ return mRulesEngine;
+ }
+
+ // === Context Menu ===
+
+ /**
+ * Generate custom actions for the context menu: <br/>
+ * - Explicit layout_width and layout_height attributes.
+ * - List of all other simple toggle attributes.
+ */
+ @Override
+ public void addContextMenuActions(@NonNull List<RuleAction> actions,
+ final @NonNull INode selectedNode) {
+ String width = null;
+ String currentWidth = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_WIDTH);
+
+ String fillParent = getFillParentValueName();
+ boolean canMatchParent = supportsMatchParent();
+ if (canMatchParent && VALUE_FILL_PARENT.equals(currentWidth)) {
+ currentWidth = VALUE_MATCH_PARENT;
+ } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(currentWidth)) {
+ currentWidth = VALUE_FILL_PARENT;
+ } else if (!VALUE_WRAP_CONTENT.equals(currentWidth) && !fillParent.equals(currentWidth)) {
+ width = currentWidth;
+ }
+
+ String height = null;
+ String currentHeight = selectedNode.getStringAttr(ANDROID_URI, ATTR_LAYOUT_HEIGHT);
+
+ if (canMatchParent && VALUE_FILL_PARENT.equals(currentHeight)) {
+ currentHeight = VALUE_MATCH_PARENT;
+ } else if (!canMatchParent && VALUE_MATCH_PARENT.equals(currentHeight)) {
+ currentHeight = VALUE_FILL_PARENT;
+ } else if (!VALUE_WRAP_CONTENT.equals(currentHeight)
+ && !fillParent.equals(currentHeight)) {
+ height = currentHeight;
+ }
+ final String newWidth = width;
+ final String newHeight = height;
+
+ final IMenuCallback onChange = new IMenuCallback() {
+ @Override
+ public void action(
+ final @NonNull RuleAction action,
+ final @NonNull List<? extends INode> selectedNodes,
+ final @Nullable String valueId, final @Nullable Boolean newValue) {
+ String fullActionId = action.getId();
+ boolean isProp = fullActionId.startsWith(PROP_PREFIX);
+ final String actionId = isProp ?
+ fullActionId.substring(PROP_PREFIX.length()) : fullActionId;
+
+ if (fullActionId.equals(ATTR_LAYOUT_WIDTH)) {
+ final String newAttrValue = getValue(valueId, newWidth);
+ if (newAttrValue != null) {
+ for (INode node : selectedNodes) {
+ node.editXml("Change Attribute " + ATTR_LAYOUT_WIDTH,
+ new PropertySettingNodeHandler(ANDROID_URI,
+ ATTR_LAYOUT_WIDTH, newAttrValue));
+ }
+ editedProperty(ATTR_LAYOUT_WIDTH);
+ }
+ return;
+ } else if (fullActionId.equals(ATTR_LAYOUT_HEIGHT)) {
+ // Ask the user
+ final String newAttrValue = getValue(valueId, newHeight);
+ if (newAttrValue != null) {
+ for (INode node : selectedNodes) {
+ node.editXml("Change Attribute " + ATTR_LAYOUT_HEIGHT,
+ new PropertySettingNodeHandler(ANDROID_URI,
+ ATTR_LAYOUT_HEIGHT, newAttrValue));
+ }
+ editedProperty(ATTR_LAYOUT_HEIGHT);
+ }
+ return;
+ } else if (fullActionId.equals(ATTR_ID)) {
+ // Ids must be set individually so open the id dialog for each
+ // selected node (though allow cancel to break the loop)
+ for (INode node : selectedNodes) {
+ if (!mRulesEngine.rename(node)) {
+ break;
+ }
+ }
+ editedProperty(ATTR_ID);
+ return;
+ } else if (isProp) {
+ INode firstNode = selectedNodes.get(0);
+ String key = getPropertyMapKey(selectedNode);
+ Map<String, Prop> props = mAttributesMap.get(key);
+ final Prop prop = (props != null) ? props.get(actionId) : null;
+
+ if (prop != null) {
+ editedProperty(actionId);
+
+ // For custom values (requiring an input dialog) input the
+ // value outside the undo-block.
+ // Input the value as a text, unless we know it's the "text" or
+ // "style" attributes (where we know we want to ask for specific
+ // resource types).
+ String uri = ANDROID_URI;
+ String v = null;
+ if (prop.isStringEdit()) {
+ boolean isStyle = actionId.equals(ATTR_STYLE);
+ boolean isText = actionId.equals(ATTR_TEXT);
+ boolean isHint = actionId.equals(ATTR_HINT);
+ if (isStyle || isText || isHint) {
+ String resourceTypeName = isStyle
+ ? ResourceType.STYLE.getName()
+ : ResourceType.STRING.getName();
+ String oldValue = selectedNodes.size() == 1
+ ? (isStyle ? firstNode.getStringAttr(ATTR_STYLE, actionId)
+ : firstNode.getStringAttr(ANDROID_URI, actionId))
+ : ""; //$NON-NLS-1$
+ oldValue = ensureValidString(oldValue);
+ v = mRulesEngine.displayResourceInput(resourceTypeName, oldValue);
+ if (isStyle) {
+ uri = null;
+ }
+ } else if (actionId.equals(ATTR_CLASS) && selectedNodes.size() >= 1 &&
+ VIEW_FRAGMENT.equals(selectedNodes.get(0).getFqcn())) {
+ v = mRulesEngine.displayFragmentSourceInput();
+ uri = null;
+ } else {
+ v = inputAttributeValue(firstNode, actionId);
+ }
+ }
+ final String customValue = v;
+
+ for (INode n : selectedNodes) {
+ if (prop.isToggle()) {
+ // case of toggle
+ String value = ""; //$NON-NLS-1$
+ if (valueId.equals(TRUE_ID)) {
+ value = newValue ? "true" : ""; //$NON-NLS-1$ //$NON-NLS-2$
+ } else if (valueId.equals(FALSE_ID)) {
+ value = newValue ? "false" : "";//$NON-NLS-1$ //$NON-NLS-2$
+ }
+ n.setAttribute(uri, actionId, value);
+ } else if (prop.isFlag()) {
+ // case of a flag
+ String values = ""; //$NON-NLS-1$
+ if (!valueId.equals(CLEAR_ID)) {
+ values = n.getStringAttr(ANDROID_URI, actionId);
+ Set<String> newValues = new HashSet<String>();
+ if (values != null) {
+ newValues.addAll(Arrays.asList(
+ values.split("\\|"))); //$NON-NLS-1$
+ }
+ if (newValue) {
+ newValues.add(valueId);
+ } else {
+ newValues.remove(valueId);
+ }
+
+ List<String> sorted = new ArrayList<String>(newValues);
+ Collections.sort(sorted);
+ values = join('|', sorted);
+
+ // Special case
+ if (valueId.equals("normal")) { //$NON-NLS-1$
+ // For textStyle for example, if you have "bold|italic"
+ // and you select the "normal" property, this should
+ // not behave in the normal flag way and "or" itself in;
+ // it should replace the other two.
+ // This also applies to imeOptions.
+ values = valueId;
+ }
+ }
+ n.setAttribute(uri, actionId, values);
+ } else if (prop.isEnum()) {
+ // case of an enum
+ String value = ""; //$NON-NLS-1$
+ if (!valueId.equals(CLEAR_ID)) {
+ value = newValue ? valueId : ""; //$NON-NLS-1$
+ }
+ n.setAttribute(uri, actionId, value);
+ } else {
+ assert prop.isStringEdit();
+ // We've already received the value outside the undo block
+ if (customValue != null) {
+ n.setAttribute(uri, actionId, customValue);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Input the custom value for the given attribute. This will use the Reference
+ * Chooser if it is a reference value, otherwise a plain text editor.
+ */
+ private String inputAttributeValue(final INode node, final String attribute) {
+ String oldValue = node.getStringAttr(ANDROID_URI, attribute);
+ oldValue = ensureValidString(oldValue);
+ IAttributeInfo attributeInfo = node.getAttributeInfo(ANDROID_URI, attribute);
+ if (attributeInfo != null
+ && attributeInfo.getFormats().contains(Format.REFERENCE)) {
+ return mRulesEngine.displayReferenceInput(oldValue);
+ } else {
+ // A single resource type? If so use a resource chooser initialized
+ // to this specific type
+ /* This does not work well, because the metadata is a bit misleading:
+ * for example a Button's "text" property and a Button's "onClick" property
+ * both claim to be of type [string], but @string/ is NOT valid for
+ * onClick..
+ if (attributeInfo != null && attributeInfo.getFormats().length == 1) {
+ // Resource chooser
+ Format format = attributeInfo.getFormats()[0];
+ return mRulesEngine.displayResourceInput(format.name(), oldValue);
+ }
+ */
+
+ // Fallback: just edit the raw XML string
+ String message = String.format("New %1$s Value:", attribute);
+ return mRulesEngine.displayInput(message, oldValue, null);
+ }
+ }
+
+ /**
+ * Returns the value (which will ask the user if the value is the special
+ * {@link #ZCUSTOM} marker
+ */
+ private String getValue(String valueId, String defaultValue) {
+ if (valueId.equals(ZCUSTOM)) {
+ if (defaultValue == null) {
+ defaultValue = "";
+ }
+ String value = mRulesEngine.displayInput(
+ "Set custom layout attribute value (example: 50dp)",
+ defaultValue, null);
+ if (value != null && value.trim().length() > 0) {
+ return value.trim();
+ } else {
+ return null;
+ }
+ }
+
+ return valueId;
+ }
+ };
+
+ IAttributeInfo textAttribute = selectedNode.getAttributeInfo(ANDROID_URI, ATTR_TEXT);
+ if (textAttribute != null) {
+ actions.add(RuleAction.createAction(PROP_PREFIX + ATTR_TEXT, "Edit Text...", onChange,
+ null, 10, true));
+ }
+
+ String editIdLabel = selectedNode.getStringAttr(ANDROID_URI, ATTR_ID) != null ?
+ "Edit ID..." : "Assign ID...";
+ actions.add(RuleAction.createAction(ATTR_ID, editIdLabel, onChange, null, 20, true));
+
+ addCommonPropertyActions(actions, selectedNode, onChange, 21);
+
+ // Create width choice submenu
+ actions.add(RuleAction.createSeparator(32));
+ List<Pair<String, String>> widthChoices = new ArrayList<Pair<String,String>>(4);
+ widthChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content"));
+ if (canMatchParent) {
+ widthChoices.add(Pair.of(VALUE_MATCH_PARENT, "Match Parent"));
+ } else {
+ widthChoices.add(Pair.of(VALUE_FILL_PARENT, "Fill Parent"));
+ }
+ if (width != null) {
+ widthChoices.add(Pair.of(width, width));
+ }
+ widthChoices.add(Pair.of(ZCUSTOM, "Other..."));
+ actions.add(RuleAction.createChoices(
+ ATTR_LAYOUT_WIDTH, "Layout Width",
+ onChange,
+ null /* iconUrls */,
+ currentWidth,
+ null, 35,
+ true, // supportsMultipleNodes
+ widthChoices));
+
+ // Create height choice submenu
+ List<Pair<String, String>> heightChoices = new ArrayList<Pair<String,String>>(4);
+ heightChoices.add(Pair.of(VALUE_WRAP_CONTENT, "Wrap Content"));
+ if (canMatchParent) {
+ heightChoices.add(Pair.of(VALUE_MATCH_PARENT, "Match Parent"));
+ } else {
+ heightChoices.add(Pair.of(VALUE_FILL_PARENT, "Fill Parent"));
+ }
+ if (height != null) {
+ heightChoices.add(Pair.of(height, height));
+ }
+ heightChoices.add(Pair.of(ZCUSTOM, "Other..."));
+ actions.add(RuleAction.createChoices(
+ ATTR_LAYOUT_HEIGHT, "Layout Height",
+ onChange,
+ null /* iconUrls */,
+ currentHeight,
+ null, 40,
+ true,
+ heightChoices));
+
+ actions.add(RuleAction.createSeparator(45));
+ RuleAction properties = RuleAction.createChoices("properties", "Other Properties", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 50,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode node) {
+ List<RuleAction> propertyActionTypes = new ArrayList<RuleAction>();
+ propertyActionTypes.add(RuleAction.createChoices(
+ "recent", "Recent", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 10,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addRecentPropertyActions(propertyActions, n, onChange);
+ return propertyActions;
+ }
+ }));
+
+ propertyActionTypes.add(RuleAction.createSeparator(20));
+
+ addInheritedProperties(propertyActionTypes, node, onChange, 30);
+
+ propertyActionTypes.add(RuleAction.createSeparator(50));
+ propertyActionTypes.add(RuleAction.createChoices(
+ "layoutparams", "Layout Parameters", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 60,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, null, true);
+ return propertyActions;
+ }
+ }));
+
+ propertyActionTypes.add(RuleAction.createSeparator(70));
+
+ propertyActionTypes.add(RuleAction.createChoices(
+ "allprops", "All By Name", //$NON-NLS-1$
+ onChange /*callback*/, null /*icon*/, 80,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, null, false);
+ return propertyActions;
+ }
+ }));
+
+ return propertyActionTypes;
+ }
+ });
+
+ actions.add(properties);
+ }
+
+ @Override
+ @Nullable
+ public String getDefaultActionId(@NonNull final INode selectedNode) {
+ IAttributeInfo textAttribute = selectedNode.getAttributeInfo(ANDROID_URI, ATTR_TEXT);
+ if (textAttribute != null) {
+ return PROP_PREFIX + ATTR_TEXT;
+ }
+
+ return null;
+ }
+
+ private static String getPropertyMapKey(INode node) {
+ // Compute the key for mAttributesMap. This depends on the type of this
+ // node and its parent in the view hierarchy.
+ StringBuilder sb = new StringBuilder();
+ sb.append(node.getFqcn());
+ sb.append('_');
+ INode parent = node.getParent();
+ if (parent != null) {
+ sb.append(parent.getFqcn());
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Adds menu items for the inherited attributes, one pull-right menu for each super class
+ * that defines attributes.
+ *
+ * @param propertyActionTypes the actions list to add into
+ * @param node the node to apply the attributes to
+ * @param onChange the callback to use for setting attributes
+ * @param sortPriority the initial sort attribute for the first menu item
+ */
+ private void addInheritedProperties(List<RuleAction> propertyActionTypes, INode node,
+ final IMenuCallback onChange, int sortPriority) {
+ List<String> attributeSources = node.getAttributeSources();
+ for (final String definedBy : attributeSources) {
+ String sourceClass = definedBy;
+
+ // Strip package prefixes when necessary
+ int index = sourceClass.length();
+ if (sourceClass.endsWith(DOT_LAYOUT_PARAMS)) {
+ index = sourceClass.length() - DOT_LAYOUT_PARAMS.length() - 1;
+ }
+ int lastDot = sourceClass.lastIndexOf('.', index);
+ if (lastDot != -1) {
+ sourceClass = sourceClass.substring(lastDot + 1);
+ }
+
+ String label;
+ if (definedBy.equals(node.getFqcn())) {
+ label = String.format("Defined by %1$s", sourceClass);
+ } else {
+ label = String.format("Inherited from %1$s", sourceClass);
+ }
+
+ propertyActionTypes.add(RuleAction.createChoices("def_" + definedBy,
+ label,
+ onChange /*callback*/, null /*icon*/, sortPriority++,
+ true /*supportsMultipleNodes*/, new ActionProvider() {
+ @Override
+ public @NonNull List<RuleAction> getNestedActions(@NonNull INode n) {
+ List<RuleAction> propertyActions = new ArrayList<RuleAction>();
+ addPropertyActions(propertyActions, n, onChange, definedBy, false);
+ return propertyActions;
+ }
+ }));
+ }
+ }
+
+ /**
+ * Creates a list of properties that are commonly edited for views of the
+ * selected node's type
+ */
+ private void addCommonPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange, int sortPriority) {
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+ IViewMetadata metadata = mRulesEngine.getMetadata(selectedNode.getFqcn());
+ if (metadata != null) {
+ List<String> attributes = metadata.getTopAttributes();
+ if (attributes.size() > 0) {
+ for (String attribute : attributes) {
+ // Text and ID are handled manually in the menu construction code because
+ // we want to place them consistently and customize the action label
+ if (ATTR_TEXT.equals(attribute) || ATTR_ID.equals(attribute)) {
+ continue;
+ }
+
+ Prop property = properties.get(attribute);
+ if (property != null) {
+ String title = property.getTitle();
+ if (title.endsWith("...")) {
+ title = String.format("Edit %1$s", property.getTitle());
+ }
+ actions.add(createPropertyAction(property, attribute, title,
+ selectedNode, onChange, sortPriority));
+ sortPriority++;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Record that the given property was just edited; adds it to the front of
+ * the recently edited property list
+ *
+ * @param property the name of the property
+ */
+ static void editedProperty(String property) {
+ if (sRecent.contains(property)) {
+ sRecent.remove(property);
+ } else if (sRecent.size() > MAX_RECENT_COUNT) {
+ sRecent.remove(sRecent.size() - 1);
+ }
+ sRecent.add(0, property);
+ }
+
+ /**
+ * Creates a list of recently modified properties that apply to the given selected node
+ */
+ private void addRecentPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange) {
+ int sortPriority = 10;
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+ for (String attribute : sRecent) {
+ Prop property = properties.get(attribute);
+ if (property != null) {
+ actions.add(createPropertyAction(property, attribute, property.getTitle(),
+ selectedNode, onChange, sortPriority));
+ sortPriority += 10;
+ }
+ }
+ }
+
+ /**
+ * Creates a list of nested actions representing the property-setting
+ * actions for the given selected node
+ */
+ private void addPropertyActions(List<RuleAction> actions, INode selectedNode,
+ IMenuCallback onChange, String definedBy, boolean layoutParamsOnly) {
+
+ Map<String, Prop> properties = getPropertyMetadata(selectedNode);
+
+ int sortPriority = 10;
+ for (Map.Entry<String, Prop> entry : properties.entrySet()) {
+ String id = entry.getKey();
+ Prop property = entry.getValue();
+ if (layoutParamsOnly) {
+ // If we have definedBy information, that is most accurate; all layout
+ // params will be defined by a class whose name ends with
+ // .LayoutParams:
+ if (definedBy != null) {
+ if (!definedBy.endsWith(DOT_LAYOUT_PARAMS)) {
+ continue;
+ }
+ } else if (!id.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) {
+ continue;
+ }
+ }
+ if (definedBy != null && !definedBy.equals(property.getDefinedBy())) {
+ continue;
+ }
+ actions.add(createPropertyAction(property, id, property.getTitle(),
+ selectedNode, onChange, sortPriority));
+ sortPriority += 10;
+ }
+
+ // The properties are coming out of map key order which isn't right, so sort
+ // alphabetically instead
+ Collections.sort(actions, new Comparator<RuleAction>() {
+ @Override
+ public int compare(RuleAction action1, RuleAction action2) {
+ return action1.getTitle().compareTo(action2.getTitle());
+ }
+ });
+ }
+
+ private RuleAction createPropertyAction(Prop p, String id, String title, INode selectedNode,
+ IMenuCallback onChange, int sortPriority) {
+ if (p.isToggle()) {
+ // Toggles are handled as a multiple-choice between true, false
+ // and nothing (clear)
+ String value = selectedNode.getStringAttr(ANDROID_URI, id);
+ if (value != null) {
+ value = value.toLowerCase(Locale.US);
+ }
+ if (VALUE_TRUE.equals(value)) {
+ value = TRUE_ID;
+ } else if (VALUE_FALSE.equals(value)) {
+ value = FALSE_ID;
+ } else {
+ value = CLEAR_ID;
+ }
+ return RuleAction.createChoices(PROP_PREFIX + id, title,
+ onChange, BOOLEAN_CHOICE_PROVIDER,
+ value,
+ null, sortPriority,
+ true);
+ } else if (p.getChoices() != null) {
+ // Enum or flags. Their possible values are the multiple-choice
+ // items, with an extra "clear" option to remove everything.
+ String current = selectedNode.getStringAttr(ANDROID_URI, id);
+ if (current == null || current.length() == 0) {
+ current = CLEAR_ID;
+ }
+ return RuleAction.createChoices(PROP_PREFIX + id, title,
+ onChange, new EnumPropertyChoiceProvider(p),
+ current,
+ null, sortPriority,
+ true);
+ } else {
+ return RuleAction.createAction(
+ PROP_PREFIX + id,
+ title,
+ onChange,
+ null, sortPriority,
+ true);
+ }
+ }
+
+ private Map<String, Prop> getPropertyMetadata(final INode selectedNode) {
+ String key = getPropertyMapKey(selectedNode);
+ Map<String, Prop> props = mAttributesMap.get(key);
+ if (props == null) {
+ // Prepare the property map
+ props = new HashMap<String, Prop>();
+ for (IAttributeInfo attrInfo : selectedNode.getDeclaredAttributes()) {
+ String id = attrInfo != null ? attrInfo.getName() : null;
+ if (id == null || id.equals(ATTR_LAYOUT_WIDTH) || id.equals(ATTR_LAYOUT_HEIGHT)) {
+ // Layout width/height are already handled at the root level
+ continue;
+ }
+ if (attrInfo == null) {
+ continue;
+ }
+ EnumSet<Format> formats = attrInfo.getFormats();
+
+ String title = getAttributeDisplayName(id);
+
+ String definedBy = attrInfo != null ? attrInfo.getDefinedBy() : null;
+ if (formats.contains(IAttributeInfo.Format.BOOLEAN)) {
+ props.put(id, new Prop(title, true, definedBy));
+ } else if (formats.contains(IAttributeInfo.Format.ENUM)) {
+ // Convert each enum into a map id=>title
+ Map<String, String> values = new HashMap<String, String>();
+ if (attrInfo != null) {
+ for (String e : attrInfo.getEnumValues()) {
+ values.put(e, getAttributeDisplayName(e));
+ }
+ }
+
+ props.put(id, new Prop(title, false, false, values, definedBy));
+ } else if (formats.contains(IAttributeInfo.Format.FLAG)) {
+ // Convert each flag into a map id=>title
+ Map<String, String> values = new HashMap<String, String>();
+ if (attrInfo != null) {
+ for (String e : attrInfo.getFlagValues()) {
+ values.put(e, getAttributeDisplayName(e));
+ }
+ }
+
+ props.put(id, new Prop(title, false, true, values, definedBy));
+ } else {
+ props.put(id, new Prop(title + "...", false, definedBy));
+ }
+ }
+ mAttributesMap.put(key, props);
+ }
+ return props;
+ }
+
+ /**
+ * A {@link ChoiceProvder} which provides alternatives suitable for choosing
+ * values for a boolean property: true, false, or "default".
+ */
+ private static ChoiceProvider BOOLEAN_CHOICE_PROVIDER = new ChoiceProvider() {
+ @Override
+ public void addChoices(@NonNull List<String> titles, @NonNull List<URL> iconUrls,
+ @NonNull List<String> ids) {
+ titles.add("True");
+ ids.add(TRUE_ID);
+
+ titles.add("False");
+ ids.add(FALSE_ID);
+
+ titles.add(RuleAction.SEPARATOR);
+ ids.add(RuleAction.SEPARATOR);
+
+ titles.add("Default");
+ ids.add(CLEAR_ID);
+ }
+ };
+
+ /**
+ * A {@link ChoiceProvider} which provides the various available
+ * attribute values available for a given {@link Prop} property descriptor.
+ */
+ private static class EnumPropertyChoiceProvider implements ChoiceProvider {
+ private Prop mProperty;
+
+ public EnumPropertyChoiceProvider(Prop property) {
+ super();
+ mProperty = property;
+ }
+
+ @Override
+ public void addChoices(@NonNull List<String> titles, @NonNull List<URL> iconUrls,
+ @NonNull List<String> ids) {
+ for (Entry<String, String> entry : mProperty.getChoices().entrySet()) {
+ ids.add(entry.getKey());
+ titles.add(entry.getValue());
+ }
+
+ titles.add(RuleAction.SEPARATOR);
+ ids.add(RuleAction.SEPARATOR);
+
+ titles.add("Default");
+ ids.add(CLEAR_ID);
+ }
+ }
+
+ /**
+ * Returns true if the given node is "filled" (e.g. has layout width set to match
+ * parent or fill parent
+ */
+ protected final boolean isFilled(INode node, String attribute) {
+ String value = node.getStringAttr(ANDROID_URI, attribute);
+ return VALUE_MATCH_PARENT.equals(value) || VALUE_FILL_PARENT.equals(value);
+ }
+
+ /**
+ * Returns fill_parent or match_parent, depending on whether the minimum supported
+ * platform supports match_parent or not
+ *
+ * @return match_parent or fill_parent depending on which is supported by the project
+ */
+ protected final String getFillParentValueName() {
+ return supportsMatchParent() ? VALUE_MATCH_PARENT : VALUE_FILL_PARENT;
+ }
+
+ /**
+ * Returns true if the project supports match_parent instead of just fill_parent
+ *
+ * @return true if the project supports match_parent instead of just fill_parent
+ */
+ protected final boolean supportsMatchParent() {
+ // fill_parent was renamed match_parent in API level 8
+ return mRulesEngine.getMinApiLevel() >= 8;
+ }
+
+ /** Join strings into a single string with the given delimiter */
+ static String join(char delimiter, Collection<String> strings) {
+ StringBuilder sb = new StringBuilder(100);
+ for (String s : strings) {
+ if (sb.length() > 0) {
+ sb.append(delimiter);
+ }
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+
+ static Map<String, String> concatenate(Map<String, String> pre, Map<String, String> post) {
+ Map<String, String> result = new HashMap<String, String>(pre.size() + post.size());
+ result.putAll(pre);
+ result.putAll(post);
+ return result;
+ }
+
+ // Quick utility for building up maps declaratively to minimize the diffs
+ static Map<String, String> mapify(String... values) {
+ Map<String, String> map = new HashMap<String, String>(values.length / 2);
+ for (int i = 0; i < values.length; i += 2) {
+ String key = values[i];
+ if (key == null) {
+ continue;
+ }
+ String value = values[i + 1];
+ map.put(key, value);
+ }
+
+ return map;
+ }
+
+ /**
+ * Produces a display name for an attribute, usually capitalizing the attribute name
+ * and splitting up underscores into new words
+ *
+ * @param name the attribute name to convert
+ * @return a display name for the attribute name
+ */
+ public static String getAttributeDisplayName(String name) {
+ if (name != null && name.length() > 0) {
+ StringBuilder sb = new StringBuilder();
+ boolean capitalizeNext = true;
+ for (int i = 0, n = name.length(); i < n; i++) {
+ char c = name.charAt(i);
+ if (capitalizeNext) {
+ c = Character.toUpperCase(c);
+ }
+ capitalizeNext = false;
+ if (c == '_') {
+ c = ' ';
+ capitalizeNext = true;
+ }
+ sb.append(c);
+ }
+
+ return sb.toString();
+ }
+
+ return name;
+ }
+
+
+ // ==== Paste support ====
+
+ /**
+ * Most views can't accept children so there's nothing to paste on them. In
+ * this case, defer the call to the parent layout and use the target node as
+ * an indication of where to paste.
+ */
+ @Override
+ public void onPaste(@NonNull INode targetNode, @Nullable Object targetView,
+ @NonNull IDragElement[] elements) {
+ //
+ INode parent = targetNode.getParent();
+ if (parent != null) {
+ String parentFqcn = parent.getFqcn();
+ IViewRule parentRule = mRulesEngine.loadRule(parentFqcn);
+
+ if (parentRule instanceof BaseLayoutRule) {
+ ((BaseLayoutRule) parentRule).onPasteBeforeChild(parent, targetView, targetNode,
+ elements);
+ }
+ }
+ }
+
+ /**
+ * Support class for the context menu code. Stores state about properties in
+ * the context menu.
+ */
+ private static class Prop {
+ private final boolean mToggle;
+ private final boolean mFlag;
+ private final String mTitle;
+ private final Map<String, String> mChoices;
+ private String mDefinedBy;
+
+ public Prop(String title, boolean isToggle, boolean isFlag, Map<String, String> choices,
+ String definedBy) {
+ mTitle = title;
+ mToggle = isToggle;
+ mFlag = isFlag;
+ mChoices = choices;
+ mDefinedBy = definedBy;
+ }
+
+ public String getDefinedBy() {
+ return mDefinedBy;
+ }
+
+ public Prop(String title, boolean isToggle, String definedBy) {
+ this(title, isToggle, false, null, definedBy);
+ }
+
+ private boolean isToggle() {
+ return mToggle;
+ }
+
+ private boolean isFlag() {
+ return mFlag && mChoices != null;
+ }
+
+ private boolean isEnum() {
+ return !mFlag && mChoices != null;
+ }
+
+ private String getTitle() {
+ return mTitle;
+ }
+
+ private Map<String, String> getChoices() {
+ return mChoices;
+ }
+
+ private boolean isStringEdit() {
+ return mChoices == null && !mToggle;
+ }
+ }
+
+ /**
+ * Returns a source attribute value which points to a sample image. This is typically
+ * used to provide an initial image shown on ImageButtons, etc. There is no guarantee
+ * that the source pointed to by this method actually exists.
+ *
+ * @return a source attribute to use for sample images, never null
+ */
+ protected final String getSampleImageSrc() {
+ // Builtin graphics available since v1:
+ return "@android:drawable/btn_star"; //$NON-NLS-1$
+ }
+
+ /**
+ * Strips the {@code @+id} or {@code @id} prefix off of the given id
+ *
+ * @param id attribute to be stripped
+ * @return the id name without the {@code @+id} or {@code @id} prefix
+ */
+ @NonNull
+ public static String stripIdPrefix(@Nullable String id) {
+ if (id == null) {
+ return ""; //$NON-NLS-1$
+ } else if (id.startsWith(NEW_ID_PREFIX)) {
+ return id.substring(NEW_ID_PREFIX.length());
+ } else if (id.startsWith(ID_PREFIX)) {
+ return id.substring(ID_PREFIX.length());
+ }
+ return id;
+ }
+
+ private static String ensureValidString(String value) {
+ if (value == null) {
+ value = ""; //$NON-NLS-1$
+ }
+ return value;
+ }
+ }