/* * Copyright (C) 2007 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.eclipse.adt.internal.editors.uimodel; import static com.android.SdkConstants.ANDROID_PKG; import static com.android.SdkConstants.ANDROID_PREFIX; import static com.android.SdkConstants.ANDROID_THEME_PREFIX; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.ATTR_LAYOUT; import static com.android.SdkConstants.ATTR_STYLE; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.PREFIX_THEME_REF; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IAttributeInfo.Format; import com.android.ide.common.resources.ResourceItem; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils; import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor; import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; import com.android.ide.eclipse.adt.internal.sdk.ProjectState; import com.android.ide.eclipse.adt.internal.sdk.Sdk; import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog; import com.android.ide.eclipse.adt.internal.ui.ResourceChooser; import com.android.resources.ResourceType; import org.eclipse.core.resources.IProject; import org.eclipse.jface.window.Window; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.forms.IManagedForm; import org.eclipse.ui.forms.widgets.FormToolkit; import org.eclipse.ui.forms.widgets.TableWrapData; 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.HashSet; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Represents an XML attribute for a resource that can be modified using a simple text field or * a dialog to choose an existing resource. * <p/> * It can be configured to represent any kind of resource, by providing the desired * {@link ResourceType} in the constructor. * <p/> * See {@link UiTextAttributeNode} for more information. */ public class UiResourceAttributeNode extends UiTextAttributeNode { private ResourceType mType; /** * Creates a new {@linkplain UiResourceAttributeNode} * * @param type the associated resource type * @param attributeDescriptor the attribute descriptor for this attribute * @param uiParent the parent ui node, if any */ public UiResourceAttributeNode(ResourceType type, AttributeDescriptor attributeDescriptor, UiElementNode uiParent) { super(attributeDescriptor, uiParent); mType = type; } /* (non-java doc) * Creates a label widget and an associated text field. * <p/> * As most other parts of the android manifest editor, this assumes the * parent uses a table layout with 2 columns. */ @Override public void createUiControl(final Composite parent, IManagedForm managedForm) { setManagedForm(managedForm); FormToolkit toolkit = managedForm.getToolkit(); TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor(); Label label = toolkit.createLabel(parent, desc.getUiName()); label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE)); SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip())); Composite composite = toolkit.createComposite(parent); composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE)); GridLayout gl = new GridLayout(2, false); gl.marginHeight = gl.marginWidth = 0; composite.setLayout(gl); // Fixes missing text borders under GTK... also requires adding a 1-pixel margin // for the text field below toolkit.paintBordersFor(composite); final Text text = toolkit.createText(composite, getCurrentValue()); GridData gd = new GridData(GridData.FILL_HORIZONTAL); gd.horizontalIndent = 1; // Needed by the fixed composite borders under GTK text.setLayoutData(gd); Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH); setTextWidget(text); // TODO Add a validator using onAddModifyListener browseButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { String result = showDialog(parent.getShell(), text.getText().trim()); if (result != null) { text.setText(result); } } }); } /** * Shows a dialog letting the user choose a set of enum, and returns a * string containing the result. * * @param shell the parent shell * @param currentValue an initial value, if any * @return the chosen string, or null */ @Nullable public String showDialog(@NonNull Shell shell, @Nullable String currentValue) { // we need to get the project of the file being edited. UiElementNode uiNode = getUiParent(); AndroidXmlEditor editor = uiNode.getEditor(); IProject project = editor.getProject(); if (project != null) { // get the resource repository for this project and the system resources. ResourceRepository projectRepository = ResourceManager.getInstance().getProjectResources(project); if (mType != null) { // get the Target Data to get the system resources AndroidTargetData data = editor.getTargetData(); ResourceChooser dlg = ResourceChooser.create(project, mType, data, shell) .setCurrentResource(currentValue); if (dlg.open() == Window.OK) { return dlg.getCurrentResource(); } } else { ReferenceChooserDialog dlg = new ReferenceChooserDialog( project, projectRepository, shell); dlg.setCurrentResource(currentValue); if (dlg.open() == Window.OK) { return dlg.getCurrentResource(); } } } return null; } /** * Gets all the values one could use to auto-complete a "resource" value in an XML * content assist. * <p/> * Typically the user is editing the value of an attribute in a resource XML, e.g. * <pre> "<Button android:test="@string/my_[caret]_string..." </pre> * <p/> * * "prefix" is the value that the user has typed so far (or more exactly whatever is on the * left side of the insertion point). In the example above it would be "@style/my_". * <p/> * * To avoid a huge long list of values, the completion works on two levels: * <ul> * <li> If a resource type as been typed so far (e.g. "@style/"), then limit the values to * the possible completions that match this type. * <li> If no resource type as been typed so far, then return the various types that could be * completed. So if the project has only strings and layouts resources, for example, * the returned list will only include "@string/" and "@layout/". * </ul> * * Finally if anywhere in the string we find the special token "android:", we use the * current framework system resources rather than the project resources. * This works for both "@android:style/foo" and "@style/android:foo" conventions even though * the reconstructed name will always be of the former form. * * Note that "android:" here is a keyword specific to Android resources and should not be * mixed with an XML namespace for an XML attribute name. */ @Override public String[] getPossibleValues(String prefix) { return computeResourceStringMatches(getUiParent().getEditor(), getDescriptor(), prefix); } /** * Computes the set of resource string matches for a given resource prefix in a given editor * * @param editor the editor context * @param descriptor the attribute descriptor, if any * @param prefix the prefix, if any * @return an array of resource string matches */ @Nullable public static String[] computeResourceStringMatches( @NonNull AndroidXmlEditor editor, @Nullable AttributeDescriptor descriptor, @Nullable String prefix) { if (prefix == null || !prefix.regionMatches(1, ANDROID_PKG, 0, ANDROID_PKG.length())) { IProject project = editor.getProject(); if (project != null) { // get the resource repository for this project and the system resources. ResourceManager resourceManager = ResourceManager.getInstance(); ResourceRepository repository = resourceManager.getProjectResources(project); List<IProject> libraries = null; ProjectState projectState = Sdk.getProjectState(project); if (projectState != null) { libraries = projectState.getFullLibraryProjects(); } String[] projectMatches = computeResourceStringMatches(descriptor, prefix, repository, false); if (libraries == null || libraries.isEmpty()) { return projectMatches; } // Also compute matches for each of the libraries, and combine them Set<String> matches = new HashSet<String>(200); for (String s : projectMatches) { matches.add(s); } for (IProject library : libraries) { repository = resourceManager.getProjectResources(library); projectMatches = computeResourceStringMatches(descriptor, prefix, repository, false); for (String s : projectMatches) { matches.add(s); } } String[] sorted = matches.toArray(new String[matches.size()]); Arrays.sort(sorted); return sorted; } } else { // If there's a prefix with "android:" in it, use the system resources // Non-public framework resources are filtered out later. AndroidTargetData data = editor.getTargetData(); if (data != null) { ResourceRepository repository = data.getFrameworkResources(); return computeResourceStringMatches(descriptor, prefix, repository, true); } } return null; } /** * Computes the set of resource string matches for a given prefix and a * given resource repository * * @param attributeDescriptor the attribute descriptor, if any * @param prefix the prefix, if any * @param repository the repository to seaerch in * @param isSystem if true, the repository contains framework repository, * otherwise it contains project repositories * @return an array of resource string matches */ @NonNull public static String[] computeResourceStringMatches( @Nullable AttributeDescriptor attributeDescriptor, @Nullable String prefix, @NonNull ResourceRepository repository, boolean isSystem) { // Get list of potential resource types, either specific to this project // or the generic list. Collection<ResourceType> resTypes = (repository != null) ? repository.getAvailableResourceTypes() : EnumSet.allOf(ResourceType.class); // Get the type name from the prefix, if any. It's any word before the / if there's one String typeName = null; if (prefix != null) { Matcher m = Pattern.compile(".*?([a-z]+)/.*").matcher(prefix); //$NON-NLS-1$ if (m.matches()) { typeName = m.group(1); } } // Now collect results List<String> results = new ArrayList<String>(); if (typeName == null) { // This prefix does not have a / in it, so the resource string is either empty // or does not have the resource type in it. Simply offer the list of potential // resource types. if (prefix != null && prefix.startsWith(PREFIX_THEME_REF)) { results.add(ANDROID_THEME_PREFIX + ResourceType.ATTR.getName() + '/'); if (resTypes.contains(ResourceType.ATTR) || resTypes.contains(ResourceType.STYLE)) { results.add(PREFIX_THEME_REF + ResourceType.ATTR.getName() + '/'); if (prefix != null && prefix.startsWith(ANDROID_THEME_PREFIX)) { // including attr isn't required for (ResourceItem item : repository.getResourceItemsOfType( ResourceType.ATTR)) { results.add(ANDROID_THEME_PREFIX + item.getName()); } } } return results.toArray(new String[results.size()]); } for (ResourceType resType : resTypes) { if (isSystem) { results.add(ANDROID_PREFIX + resType.getName() + '/'); } else { results.add('@' + resType.getName() + '/'); } if (resType == ResourceType.ID) { // Also offer the + version to create an id from scratch results.add("@+" + resType.getName() + '/'); //$NON-NLS-1$ } } // Also add in @android: prefix to completion such that if user has typed // "@an" we offer to complete it. if (prefix == null || ANDROID_PKG.regionMatches(0, prefix, 1, prefix.length() - 1)) { results.add(ANDROID_PREFIX); } } else if (repository != null) { // We have a style name and a repository. Find all resources that match this // type and recreate suggestions out of them. String initial = prefix != null && prefix.startsWith(PREFIX_THEME_REF) ? PREFIX_THEME_REF : PREFIX_RESOURCE_REF; ResourceType resType = ResourceType.getEnum(typeName); if (resType != null) { StringBuilder sb = new StringBuilder(); sb.append(initial); if (prefix != null && prefix.indexOf('+') >= 0) { sb.append('+'); } if (isSystem) { sb.append(ANDROID_PKG).append(':'); } sb.append(typeName).append('/'); String base = sb.toString(); for (ResourceItem item : repository.getResourceItemsOfType(resType)) { results.add(base + item.getName()); } if (!isSystem && resType == ResourceType.ATTR) { for (ResourceItem item : repository.getResourceItemsOfType( ResourceType.STYLE)) { results.add(base + item.getName()); } } } } if (attributeDescriptor != null) { sortAttributeChoices(attributeDescriptor, results); } else { Collections.sort(results); } return results.toArray(new String[results.size()]); } /** * Attempts to sort the attribute values to bubble up the most likely choices to * the top. * <p> * For example, if you are editing a style attribute, it's likely that among the * resource values you would rather see @style or @android than @string. * @param descriptor the descriptor that the resource values are being completed for, * used to prioritize some of the resource types * @param choices the set of string resource values */ public static void sortAttributeChoices(AttributeDescriptor descriptor, List<String> choices) { final IAttributeInfo attributeInfo = descriptor.getAttributeInfo(); Collections.sort(choices, new Comparator<String>() { @Override public int compare(String s1, String s2) { int compare = score(attributeInfo, s1) - score(attributeInfo, s2); if (compare == 0) { // Sort alphabetically as a fallback compare = s1.compareToIgnoreCase(s2); } return compare; } }); } /** Compute a suitable sorting score for the given */ private static final int score(IAttributeInfo attributeInfo, String value) { if (value.equals(ANDROID_PREFIX)) { return -1; } for (Format format : attributeInfo.getFormats()) { String type = null; switch (format) { case BOOLEAN: type = "bool"; //$NON-NLS-1$ break; case COLOR: type = "color"; //$NON-NLS-1$ break; case DIMENSION: type = "dimen"; //$NON-NLS-1$ break; case INTEGER: type = "integer"; //$NON-NLS-1$ break; case STRING: type = "string"; //$NON-NLS-1$ break; // default: REFERENCE, FLAG, ENUM, etc - don't have type info about individual // elements to help make a decision } if (type != null) { if (value.startsWith(PREFIX_RESOURCE_REF)) { if (value.startsWith(PREFIX_RESOURCE_REF + type + '/')) { return -2; } if (value.startsWith(ANDROID_PREFIX + type + '/')) { return -2; } } if (value.startsWith(PREFIX_THEME_REF)) { if (value.startsWith(PREFIX_THEME_REF + type + '/')) { return -2; } if (value.startsWith(ANDROID_THEME_PREFIX + type + '/')) { return -2; } } } } // Handle a few more cases not covered by the Format metadata check String type = null; String attribute = attributeInfo.getName(); if (attribute.equals(ATTR_ID)) { type = "id"; //$NON-NLS-1$ } else if (attribute.equals(ATTR_STYLE)) { type = "style"; //$NON-NLS-1$ } else if (attribute.equals(ATTR_LAYOUT)) { type = "layout"; //$NON-NLS-1$ } else if (attribute.equals("drawable")) { //$NON-NLS-1$ type = "drawable"; //$NON-NLS-1$ } else if (attribute.equals("entries")) { //$NON-NLS-1$ // Spinner type = "array"; //$NON-NLS-1$ } if (type != null) { if (value.startsWith(PREFIX_RESOURCE_REF)) { if (value.startsWith(PREFIX_RESOURCE_REF + type + '/')) { return -2; } if (value.startsWith(ANDROID_PREFIX + type + '/')) { return -2; } } if (value.startsWith(PREFIX_THEME_REF)) { if (value.startsWith(PREFIX_THEME_REF + type + '/')) { return -2; } if (value.startsWith(ANDROID_THEME_PREFIX + type + '/')) { return -2; } } } return 0; } }