• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
UiResourceAttributeNode(ResourceType type, AttributeDescriptor attributeDescriptor, UiElementNode uiParent)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
createUiControl(final Composite parent, IManagedForm managedForm)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      */
showDialog(Shell shell, String currentValue)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
getPossibleValues(String prefix)208     public String[] getPossibleValues(String prefix) {
209         return computeResourceStringMatches(getUiParent().getEditor(), getDescriptor(), prefix);
210     }
211 
computeResourceStringMatches(AndroidXmlEditor editor, AttributeDescriptor attributeDescriptor, String prefix)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      * @param descriptor the descriptor that the resource values are being completed for,
313      *          used to prioritize some of the resource types
314      * @param choices the set of string resource values
315      */
sortAttributeChoices(AttributeDescriptor descriptor, List<String> choices)316     public static void sortAttributeChoices(AttributeDescriptor descriptor,
317             List<String> choices) {
318         final IAttributeInfo attributeInfo = descriptor.getAttributeInfo();
319         Collections.sort(choices, new Comparator<String>() {
320             @Override
321             public int compare(String s1, String s2) {
322                 int compare = score(attributeInfo, s1) - score(attributeInfo, s2);
323                 if (compare == 0) {
324                     // Sort alphabetically as a fallback
325                     compare = s1.compareToIgnoreCase(s2);
326                 }
327                 return compare;
328             }
329         });
330     }
331 
332     /** Compute a suitable sorting score for the given  */
score(IAttributeInfo attributeInfo, String value)333     private static final int score(IAttributeInfo attributeInfo, String value) {
334         if (value.equals(PREFIX_ANDROID_RESOURCE_REF)) {
335             return -1;
336         }
337 
338         for (Format format : attributeInfo.getFormats()) {
339             String type = null;
340             switch (format) {
341                 case BOOLEAN:
342                     type = "bool"; //$NON-NLS-1$
343                     break;
344                 case COLOR:
345                     type = "color"; //$NON-NLS-1$
346                     break;
347                 case DIMENSION:
348                     type = "dimen"; //$NON-NLS-1$
349                     break;
350                 case INTEGER:
351                     type = "integer"; //$NON-NLS-1$
352                     break;
353                 case STRING:
354                     type = "string"; //$NON-NLS-1$
355                     break;
356                 // default: REFERENCE, FLAG, ENUM, etc - don't have type info about individual
357                 // elements to help make a decision
358             }
359 
360             if (type != null) {
361                 if (value.startsWith('@' + type + '/')) {
362                     return -2;
363                 }
364 
365                 if (value.startsWith(PREFIX_ANDROID_RESOURCE_REF + type + '/')) {
366                     return -2;
367                 }
368             }
369         }
370 
371         // Handle a few more cases not covered by the Format metadata check
372         String type = null;
373 
374         String attribute = attributeInfo.getName();
375         if (attribute.equals(ATTR_ID)) {
376             type = "id"; //$NON-NLS-1$
377         } else if (attribute.equals(ATTR_STYLE)) {
378             type = "style"; //$NON-NLS-1$
379         } else if (attribute.equals(LayoutDescriptors.ATTR_LAYOUT)) {
380             type = "layout"; //$NON-NLS-1$
381         } else if (attribute.equals("drawable")) { //$NON-NLS-1$
382             type = "drawable"; //$NON-NLS-1$
383         } else if (attribute.equals("entries")) { //$NON-NLS-1$
384             // Spinner
385             type = "array";    //$NON-NLS-1$
386         }
387 
388         if (type != null) {
389             if (value.startsWith('@' + type + '/')) {
390                 return -2;
391             }
392 
393             if (value.startsWith(PREFIX_ANDROID_RESOURCE_REF + type + '/')) {
394                 return -2;
395             }
396         }
397 
398         return 0;
399     }
400 }
401