blob: 83ce9ef8f640b8ef6c4eefe86d9ee006d81a5887 [file] [log] [blame]
/*
* 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;
}
}