diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java new file mode 100644 index 000000000..eb51d3f86 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/uimodel/UiResourceAttributeNode.java @@ -0,0 +1,523 @@ +/* + * 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; + } +} |