blob: eb51d3f860479b64c3ad160cae575cb8bc27cd4f [file] [log] [blame]
/*
* 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> "&lt;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;
}
}