diff options
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.java | 996 |
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; + } + } |