1 /* 2 * Copyright (C) 2012 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.layout.properties; 18 19 import static com.android.SdkConstants.ANDROID_PREFIX; 20 import static com.android.SdkConstants.ANDROID_THEME_PREFIX; 21 import static com.android.SdkConstants.ATTR_ID; 22 import static com.android.SdkConstants.DOT_PNG; 23 import static com.android.SdkConstants.DOT_XML; 24 import static com.android.SdkConstants.NEW_ID_PREFIX; 25 import static com.android.SdkConstants.PREFIX_RESOURCE_REF; 26 import static com.android.SdkConstants.PREFIX_THEME_REF; 27 import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix; 28 29 import com.android.annotations.NonNull; 30 import com.android.ide.common.api.IAttributeInfo; 31 import com.android.ide.common.api.IAttributeInfo.Format; 32 import com.android.ide.common.layout.BaseViewRule; 33 import com.android.ide.common.rendering.api.ResourceValue; 34 import com.android.ide.common.resources.ResourceRepository; 35 import com.android.ide.common.resources.ResourceResolver; 36 import com.android.ide.eclipse.adt.AdtPlugin; 37 import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; 38 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 39 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 40 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; 41 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; 42 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; 43 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; 44 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; 45 import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; 46 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 47 import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; 48 import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; 49 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; 50 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 51 import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog; 52 import com.android.ide.eclipse.adt.internal.ui.ResourceChooser; 53 import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper; 54 import com.android.resources.ResourceType; 55 import com.google.common.collect.Maps; 56 57 import org.eclipse.core.resources.IProject; 58 import org.eclipse.core.runtime.CoreException; 59 import org.eclipse.core.runtime.QualifiedName; 60 import org.eclipse.jface.dialogs.IDialogConstants; 61 import org.eclipse.jface.dialogs.MessageDialogWithToggle; 62 import org.eclipse.jface.preference.IPreferenceStore; 63 import org.eclipse.jface.window.Window; 64 import org.eclipse.swt.graphics.Color; 65 import org.eclipse.swt.graphics.GC; 66 import org.eclipse.swt.graphics.Image; 67 import org.eclipse.swt.graphics.ImageData; 68 import org.eclipse.swt.graphics.Point; 69 import org.eclipse.swt.graphics.RGB; 70 import org.eclipse.swt.widgets.Shell; 71 import org.eclipse.wb.draw2d.IColorConstants; 72 import org.eclipse.wb.internal.core.model.property.Property; 73 import org.eclipse.wb.internal.core.model.property.editor.AbstractTextPropertyEditor; 74 import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation; 75 import org.eclipse.wb.internal.core.model.property.editor.presentation.PropertyEditorPresentation; 76 import org.eclipse.wb.internal.core.model.property.table.PropertyTable; 77 import org.eclipse.wb.internal.core.utils.ui.DrawUtils; 78 79 import java.awt.image.BufferedImage; 80 import java.io.File; 81 import java.io.IOException; 82 import java.util.ArrayList; 83 import java.util.EnumSet; 84 import java.util.List; 85 import java.util.Map; 86 87 import javax.imageio.ImageIO; 88 89 /** 90 * Special property editor used for the {@link XmlProperty} instances which handles 91 * editing the XML properties, rendering defaults by looking up the actual colors and images, 92 */ 93 class XmlPropertyEditor extends AbstractTextPropertyEditor { 94 public static final XmlPropertyEditor INSTANCE = new XmlPropertyEditor(); 95 private static final int SAMPLE_SIZE = 10; 96 private static final int SAMPLE_MARGIN = 3; 97 98 protected XmlPropertyEditor() { 99 } 100 101 private final PropertyEditorPresentation mPresentation = 102 new ButtonPropertyEditorPresentation() { 103 @Override 104 protected void onClick(PropertyTable propertyTable, Property property) throws Exception { 105 openDialog(propertyTable, property); 106 } 107 }; 108 109 @Override 110 public PropertyEditorPresentation getPresentation() { 111 return mPresentation; 112 } 113 114 @Override 115 public String getText(Property property) throws Exception { 116 Object value = property.getValue(); 117 if (value instanceof String) { 118 return (String) value; 119 } 120 return null; 121 } 122 123 @Override 124 protected String getEditorText(Property property) throws Exception { 125 return getText(property); 126 } 127 128 @Override 129 public void paint(Property property, GC gc, int x, int y, int width, int height) 130 throws Exception { 131 String text = getText(property); 132 if (text != null) { 133 ResourceValue resValue = null; 134 String resolvedText = null; 135 136 // TODO: Use the constants for @, ?, @android: etc 137 if (text.startsWith("@") || text.startsWith("?")) { //$NON-NLS-1$ //$NON-NLS-2$ 138 // Yes, try to resolve it in order to show better info 139 XmlProperty xmlProperty = (XmlProperty) property; 140 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 141 if (graphicalEditor != null) { 142 ResourceResolver resolver = graphicalEditor.getResourceResolver(); 143 boolean isFramework = text.startsWith(ANDROID_PREFIX) 144 || text.startsWith(ANDROID_THEME_PREFIX); 145 resValue = resolver.findResValue(text, isFramework); 146 while (resValue != null && resValue.getValue() != null) { 147 String value = resValue.getValue(); 148 if (value.startsWith(PREFIX_RESOURCE_REF) 149 || value.startsWith(PREFIX_THEME_REF)) { 150 // TODO: do I have to strip off the @ too? 151 isFramework = isFramework 152 || value.startsWith(ANDROID_PREFIX) 153 || value.startsWith(ANDROID_THEME_PREFIX); 154 ResourceValue v = resolver.findResValue(text, isFramework); 155 if (v != null && !value.equals(v.getValue())) { 156 resValue = v; 157 } else { 158 break; 159 } 160 } else { 161 break; 162 } 163 } 164 } 165 } else if (text.startsWith("#") && text.matches("#\\p{XDigit}+")) { //$NON-NLS-1$ 166 resValue = new ResourceValue(ResourceType.COLOR, property.getName(), text, false); 167 } 168 169 if (resValue != null && resValue.getValue() != null) { 170 String value = resValue.getValue(); 171 // Decide whether it's a color, an image, a nine patch etc 172 // and decide how to render it 173 if (value.startsWith("#") || value.endsWith(DOT_XML) //$NON-NLS-1$ 174 && value.contains("res/color")) { //$NON-NLS-1$ // TBD: File.separator? 175 XmlProperty xmlProperty = (XmlProperty) property; 176 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 177 if (graphicalEditor != null) { 178 ResourceResolver resolver = graphicalEditor.getResourceResolver(); 179 RGB rgb = ResourceHelper.resolveColor(resolver, resValue); 180 if (rgb != null) { 181 Color color = new Color(gc.getDevice(), rgb); 182 // draw color sample 183 Color oldBackground = gc.getBackground(); 184 Color oldForeground = gc.getForeground(); 185 try { 186 int width_c = SAMPLE_SIZE; 187 int height_c = SAMPLE_SIZE; 188 int x_c = x; 189 int y_c = y + (height - height_c) / 2; 190 // update rest bounds 191 int delta = SAMPLE_SIZE + SAMPLE_MARGIN; 192 x += delta; 193 width -= delta; 194 // fill 195 gc.setBackground(color); 196 gc.fillRectangle(x_c, y_c, width_c, height_c); 197 // draw line 198 gc.setForeground(IColorConstants.gray); 199 gc.drawRectangle(x_c, y_c, width_c, height_c); 200 } finally { 201 gc.setBackground(oldBackground); 202 gc.setForeground(oldForeground); 203 } 204 color.dispose(); 205 } 206 } 207 } else { 208 Image swtImage = null; 209 if (value.endsWith(DOT_XML) && value.contains("res/drawable")) { // TBD: Filesep? 210 Map<String, Image> cache = getImageCache(property); 211 swtImage = cache.get(value); 212 if (swtImage == null) { 213 XmlProperty xmlProperty = (XmlProperty) property; 214 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 215 RenderService service = RenderService.create(graphicalEditor); 216 service.setOverrideRenderSize(SAMPLE_SIZE, SAMPLE_SIZE); 217 BufferedImage drawable = service.renderDrawable(resValue); 218 if (drawable != null) { 219 swtImage = SwtUtils.convertToSwt(gc.getDevice(), drawable, 220 true /*transferAlpha*/, -1); 221 cache.put(value, swtImage); 222 } 223 } 224 } else if (value.endsWith(DOT_PNG)) { 225 // TODO: 9-patch handling? 226 //if (text.endsWith(DOT_9PNG)) { 227 // // 9-patch image: How do we paint this? 228 // URL url = new File(text).toURI().toURL(); 229 // NinePatch ninepatch = NinePatch.load(url, false /* ?? */); 230 // BufferedImage image = ninepatch.getImage(); 231 //} 232 Map<String, Image> cache = getImageCache(property); 233 swtImage = cache.get(value); 234 if (swtImage == null) { 235 File file = new File(value); 236 if (file.exists()) { 237 try { 238 BufferedImage awtImage = ImageIO.read(file); 239 if (awtImage != null && awtImage.getWidth() > 0 240 && awtImage.getHeight() > 0) { 241 awtImage = ImageUtils.cropBlank(awtImage, null); 242 if (awtImage != null) { 243 // Scale image 244 int imageWidth = awtImage.getWidth(); 245 int imageHeight = awtImage.getHeight(); 246 int maxWidth = 3 * height; 247 248 if (imageWidth > maxWidth || imageHeight > height) { 249 double scale = height / (double) imageHeight; 250 int scaledWidth = (int) (imageWidth * scale); 251 if (scaledWidth > maxWidth) { 252 scale = maxWidth / (double) imageWidth; 253 } 254 awtImage = ImageUtils.scale(awtImage, scale, 255 scale); 256 } 257 swtImage = SwtUtils.convertToSwt(gc.getDevice(), 258 awtImage, true /*transferAlpha*/, -1); 259 } 260 } 261 } catch (IOException e) { 262 AdtPlugin.log(e, value); 263 } 264 } 265 cache.put(value, swtImage); 266 } 267 268 } else if (value != null) { 269 // It's a normal string: if different from the text, paint 270 // it in parentheses, e.g. 271 // @string/foo: Foo Bar (probably cropped) 272 if (!value.equals(text) && !value.equals("@null")) { //$NON-NLS-1$ 273 resolvedText = value; 274 } 275 } 276 277 if (swtImage != null) { 278 // Make a square the size of the height 279 ImageData imageData = swtImage.getImageData(); 280 int imageWidth = imageData.width; 281 int imageHeight = imageData.height; 282 if (imageWidth > 0 && imageHeight > 0) { 283 gc.drawImage(swtImage, x, y + (height - imageHeight) / 2); 284 int delta = imageWidth + SAMPLE_MARGIN; 285 x += delta; 286 width -= delta; 287 } 288 } 289 } 290 } 291 292 DrawUtils.drawStringCV(gc, text, x, y, width, height); 293 294 if (resolvedText != null && resolvedText.length() > 0) { 295 Point size = gc.stringExtent(text); 296 x += size.x; 297 width -= size.x; 298 299 x += SAMPLE_MARGIN; 300 width -= SAMPLE_MARGIN; 301 302 if (width > 0) { 303 Color oldForeground = gc.getForeground(); 304 try { 305 gc.setForeground(PropertyTable.COLOR_PROPERTY_FG_DEFAULT); 306 DrawUtils.drawStringCV(gc, '(' + resolvedText + ')', x, y, width, height); 307 } finally { 308 gc.setForeground(oldForeground); 309 } 310 } 311 } 312 } 313 } 314 315 @Override 316 protected boolean setEditorText(Property property, String text) throws Exception { 317 Object oldValue = property.getValue(); 318 String old = oldValue != null ? oldValue.toString() : null; 319 320 // If users enters a new id without specifying the @id/@+id prefix, insert it 321 boolean isId = isIdProperty(property); 322 if (isId && !text.startsWith(PREFIX_RESOURCE_REF)) { 323 text = NEW_ID_PREFIX + text; 324 } 325 326 // Handle id refactoring: if you change an id, may want to update references too. 327 // Ask user. 328 if (isId && property instanceof XmlProperty 329 && old != null && !old.isEmpty() 330 && text != null && !text.isEmpty() 331 && !text.equals(old)) { 332 XmlProperty xmlProperty = (XmlProperty) property; 333 IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); 334 String refactorPref = store.getString(AdtPrefs.PREFS_REFACTOR_IDS); 335 boolean performRefactor = false; 336 Shell shell = AdtPlugin.getShell(); 337 if (refactorPref == null 338 || refactorPref.isEmpty() 339 || refactorPref.equals(MessageDialogWithToggle.PROMPT)) { 340 MessageDialogWithToggle dialog = 341 MessageDialogWithToggle.openYesNoCancelQuestion( 342 shell, 343 "Update References?", 344 "Update all references as well? " + 345 "This will update all XML references and Java R field references.", 346 "Do not show again", 347 false, 348 store, 349 AdtPrefs.PREFS_REFACTOR_IDS); 350 switch (dialog.getReturnCode()) { 351 case IDialogConstants.CANCEL_ID: 352 return false; 353 case IDialogConstants.YES_ID: 354 performRefactor = true; 355 break; 356 case IDialogConstants.NO_ID: 357 performRefactor = false; 358 break; 359 } 360 } else { 361 performRefactor = refactorPref.equals(MessageDialogWithToggle.ALWAYS); 362 } 363 if (performRefactor) { 364 CommonXmlEditor xmlEditor = xmlProperty.getXmlEditor(); 365 if (xmlEditor != null) { 366 IProject project = xmlEditor.getProject(); 367 if (project != null && shell != null) { 368 RenameResourceWizard.renameResource(shell, project, 369 ResourceType.ID, stripIdPrefix(old), stripIdPrefix(text), false); 370 } 371 } 372 } 373 } 374 375 property.setValue(text); 376 377 return true; 378 } 379 380 private static boolean isIdProperty(Property property) { 381 XmlProperty xmlProperty = (XmlProperty) property; 382 return xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID); 383 } 384 385 private void openDialog(PropertyTable propertyTable, Property property) throws Exception { 386 XmlProperty xmlProperty = (XmlProperty) property; 387 IAttributeInfo attributeInfo = xmlProperty.getDescriptor().getAttributeInfo(); 388 389 if (isIdProperty(property)) { 390 Object value = xmlProperty.getValue(); 391 if (value != null && !value.toString().isEmpty()) { 392 GraphicalEditorPart editor = xmlProperty.getGraphicalEditor(); 393 if (editor != null) { 394 LayoutCanvas canvas = editor.getCanvasControl(); 395 SelectionManager manager = canvas.getSelectionManager(); 396 397 NodeProxy primary = canvas.getNodeFactory().create(xmlProperty.getNode()); 398 if (primary != null) { 399 RenameResult result = manager.performRename(primary, null); 400 if (result.isCanceled()) { 401 return; 402 } else if (!result.isUnavailable()) { 403 String name = result.getName(); 404 String id = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(name); 405 xmlProperty.setValue(id); 406 return; 407 } 408 } 409 } 410 } 411 412 // When editing the id attribute, don't offer a resource chooser: usually 413 // you want to enter a *new* id here 414 attributeInfo = null; 415 } 416 417 boolean referenceAllowed = false; 418 if (attributeInfo != null) { 419 EnumSet<Format> formats = attributeInfo.getFormats(); 420 ResourceType type = null; 421 List<ResourceType> types = null; 422 if (formats.contains(Format.FLAG)) { 423 String[] flagValues = attributeInfo.getFlagValues(); 424 if (flagValues != null) { 425 FlagXmlPropertyDialog dialog = 426 new FlagXmlPropertyDialog(propertyTable.getShell(), 427 "Select Flag Values", false /* radio */, 428 flagValues, xmlProperty); 429 430 dialog.open(); 431 return; 432 } 433 } else if (formats.contains(Format.ENUM)) { 434 String[] enumValues = attributeInfo.getEnumValues(); 435 if (enumValues != null) { 436 FlagXmlPropertyDialog dialog = 437 new FlagXmlPropertyDialog(propertyTable.getShell(), 438 "Select Enum Value", true /* radio */, 439 enumValues, xmlProperty); 440 dialog.open(); 441 return; 442 } 443 } else { 444 for (Format format : formats) { 445 ResourceType t = format.getResourceType(); 446 if (t != null) { 447 if (type != null) { 448 if (types == null) { 449 types = new ArrayList<ResourceType>(); 450 types.add(type); 451 } 452 types.add(t); 453 } 454 type = t; 455 } else if (format == Format.REFERENCE) { 456 referenceAllowed = true; 457 } 458 } 459 } 460 if (types != null || referenceAllowed) { 461 // Multiple resource types (such as string *and* boolean): 462 // just use a reference chooser 463 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 464 if (graphicalEditor != null) { 465 LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate(); 466 IProject project = delegate.getEditor().getProject(); 467 if (project != null) { 468 // get the resource repository for this project and the system resources. 469 ResourceRepository projectRepository = 470 ResourceManager.getInstance().getProjectResources(project); 471 Shell shell = AdtPlugin.getShell(); 472 ReferenceChooserDialog dlg = new ReferenceChooserDialog( 473 project, 474 projectRepository, 475 shell); 476 dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor)); 477 478 String currentValue = (String) property.getValue(); 479 dlg.setCurrentResource(currentValue); 480 481 if (dlg.open() == Window.OK) { 482 String resource = dlg.getCurrentResource(); 483 if (resource != null) { 484 // Returns null for cancel, "" for clear and otherwise a new value 485 if (resource.length() > 0) { 486 property.setValue(resource); 487 } else { 488 property.setValue(null); 489 } 490 } 491 } 492 493 return; 494 } 495 } 496 } else if (type != null) { 497 // Single resource type: use a resource chooser 498 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 499 if (graphicalEditor != null) { 500 String currentValue = (String) property.getValue(); 501 // TODO: Add validator factory? 502 String resource = ResourceChooser.chooseResource(graphicalEditor, 503 type, currentValue, null /* validator */); 504 // Returns null for cancel, "" for clear and otherwise a new value 505 if (resource != null) { 506 if (resource.length() > 0) { 507 property.setValue(resource); 508 } else { 509 property.setValue(null); 510 } 511 } 512 } 513 514 return; 515 } 516 } 517 518 // Fallback: Just use a plain string editor 519 StringXmlPropertyDialog dialog = 520 new StringXmlPropertyDialog(propertyTable.getShell(), property); 521 if (dialog.open() == Window.OK) { 522 // TODO: Do I need to activate? 523 } 524 } 525 526 /** Qualified name for the per-project persistent property include-map */ 527 private final static QualifiedName CACHE_NAME = new QualifiedName(AdtPlugin.PLUGIN_ID, 528 "property-images");//$NON-NLS-1$ 529 530 @NonNull 531 private static Map<String, Image> getImageCache(@NonNull Property property) { 532 XmlProperty xmlProperty = (XmlProperty) property; 533 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 534 IProject project = graphicalEditor.getProject(); 535 try { 536 Map<String, Image> cache = (Map<String, Image>) project.getSessionProperty(CACHE_NAME); 537 if (cache == null) { 538 cache = Maps.newHashMap(); 539 project.setSessionProperty(CACHE_NAME, cache); 540 } 541 542 return cache; 543 } catch (CoreException e) { 544 AdtPlugin.log(e, null); 545 return Maps.newHashMap(); 546 } 547 } 548 } 549