Home | History | Annotate | Download | only in uimodel
      1 /*
      2  * Copyright (C) 2007 The Android Open Source Project
      3  *
      4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.eclipse.org/org/documents/epl-v10.php
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.adt.internal.editors.uimodel;
     18 
     19 import static com.android.ide.common.layout.LayoutConstants.ATTR_ID;
     20 import static com.android.ide.common.layout.LayoutConstants.ATTR_STYLE;
     21 import static com.android.ide.common.resources.ResourceResolver.PREFIX_ANDROID_RESOURCE_REF;
     22 import static com.android.ide.eclipse.adt.AdtConstants.ANDROID_PKG;
     23 
     24 import com.android.ide.common.api.IAttributeInfo;
     25 import com.android.ide.common.api.IAttributeInfo.Format;
     26 import com.android.ide.common.resources.ResourceItem;
     27 import com.android.ide.common.resources.ResourceRepository;
     28 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     29 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     30 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
     31 import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
     32 import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.LayoutDescriptors;
     33 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
     34 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
     35 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     36 import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog;
     37 import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
     38 import com.android.resources.ResourceType;
     39 
     40 import org.eclipse.core.resources.IProject;
     41 import org.eclipse.jface.window.Window;
     42 import org.eclipse.swt.SWT;
     43 import org.eclipse.swt.events.SelectionAdapter;
     44 import org.eclipse.swt.events.SelectionEvent;
     45 import org.eclipse.swt.layout.GridData;
     46 import org.eclipse.swt.layout.GridLayout;
     47 import org.eclipse.swt.widgets.Button;
     48 import org.eclipse.swt.widgets.Composite;
     49 import org.eclipse.swt.widgets.Label;
     50 import org.eclipse.swt.widgets.Shell;
     51 import org.eclipse.swt.widgets.Text;
     52 import org.eclipse.ui.forms.IManagedForm;
     53 import org.eclipse.ui.forms.widgets.FormToolkit;
     54 import org.eclipse.ui.forms.widgets.TableWrapData;
     55 
     56 import java.util.ArrayList;
     57 import java.util.Collection;
     58 import java.util.Collections;
     59 import java.util.Comparator;
     60 import java.util.EnumSet;
     61 import java.util.List;
     62 import java.util.regex.Matcher;
     63 import java.util.regex.Pattern;
     64 
     65 /**
     66  * Represents an XML attribute for a resource that can be modified using a simple text field or
     67  * a dialog to choose an existing resource.
     68  * <p/>
     69  * It can be configured to represent any kind of resource, by providing the desired
     70  * {@link ResourceType} in the constructor.
     71  * <p/>
     72  * See {@link UiTextAttributeNode} for more information.
     73  */
     74 public class UiResourceAttributeNode extends UiTextAttributeNode {
     75     private ResourceType mType;
     76 
     77     public UiResourceAttributeNode(ResourceType type,
     78             AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
     79         super(attributeDescriptor, uiParent);
     80 
     81         mType = type;
     82     }
     83 
     84     /* (non-java doc)
     85      * Creates a label widget and an associated text field.
     86      * <p/>
     87      * As most other parts of the android manifest editor, this assumes the
     88      * parent uses a table layout with 2 columns.
     89      */
     90     @Override
     91     public void createUiControl(final Composite parent, IManagedForm managedForm) {
     92         setManagedForm(managedForm);
     93         FormToolkit toolkit = managedForm.getToolkit();
     94         TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
     95 
     96         Label label = toolkit.createLabel(parent, desc.getUiName());
     97         label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
     98         SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
     99 
    100         Composite composite = toolkit.createComposite(parent);
    101         composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
    102         GridLayout gl = new GridLayout(2, false);
    103         gl.marginHeight = gl.marginWidth = 0;
    104         composite.setLayout(gl);
    105         // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
    106         // for the text field below
    107         toolkit.paintBordersFor(composite);
    108 
    109         final Text text = toolkit.createText(composite, getCurrentValue());
    110         GridData gd = new GridData(GridData.FILL_HORIZONTAL);
    111         gd.horizontalIndent = 1;  // Needed by the fixed composite borders under GTK
    112         text.setLayoutData(gd);
    113         Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
    114 
    115         setTextWidget(text);
    116 
    117         // TODO Add a validator using onAddModifyListener
    118 
    119         browseButton.addSelectionListener(new SelectionAdapter() {
    120             @Override
    121             public void widgetSelected(SelectionEvent e) {
    122                 String result = showDialog(parent.getShell(), text.getText().trim());
    123                 if (result != null) {
    124                     text.setText(result);
    125                 }
    126             }
    127         });
    128     }
    129 
    130     /**
    131      * Shows a dialog letting the user choose a set of enum, and returns a string
    132      * containing the result.
    133      */
    134     public String showDialog(Shell shell, String currentValue) {
    135         // we need to get the project of the file being edited.
    136         UiElementNode uiNode = getUiParent();
    137         AndroidXmlEditor editor = uiNode.getEditor();
    138         IProject project = editor.getProject();
    139         if (project != null) {
    140             // get the resource repository for this project and the system resources.
    141             ResourceRepository projectRepository =
    142                 ResourceManager.getInstance().getProjectResources(project);
    143 
    144             if (mType != null) {
    145                 // get the Target Data to get the system resources
    146                 AndroidTargetData data = editor.getTargetData();
    147                 ResourceRepository frameworkRepository = data.getFrameworkResources();
    148 
    149                 // open a resource chooser dialog for specified resource type.
    150                 ResourceChooser dlg = new ResourceChooser(project,
    151                         mType,
    152                         projectRepository,
    153                         frameworkRepository,
    154                         shell);
    155 
    156                 dlg.setCurrentResource(currentValue);
    157 
    158                 if (dlg.open() == Window.OK) {
    159                     return dlg.getCurrentResource();
    160                 }
    161             } else {
    162                 ReferenceChooserDialog dlg = new ReferenceChooserDialog(
    163                         project,
    164                         projectRepository,
    165                         shell);
    166 
    167                 dlg.setCurrentResource(currentValue);
    168 
    169                 if (dlg.open() == Window.OK) {
    170                     return dlg.getCurrentResource();
    171                 }
    172             }
    173         }
    174 
    175         return null;
    176     }
    177 
    178     /**
    179      * Gets all the values one could use to auto-complete a "resource" value in an XML
    180      * content assist.
    181      * <p/>
    182      * Typically the user is editing the value of an attribute in a resource XML, e.g.
    183      *   <pre> "&lt;Button android:test="@string/my_[caret]_string..." </pre>
    184      * <p/>
    185      *
    186      * "prefix" is the value that the user has typed so far (or more exactly whatever is on the
    187      * left side of the insertion point). In the example above it would be "@style/my_".
    188      * <p/>
    189      *
    190      * To avoid a huge long list of values, the completion works on two levels:
    191      * <ul>
    192      * <li> If a resource type as been typed so far (e.g. "@style/"), then limit the values to
    193      *      the possible completions that match this type.
    194      * <li> If no resource type as been typed so far, then return the various types that could be
    195      *      completed. So if the project has only strings and layouts resources, for example,
    196      *      the returned list will only include "@string/" and "@layout/".
    197      * </ul>
    198      *
    199      * Finally if anywhere in the string we find the special token "android:", we use the
    200      * current framework system resources rather than the project resources.
    201      * This works for both "@android:style/foo" and "@style/android:foo" conventions even though
    202      * the reconstructed name will always be of the former form.
    203      *
    204      * Note that "android:" here is a keyword specific to Android resources and should not be
    205      * mixed with an XML namespace for an XML attribute name.
    206      */
    207     @Override
    208     public String[] getPossibleValues(String prefix) {
    209         return computeResourceStringMatches(getUiParent().getEditor(), getDescriptor(), prefix);
    210     }
    211 
    212     public static String[] computeResourceStringMatches(AndroidXmlEditor editor,
    213             AttributeDescriptor attributeDescriptor, String prefix) {
    214         ResourceRepository repository = null;
    215         boolean isSystem = false;
    216 
    217         if (prefix == null || !prefix.regionMatches(1, ANDROID_PKG, 0, ANDROID_PKG.length())) {
    218             IProject project = editor.getProject();
    219             if (project != null) {
    220                 // get the resource repository for this project and the system resources.
    221                 repository = ResourceManager.getInstance().getProjectResources(project);
    222             }
    223         } else {
    224             // If there's a prefix with "android:" in it, use the system resources
    225             // Non-public framework resources are filtered out later.
    226             AndroidTargetData data = editor.getTargetData();
    227             repository = data.getFrameworkResources();
    228             isSystem = true;
    229         }
    230 
    231         // Get list of potential resource types, either specific to this project
    232         // or the generic list.
    233         Collection<ResourceType> resTypes = (repository != null) ?
    234                     repository.getAvailableResourceTypes() :
    235                     EnumSet.allOf(ResourceType.class);
    236 
    237         // Get the type name from the prefix, if any. It's any word before the / if there's one
    238         String typeName = null;
    239         if (prefix != null) {
    240             Matcher m = Pattern.compile(".*?([a-z]+)/.*").matcher(prefix);      //$NON-NLS-1$
    241             if (m.matches()) {
    242                 typeName = m.group(1);
    243             }
    244         }
    245 
    246         // Now collect results
    247         List<String> results = new ArrayList<String>();
    248 
    249         if (typeName == null) {
    250             // This prefix does not have a / in it, so the resource string is either empty
    251             // or does not have the resource type in it. Simply offer the list of potential
    252             // resource types.
    253 
    254             for (ResourceType resType : resTypes) {
    255                 if (isSystem) {
    256                     results.add(PREFIX_ANDROID_RESOURCE_REF + resType.getName() + '/');
    257                 } else {
    258                     results.add('@' + resType.getName() + '/');
    259                 }
    260                 if (resType == ResourceType.ID) {
    261                     // Also offer the + version to create an id from scratch
    262                     results.add("@+" + resType.getName() + '/');    //$NON-NLS-1$
    263                 }
    264             }
    265 
    266             // Also add in @android: prefix to completion such that if user has typed
    267             // "@an" we offer to complete it.
    268             if (prefix == null ||
    269                     ANDROID_PKG.regionMatches(0, prefix, 1, prefix.length() - 1)) {
    270                 results.add(PREFIX_ANDROID_RESOURCE_REF);
    271             }
    272         } else if (repository != null) {
    273             // We have a style name and a repository. Find all resources that match this
    274             // type and recreate suggestions out of them.
    275 
    276             ResourceType resType = ResourceType.getEnum(typeName);
    277             if (resType != null) {
    278                 StringBuilder sb = new StringBuilder();
    279                 sb.append('@');
    280                 if (prefix != null && prefix.indexOf('+') >= 0) {
    281                     sb.append('+');
    282                 }
    283 
    284                 if (isSystem) {
    285                     sb.append(ANDROID_PKG).append(':');
    286                 }
    287 
    288                 sb.append(typeName).append('/');
    289                 String base = sb.toString();
    290 
    291                 for (ResourceItem item : repository.getResourceItemsOfType(resType)) {
    292                     results.add(base + item.getName());
    293                 }
    294             }
    295         }
    296 
    297         if (attributeDescriptor != null) {
    298             sortAttributeChoices(attributeDescriptor, results);
    299         } else {
    300             Collections.sort(results);
    301         }
    302 
    303         return results.toArray(new String[results.size()]);
    304     }
    305 
    306     /**
    307      * Attempts to sort the attribute values to bubble up the most likely choices to
    308      * the top.
    309      * <p>
    310      * For example, if you are editing a style attribute, it's likely that among the
    311      * resource values you would rather see @style or @android than @string.
    312      */
    313     private static void sortAttributeChoices(AttributeDescriptor descriptor,
    314             List<String> choices) {
    315         final IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
    316         Collections.sort(choices, new Comparator<String>() {
    317             public int compare(String s1, String s2) {
    318                 int compare = score(attributeInfo, s1) - score(attributeInfo, s2);
    319                 if (compare == 0) {
    320                     // Sort alphabetically as a fallback
    321                     compare = s1.compareTo(s2);
    322                 }
    323                 return compare;
    324             }
    325         });
    326     }
    327 
    328     /** Compute a suitable sorting score for the given  */
    329     private static final int score(IAttributeInfo attributeInfo, String value) {
    330         if (value.equals(PREFIX_ANDROID_RESOURCE_REF)) {
    331             return -1;
    332         }
    333 
    334         for (Format format : attributeInfo.getFormats()) {
    335             String type = null;
    336             switch (format) {
    337                 case BOOLEAN:
    338                     type = "bool"; //$NON-NLS-1$
    339                     break;
    340                 case COLOR:
    341                     type = "color"; //$NON-NLS-1$
    342                     break;
    343                 case DIMENSION:
    344                     type = "dimen"; //$NON-NLS-1$
    345                     break;
    346                 case INTEGER:
    347                     type = "integer"; //$NON-NLS-1$
    348                     break;
    349                 case STRING:
    350                     type = "string"; //$NON-NLS-1$
    351                     break;
    352                 // default: REFERENCE, FLAG, ENUM, etc - don't have type info about individual
    353                 // elements to help make a decision
    354             }
    355 
    356             if (type != null) {
    357                 if (value.startsWith('@' + type + '/')) {
    358                     return -2;
    359                 }
    360 
    361                 if (value.startsWith(PREFIX_ANDROID_RESOURCE_REF + type + '/')) {
    362                     return -2;
    363                 }
    364             }
    365         }
    366 
    367         // Handle a few more cases not covered by the Format metadata check
    368         String type = null;
    369 
    370         String attribute = attributeInfo.getName();
    371         if (attribute.equals(ATTR_ID)) {
    372             type = "id"; //$NON-NLS-1$
    373         } else if (attribute.equals(ATTR_STYLE)) {
    374             type = "style"; //$NON-NLS-1$
    375         } else if (attribute.equals(LayoutDescriptors.ATTR_LAYOUT)) {
    376             type = "layout"; //$NON-NLS-1$
    377         } else if (attribute.equals("drawable")) { //$NON-NLS-1$
    378             type = "drawable"; //$NON-NLS-1$
    379         }
    380 
    381         if (type != null) {
    382             if (value.startsWith('@' + type + '/')) {
    383                 return -2;
    384             }
    385 
    386             if (value.startsWith(PREFIX_ANDROID_RESOURCE_REF + type + '/')) {
    387                 return -2;
    388             }
    389         }
    390 
    391         return 0;
    392     }
    393 }
    394