/* * 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; } }