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.SdkConstants.ANDROID_PKG;
     20 import static com.android.SdkConstants.ANDROID_PREFIX;
     21 import static com.android.SdkConstants.ANDROID_THEME_PREFIX;
     22 import static com.android.SdkConstants.ATTR_ID;
     23 import static com.android.SdkConstants.ATTR_LAYOUT;
     24 import static com.android.SdkConstants.ATTR_STYLE;
     25 import static com.android.SdkConstants.PREFIX_RESOURCE_REF;
     26 import static com.android.SdkConstants.PREFIX_THEME_REF;
     27 
     28 import com.android.annotations.NonNull;
     29 import com.android.annotations.Nullable;
     30 import com.android.ide.common.api.IAttributeInfo;
     31 import com.android.ide.common.api.IAttributeInfo.Format;
     32 import com.android.ide.common.resources.ResourceItem;
     33 import com.android.ide.common.resources.ResourceRepository;
     34 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     35 import com.android.ide.eclipse.adt.internal.editors.descriptors.AttributeDescriptor;
     36 import com.android.ide.eclipse.adt.internal.editors.descriptors.DescriptorsUtils;
     37 import com.android.ide.eclipse.adt.internal.editors.descriptors.TextAttributeDescriptor;
     38 import com.android.ide.eclipse.adt.internal.editors.ui.SectionHelper;
     39 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager;
     40 import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData;
     41 import com.android.ide.eclipse.adt.internal.sdk.ProjectState;
     42 import com.android.ide.eclipse.adt.internal.sdk.Sdk;
     43 import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog;
     44 import com.android.ide.eclipse.adt.internal.ui.ResourceChooser;
     45 import com.android.resources.ResourceType;
     46 
     47 import org.eclipse.core.resources.IProject;
     48 import org.eclipse.jface.window.Window;
     49 import org.eclipse.swt.SWT;
     50 import org.eclipse.swt.events.SelectionAdapter;
     51 import org.eclipse.swt.events.SelectionEvent;
     52 import org.eclipse.swt.layout.GridData;
     53 import org.eclipse.swt.layout.GridLayout;
     54 import org.eclipse.swt.widgets.Button;
     55 import org.eclipse.swt.widgets.Composite;
     56 import org.eclipse.swt.widgets.Label;
     57 import org.eclipse.swt.widgets.Shell;
     58 import org.eclipse.swt.widgets.Text;
     59 import org.eclipse.ui.forms.IManagedForm;
     60 import org.eclipse.ui.forms.widgets.FormToolkit;
     61 import org.eclipse.ui.forms.widgets.TableWrapData;
     62 
     63 import java.util.ArrayList;
     64 import java.util.Arrays;
     65 import java.util.Collection;
     66 import java.util.Collections;
     67 import java.util.Comparator;
     68 import java.util.EnumSet;
     69 import java.util.HashSet;
     70 import java.util.List;
     71 import java.util.Set;
     72 import java.util.regex.Matcher;
     73 import java.util.regex.Pattern;
     74 
     75 /**
     76  * Represents an XML attribute for a resource that can be modified using a simple text field or
     77  * a dialog to choose an existing resource.
     78  * <p/>
     79  * It can be configured to represent any kind of resource, by providing the desired
     80  * {@link ResourceType} in the constructor.
     81  * <p/>
     82  * See {@link UiTextAttributeNode} for more information.
     83  */
     84 public class UiResourceAttributeNode extends UiTextAttributeNode {
     85     private ResourceType mType;
     86 
     87     /**
     88      * Creates a new {@linkplain UiResourceAttributeNode}
     89      *
     90      * @param type the associated resource type
     91      * @param attributeDescriptor the attribute descriptor for this attribute
     92      * @param uiParent the parent ui node, if any
     93      */
     94     public UiResourceAttributeNode(ResourceType type,
     95             AttributeDescriptor attributeDescriptor, UiElementNode uiParent) {
     96         super(attributeDescriptor, uiParent);
     97 
     98         mType = type;
     99     }
    100 
    101     /* (non-java doc)
    102      * Creates a label widget and an associated text field.
    103      * <p/>
    104      * As most other parts of the android manifest editor, this assumes the
    105      * parent uses a table layout with 2 columns.
    106      */
    107     @Override
    108     public void createUiControl(final Composite parent, IManagedForm managedForm) {
    109         setManagedForm(managedForm);
    110         FormToolkit toolkit = managedForm.getToolkit();
    111         TextAttributeDescriptor desc = (TextAttributeDescriptor) getDescriptor();
    112 
    113         Label label = toolkit.createLabel(parent, desc.getUiName());
    114         label.setLayoutData(new TableWrapData(TableWrapData.LEFT, TableWrapData.MIDDLE));
    115         SectionHelper.addControlTooltip(label, DescriptorsUtils.formatTooltip(desc.getTooltip()));
    116 
    117         Composite composite = toolkit.createComposite(parent);
    118         composite.setLayoutData(new TableWrapData(TableWrapData.FILL_GRAB, TableWrapData.MIDDLE));
    119         GridLayout gl = new GridLayout(2, false);
    120         gl.marginHeight = gl.marginWidth = 0;
    121         composite.setLayout(gl);
    122         // Fixes missing text borders under GTK... also requires adding a 1-pixel margin
    123         // for the text field below
    124         toolkit.paintBordersFor(composite);
    125 
    126         final Text text = toolkit.createText(composite, getCurrentValue());
    127         GridData gd = new GridData(GridData.FILL_HORIZONTAL);
    128         gd.horizontalIndent = 1;  // Needed by the fixed composite borders under GTK
    129         text.setLayoutData(gd);
    130         Button browseButton = toolkit.createButton(composite, "Browse...", SWT.PUSH);
    131 
    132         setTextWidget(text);
    133 
    134         // TODO Add a validator using onAddModifyListener
    135 
    136         browseButton.addSelectionListener(new SelectionAdapter() {
    137             @Override
    138             public void widgetSelected(SelectionEvent e) {
    139                 String result = showDialog(parent.getShell(), text.getText().trim());
    140                 if (result != null) {
    141                     text.setText(result);
    142                 }
    143             }
    144         });
    145     }
    146 
    147     /**
    148      * Shows a dialog letting the user choose a set of enum, and returns a
    149      * string containing the result.
    150      *
    151      * @param shell the parent shell
    152      * @param currentValue an initial value, if any
    153      * @return the chosen string, or null
    154      */
    155     @Nullable
    156     public String showDialog(@NonNull Shell shell, @Nullable String currentValue) {
    157         // we need to get the project of the file being edited.
    158         UiElementNode uiNode = getUiParent();
    159         AndroidXmlEditor editor = uiNode.getEditor();
    160         IProject project = editor.getProject();
    161         if (project != null) {
    162             // get the resource repository for this project and the system resources.
    163             ResourceRepository projectRepository =
    164                 ResourceManager.getInstance().getProjectResources(project);
    165 
    166             if (mType != null) {
    167                 // get the Target Data to get the system resources
    168                 AndroidTargetData data = editor.getTargetData();
    169                 ResourceChooser dlg = ResourceChooser.create(project, mType, data, shell)
    170                     .setCurrentResource(currentValue);
    171                 if (dlg.open() == Window.OK) {
    172                     return dlg.getCurrentResource();
    173                 }
    174             } else {
    175                 ReferenceChooserDialog dlg = new ReferenceChooserDialog(
    176                         project,
    177                         projectRepository,
    178                         shell);
    179 
    180                 dlg.setCurrentResource(currentValue);
    181 
    182                 if (dlg.open() == Window.OK) {
    183                     return dlg.getCurrentResource();
    184                 }
    185             }
    186         }
    187 
    188         return null;
    189     }
    190 
    191     /**
    192      * Gets all the values one could use to auto-complete a "resource" value in an XML
    193      * content assist.
    194      * <p/>
    195      * Typically the user is editing the value of an attribute in a resource XML, e.g.
    196      *   <pre> "&lt;Button android:test="@string/my_[caret]_string..." </pre>
    197      * <p/>
    198      *
    199      * "prefix" is the value that the user has typed so far (or more exactly whatever is on the
    200      * left side of the insertion point). In the example above it would be "@style/my_".
    201      * <p/>
    202      *
    203      * To avoid a huge long list of values, the completion works on two levels:
    204      * <ul>
    205      * <li> If a resource type as been typed so far (e.g. "@style/"), then limit the values to
    206      *      the possible completions that match this type.
    207      * <li> If no resource type as been typed so far, then return the various types that could be
    208      *      completed. So if the project has only strings and layouts resources, for example,
    209      *      the returned list will only include "@string/" and "@layout/".
    210      * </ul>
    211      *
    212      * Finally if anywhere in the string we find the special token "android:", we use the
    213      * current framework system resources rather than the project resources.
    214      * This works for both "@android:style/foo" and "@style/android:foo" conventions even though
    215      * the reconstructed name will always be of the former form.
    216      *
    217      * Note that "android:" here is a keyword specific to Android resources and should not be
    218      * mixed with an XML namespace for an XML attribute name.
    219      */
    220     @Override
    221     public String[] getPossibleValues(String prefix) {
    222         return computeResourceStringMatches(getUiParent().getEditor(), getDescriptor(), prefix);
    223     }
    224 
    225     /**
    226      * Computes the set of resource string matches for a given resource prefix in a given editor
    227      *
    228      * @param editor the editor context
    229      * @param descriptor the attribute descriptor, if any
    230      * @param prefix the prefix, if any
    231      * @return an array of resource string matches
    232      */
    233     @Nullable
    234     public static String[] computeResourceStringMatches(
    235             @NonNull AndroidXmlEditor editor,
    236             @Nullable AttributeDescriptor descriptor,
    237             @Nullable String prefix) {
    238 
    239         if (prefix == null || !prefix.regionMatches(1, ANDROID_PKG, 0, ANDROID_PKG.length())) {
    240             IProject project = editor.getProject();
    241             if (project != null) {
    242                 // get the resource repository for this project and the system resources.
    243                 ResourceManager resourceManager = ResourceManager.getInstance();
    244                 ResourceRepository repository = resourceManager.getProjectResources(project);
    245 
    246                 List<IProject> libraries = null;
    247                 ProjectState projectState = Sdk.getProjectState(project);
    248                 if (projectState != null) {
    249                     libraries = projectState.getFullLibraryProjects();
    250                 }
    251 
    252                 String[] projectMatches = computeResourceStringMatches(descriptor, prefix,
    253                         repository, false);
    254 
    255                 if (libraries == null || libraries.isEmpty()) {
    256                     return projectMatches;
    257                 }
    258 
    259                 // Also compute matches for each of the libraries, and combine them
    260                 Set<String> matches = new HashSet<String>(200);
    261                 for (String s : projectMatches) {
    262                     matches.add(s);
    263                 }
    264 
    265                 for (IProject library : libraries) {
    266                     repository = resourceManager.getProjectResources(library);
    267                     projectMatches = computeResourceStringMatches(descriptor, prefix,
    268                             repository, false);
    269                     for (String s : projectMatches) {
    270                         matches.add(s);
    271                     }
    272                 }
    273 
    274                 String[] sorted = matches.toArray(new String[matches.size()]);
    275                 Arrays.sort(sorted);
    276                 return sorted;
    277             }
    278         } else {
    279             // If there's a prefix with "android:" in it, use the system resources
    280             // Non-public framework resources are filtered out later.
    281             AndroidTargetData data = editor.getTargetData();
    282             if (data != null) {
    283                 ResourceRepository repository = data.getFrameworkResources();
    284                 return computeResourceStringMatches(descriptor, prefix, repository, true);
    285             }
    286         }
    287 
    288         return null;
    289     }
    290 
    291     /**
    292      * Computes the set of resource string matches for a given prefix and a
    293      * given resource repository
    294      *
    295      * @param attributeDescriptor the attribute descriptor, if any
    296      * @param prefix the prefix, if any
    297      * @param repository the repository to seaerch in
    298      * @param isSystem if true, the repository contains framework repository,
    299      *            otherwise it contains project repositories
    300      * @return an array of resource string matches
    301      */
    302     @NonNull
    303     public static String[] computeResourceStringMatches(
    304             @Nullable AttributeDescriptor attributeDescriptor,
    305             @Nullable String prefix,
    306             @NonNull ResourceRepository repository,
    307             boolean isSystem) {
    308         // Get list of potential resource types, either specific to this project
    309         // or the generic list.
    310         Collection<ResourceType> resTypes = (repository != null) ?
    311                     repository.getAvailableResourceTypes() :
    312                     EnumSet.allOf(ResourceType.class);
    313 
    314         // Get the type name from the prefix, if any. It's any word before the / if there's one
    315         String typeName = null;
    316         if (prefix != null) {
    317             Matcher m = Pattern.compile(".*?([a-z]+)/.*").matcher(prefix);      //$NON-NLS-1$
    318             if (m.matches()) {
    319                 typeName = m.group(1);
    320             }
    321         }
    322 
    323         // Now collect results
    324         List<String> results = new ArrayList<String>();
    325 
    326         if (typeName == null) {
    327             // This prefix does not have a / in it, so the resource string is either empty
    328             // or does not have the resource type in it. Simply offer the list of potential
    329             // resource types.
    330             if (prefix != null && prefix.startsWith(PREFIX_THEME_REF)) {
    331                 results.add(ANDROID_THEME_PREFIX + ResourceType.ATTR.getName() + '/');
    332                 if (resTypes.contains(ResourceType.ATTR)
    333                         || resTypes.contains(ResourceType.STYLE)) {
    334                     results.add(PREFIX_THEME_REF + ResourceType.ATTR.getName() + '/');
    335                     if (prefix != null && prefix.startsWith(ANDROID_THEME_PREFIX)) {
    336                         // including attr isn't required
    337                         for (ResourceItem item : repository.getResourceItemsOfType(
    338                                 ResourceType.ATTR)) {
    339                             results.add(ANDROID_THEME_PREFIX + item.getName());
    340                         }
    341                     }
    342                 }
    343                 return results.toArray(new String[results.size()]);
    344             }
    345 
    346             for (ResourceType resType : resTypes) {
    347                 if (isSystem) {
    348                     results.add(ANDROID_PREFIX + resType.getName() + '/');
    349                 } else {
    350                     results.add('@' + resType.getName() + '/');
    351                 }
    352                 if (resType == ResourceType.ID) {
    353                     // Also offer the + version to create an id from scratch
    354                     results.add("@+" + resType.getName() + '/');    //$NON-NLS-1$
    355                 }
    356             }
    357 
    358             // Also add in @android: prefix to completion such that if user has typed
    359             // "@an" we offer to complete it.
    360             if (prefix == null ||
    361                     ANDROID_PKG.regionMatches(0, prefix, 1, prefix.length() - 1)) {
    362                 results.add(ANDROID_PREFIX);
    363             }
    364         } else if (repository != null) {
    365             // We have a style name and a repository. Find all resources that match this
    366             // type and recreate suggestions out of them.
    367 
    368             String initial = prefix != null && prefix.startsWith(PREFIX_THEME_REF)
    369                     ? PREFIX_THEME_REF : PREFIX_RESOURCE_REF;
    370             ResourceType resType = ResourceType.getEnum(typeName);
    371             if (resType != null) {
    372                 StringBuilder sb = new StringBuilder();
    373                 sb.append(initial);
    374                 if (prefix != null && prefix.indexOf('+') >= 0) {
    375                     sb.append('+');
    376                 }
    377 
    378                 if (isSystem) {
    379                     sb.append(ANDROID_PKG).append(':');
    380                 }
    381 
    382                 sb.append(typeName).append('/');
    383                 String base = sb.toString();
    384 
    385                 for (ResourceItem item : repository.getResourceItemsOfType(resType)) {
    386                     results.add(base + item.getName());
    387                 }
    388 
    389                 if (!isSystem && resType == ResourceType.ATTR) {
    390                     for (ResourceItem item : repository.getResourceItemsOfType(
    391                             ResourceType.STYLE)) {
    392                         results.add(base + item.getName());
    393                     }
    394                 }
    395             }
    396         }
    397 
    398         if (attributeDescriptor != null) {
    399             sortAttributeChoices(attributeDescriptor, results);
    400         } else {
    401             Collections.sort(results);
    402         }
    403 
    404         return results.toArray(new String[results.size()]);
    405     }
    406 
    407     /**
    408      * Attempts to sort the attribute values to bubble up the most likely choices to
    409      * the top.
    410      * <p>
    411      * For example, if you are editing a style attribute, it's likely that among the
    412      * resource values you would rather see @style or @android than @string.
    413      * @param descriptor the descriptor that the resource values are being completed for,
    414      *          used to prioritize some of the resource types
    415      * @param choices the set of string resource values
    416      */
    417     public static void sortAttributeChoices(AttributeDescriptor descriptor,
    418             List<String> choices) {
    419         final IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
    420         Collections.sort(choices, new Comparator<String>() {
    421             @Override
    422             public int compare(String s1, String s2) {
    423                 int compare = score(attributeInfo, s1) - score(attributeInfo, s2);
    424                 if (compare == 0) {
    425                     // Sort alphabetically as a fallback
    426                     compare = s1.compareToIgnoreCase(s2);
    427                 }
    428                 return compare;
    429             }
    430         });
    431     }
    432 
    433     /** Compute a suitable sorting score for the given  */
    434     private static final int score(IAttributeInfo attributeInfo, String value) {
    435         if (value.equals(ANDROID_PREFIX)) {
    436             return -1;
    437         }
    438 
    439         for (Format format : attributeInfo.getFormats()) {
    440             String type = null;
    441             switch (format) {
    442                 case BOOLEAN:
    443                     type = "bool"; //$NON-NLS-1$
    444                     break;
    445                 case COLOR:
    446                     type = "color"; //$NON-NLS-1$
    447                     break;
    448                 case DIMENSION:
    449                     type = "dimen"; //$NON-NLS-1$
    450                     break;
    451                 case INTEGER:
    452                     type = "integer"; //$NON-NLS-1$
    453                     break;
    454                 case STRING:
    455                     type = "string"; //$NON-NLS-1$
    456                     break;
    457                 // default: REFERENCE, FLAG, ENUM, etc - don't have type info about individual
    458                 // elements to help make a decision
    459             }
    460 
    461             if (type != null) {
    462                 if (value.startsWith(PREFIX_RESOURCE_REF)) {
    463                     if (value.startsWith(PREFIX_RESOURCE_REF + type + '/')) {
    464                         return -2;
    465                     }
    466 
    467                     if (value.startsWith(ANDROID_PREFIX + type + '/')) {
    468                         return -2;
    469                     }
    470                 }
    471                 if (value.startsWith(PREFIX_THEME_REF)) {
    472                     if (value.startsWith(PREFIX_THEME_REF + type + '/')) {
    473                         return -2;
    474                     }
    475 
    476                     if (value.startsWith(ANDROID_THEME_PREFIX + type + '/')) {
    477                         return -2;
    478                     }
    479                 }
    480             }
    481         }
    482 
    483         // Handle a few more cases not covered by the Format metadata check
    484         String type = null;
    485 
    486         String attribute = attributeInfo.getName();
    487         if (attribute.equals(ATTR_ID)) {
    488             type = "id"; //$NON-NLS-1$
    489         } else if (attribute.equals(ATTR_STYLE)) {
    490             type = "style"; //$NON-NLS-1$
    491         } else if (attribute.equals(ATTR_LAYOUT)) {
    492             type = "layout"; //$NON-NLS-1$
    493         } else if (attribute.equals("drawable")) { //$NON-NLS-1$
    494             type = "drawable"; //$NON-NLS-1$
    495         } else if (attribute.equals("entries")) { //$NON-NLS-1$
    496             // Spinner
    497             type = "array";    //$NON-NLS-1$
    498         }
    499 
    500         if (type != null) {
    501             if (value.startsWith(PREFIX_RESOURCE_REF)) {
    502                 if (value.startsWith(PREFIX_RESOURCE_REF + type + '/')) {
    503                     return -2;
    504                 }
    505 
    506                 if (value.startsWith(ANDROID_PREFIX + type + '/')) {
    507                     return -2;
    508                 }
    509             }
    510             if (value.startsWith(PREFIX_THEME_REF)) {
    511                 if (value.startsWith(PREFIX_THEME_REF + type + '/')) {
    512                     return -2;
    513                 }
    514 
    515                 if (value.startsWith(ANDROID_THEME_PREFIX + type + '/')) {
    516                     return -2;
    517                 }
    518             }
    519         }
    520 
    521         return 0;
    522     }
    523 }
    524