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> "<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