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