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      */
UiResourceAttributeNode(ResourceType type, AttributeDescriptor attributeDescriptor, UiElementNode uiParent)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
createUiControl(final Composite parent, IManagedForm managedForm)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
showDialog(@onNull Shell shell, @Nullable String currentValue)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
getPossibleValues(String prefix)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
computeResourceStringMatches( @onNull AndroidXmlEditor editor, @Nullable AttributeDescriptor descriptor, @Nullable String prefix)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
computeResourceStringMatches( @ullable AttributeDescriptor attributeDescriptor, @Nullable String prefix, @NonNull ResourceRepository repository, boolean isSystem)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      */
sortAttributeChoices(AttributeDescriptor descriptor, List<String> choices)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  */
score(IAttributeInfo attributeInfo, String value)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